Repository: 7mind/izumi
Pull request #996 (2020-04-08 17:07:26)

Monix BIO instances

codecov-commenter: # [Codecov](https://codecov.io/gh/7mind/izumi/pull/996?src=pr&el=h1) Report
> Merging [#996](https://codecov.io/gh/7mind/izumi/pull/996?src=pr&el=desc) into [develop](https://codecov.io/gh/7mind/izumi/commit/76857930a416f584a998a6a9461796960261e662&el=desc) will **decrease** coverage by `0.35%`.
> The diff coverage is `0.00%`.


```diff
@@             Coverage Diff             @@
##           develop     #996      +/-   ##
===========================================
- Coverage    54.27%   53.91%   -0.36%     
===========================================
  Files          402      402              
  Lines         7652     7536     -116     
  Branches       372      375       +3     
===========================================
- Hits          4153     4063      -90     
+ Misses        3499     3473      -26     
```

neko-kai: @VladPodilnyk 
Also we now need a test that `Optional` dependency on monix works, correctly - that is, implicit searches still work and `BIOConvert3To2` instance is accessible. Check out `OptionalDependencyTest` in distage-config/test, you may add a test that uses `BIOConvert3To2` instance there to check that scalac is not broken when monix-bio is not a dependency, e.g. a test like this:

```scala
def threeTo2[FR[-_, +_, +_]](implicit FR: BIO3[FR]): FR[Any, Nothing, Unit] = {
  val F: BIO[F[Any, +?, +?]] = implicitly // must use `BIOConvert3To2` instance to convert FR -> F
  F.unit
}
```

Patch:
diff --git a/build.sbt b/build.sbt
index 75d8bd48e..125d024eb 100644
--- a/build.sbt
+++ b/build.sbt
@@ -532,7 +532,8 @@ lazy val `fundamentals-bio` = project.in(file("fundamentals/fundamentals-bio"))
       "org.scalatest" %% "scalatest" % V.scalatest % Test,
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
-      "dev.zio" %% "zio" % V.zio % Optional
+      "dev.zio" %% "zio" % V.zio % Optional,
+      "io.monix" %% "monix-bio" % V.monix_bio % Optional
     )
   )
   .settings(
@@ -741,6 +742,7 @@ lazy val `distage-core-api` = project.in(file("distage/distage-core-api"))
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
       "dev.zio" %% "zio" % V.zio % Optional,
+      "io.monix" %% "monix-bio" % V.monix_bio % Optional,
       "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided
     )
   )
@@ -944,7 +946,8 @@ lazy val `distage-core` = project.in(file("distage/distage-core"))
       "org.scalatest" %% "scalatest" % V.scalatest % Test,
       "org.typelevel" %% "cats-core" % V.cats % Test,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Test,
-      "dev.zio" %% "zio" % V.zio % Test
+      "dev.zio" %% "zio" % V.zio % Test,
+      "io.monix" %% "monix-bio" % V.monix_bio % Test
     )
   )
   .settings(
@@ -1462,6 +1465,7 @@ lazy val `distage-framework` = project.in(file("distage/distage-framework"))
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
       "dev.zio" %% "zio" % V.zio % Optional,
+      "io.monix" %% "monix-bio" % V.monix_bio % Optional,
       "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided
     )
   )
@@ -1569,6 +1573,7 @@ lazy val `distage-framework-docker` = project.in(file("distage/distage-framework
       "org.typelevel" %% "cats-core" % V.cats % Test,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Test,
       "dev.zio" %% "zio" % V.zio % Test,
+      "io.monix" %% "monix-bio" % V.monix_bio % Test,
       "com.github.docker-java" % "docker-java" % V.docker_java
     )
   )
@@ -1674,7 +1679,8 @@ lazy val `distage-testkit-core` = project.in(file("distage/distage-testkit-core"
       "org.scalatest" %% "scalatest" % V.scalatest % Test,
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
-      "dev.zio" %% "zio" % V.zio % Optional
+      "dev.zio" %% "zio" % V.zio % Optional,
+      "io.monix" %% "monix-bio" % V.monix_bio % Optional
     )
   )
   .settings(
@@ -1779,6 +1785,7 @@ lazy val `distage-testkit-scalatest` = project.in(file("distage/distage-testkit-
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
       "dev.zio" %% "zio" % V.zio % Optional,
+      "io.monix" %% "monix-bio" % V.monix_bio % Optional,
       "org.scalamock" %% "scalamock" % V.scalamock % Test,
       "org.scalatest" %% "scalatest" % V.scalatest
     )
@@ -1884,7 +1891,8 @@ lazy val `logstage-core` = project.in(file("logstage/logstage-core"))
       "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided,
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
-      "dev.zio" %% "zio" % V.zio % Optional
+      "dev.zio" %% "zio" % V.zio % Optional,
+      "io.monix" %% "monix-bio" % V.monix_bio % Optional
     )
   )
   .settings(
diff --git a/distage/distage-core-api/.jvm/src/main/scala/izumi/distage/model/effect/DIEffectRunner.scala b/distage/distage-core-api/.jvm/src/main/scala/izumi/distage/model/effect/DIEffectRunner.scala
index f668537b7..8daa58a6a 100644
--- a/distage/distage-core-api/.jvm/src/main/scala/izumi/distage/model/effect/DIEffectRunner.scala
+++ b/distage/distage-core-api/.jvm/src/main/scala/izumi/distage/model/effect/DIEffectRunner.scala
@@ -1,8 +1,9 @@
 package izumi.distage.model.effect
 
-import cats.effect.IO
+import cats.effect.{Effect, IO}
 import izumi.functional.bio.BIORunner
 import izumi.fundamentals.platform.functional.Identity
+import monix.bio.Task
 
 trait DIEffectRunner[F[_]] {
   def run[A](f: => F[A]): A
@@ -11,6 +12,11 @@ trait DIEffectRunner[F[_]] {
 object DIEffectRunner {
   def apply[F[_]: DIEffectRunner]: DIEffectRunner[F] = implicitly
 
+  def catsAndMonix[F[_]: Effect]: DIEffectRunner[F] =
+    new DIEffectRunner[F] {
+      override def run[A](f: => F[A]): A = Effect[F].toIO(f).unsafeRunSync()
+    }
+
   implicit object IdentityImpl extends DIEffectRunner[Identity] {
     override def run[A](f: => A): A = f
   }
@@ -19,6 +25,11 @@ object DIEffectRunner {
     override def run[A](f: => IO[A]): A = f.unsafeRunSync()
   }
 
+  implicit object MonixBIOImpl extends DIEffectRunner[Task] {
+    import monix.execution.Scheduler.Implicits.global
+    override def run[A](f: => Task[A]): A = f.runSyncUnsafe()
+  }
+
   implicit def bio[F[_, _]: BIORunner]: DIEffectRunner[F[Throwable, ?]] = new BIOImpl[F]
 
   final class BIOImpl[F[_, _]: BIORunner] extends DIEffectRunner[F[Throwable, ?]] {
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/CatsDIEffectModule.scala b/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/CatsDIEffectModule.scala
index ac756b185..10677293b 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/CatsDIEffectModule.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/CatsDIEffectModule.scala
@@ -1,36 +1,12 @@
 package izumi.distage.effect.modules
 
-import cats.effect.{Async, Bracket, Concurrent, ConcurrentEffect, ContextShift, Effect, ExitCode, IO, IOApp, Sync, Timer}
-import cats.{Applicative, Functor, Monad, MonadError, Parallel}
+import cats.Parallel
+import cats.effect.{Concurrent, ConcurrentEffect, ContextShift, ExitCode, IO, IOApp, Timer}
 import izumi.distage.model.definition.ModuleDef
-import izumi.distage.model.effect.{DIApplicative, DIEffect, DIEffectAsync, DIEffectRunner}
-import izumi.functional.mono.SyncSafe
 
 object CatsDIEffectModule extends CatsDIEffectModule
 
 trait CatsDIEffectModule extends ModuleDef {
-  addImplicit[DIEffectRunner[IO]]
-  addImplicit[DIApplicative[IO]]
-  addImplicit[DIEffect[IO]]
-
-  make[DIEffectAsync[IO]].from {
-    (P0: Parallel[IO], T0: Timer[IO], C0: Concurrent[IO]) =>
-      implicit val P: Parallel[IO] = P0
-      implicit val T: Timer[IO] = T0
-      implicit val C: Concurrent[IO] = C0
-      DIEffectAsync[IO]
-  }
-
-  addImplicit[Functor[IO]]
-  addImplicit[Applicative[IO]]
-  addImplicit[Monad[IO]]
-  addImplicit[MonadError[IO, Throwable]]
-  addImplicit[Bracket[IO, Throwable]]
-  addImplicit[Sync[IO]]
-  addImplicit[Async[IO]]
-  addImplicit[Effect[IO]]
-  addImplicit[SyncSafe[IO]]
-
   make[Parallel[IO]].from(IO.ioParallel(_: ContextShift[IO]))
   make[ConcurrentEffect[IO]]
     .from(IO.ioConcurrentEffect(_: ContextShift[IO]))
@@ -42,9 +18,10 @@ trait CatsDIEffectModule extends ModuleDef {
   make[PublicIOApp].from {
     new PublicIOApp { override def run(args: List[String]): IO[ExitCode] = IO.pure(ExitCode(0)) }
   }
+
+  include(PolymorphicCatsDIEffectModule[IO])
 }
 
-// extract default ContextShift & Timer from IOApp
 trait PublicIOApp extends IOApp {
   override def contextShift: ContextShift[IO] = super.contextShift
   override def timer: Timer[IO] = super.timer
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/MonixDIEffectModule.scala b/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/MonixDIEffectModule.scala
new file mode 100644
index 000000000..d8ed57d07
--- /dev/null
+++ b/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/MonixDIEffectModule.scala
@@ -0,0 +1,25 @@
+package izumi.distage.effect.modules
+
+import cats.Parallel
+import cats.effect.{Concurrent, ConcurrentEffect, ContextShift, Timer}
+import izumi.distage.model.definition.ModuleDef
+import monix.bio
+import monix.bio.Task
+import monix.bio.instances.{CatsConcurrentEffectForTask, CatsParallelForTask}
+import monix.catnap.SchedulerEffect
+import monix.execution.Scheduler
+
+object MonixDIEffectModule extends MonixDIEffectModule
+
+trait MonixDIEffectModule extends ModuleDef {
+  implicit val scheduler: Scheduler = Scheduler.global
+  implicit val ce: CatsConcurrentEffectForTask = new CatsConcurrentEffectForTask()(scheduler, bio.IO.defaultOptions)
+
+  make[Parallel[Task]].from(new CatsParallelForTask[Throwable])
+  make[ConcurrentEffect[Task]]
+    .from(new CatsConcurrentEffectForTask()(scheduler, bio.IO.defaultOptions)).aliased[Concurrent[Task]]
+
+  make[ContextShift[Task]].from(SchedulerEffect.contextShift(scheduler))
+  make[Timer[Task]].from(SchedulerEffect.timer(scheduler))
+  include(PolymorphicCatsDIEffectModule[Task])
+}
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/PolymorphicCatsDIEffectModule.scala b/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/PolymorphicCatsDIEffectModule.scala
new file mode 100644
index 000000000..5d3069a8b
--- /dev/null
+++ b/distage/distage-framework/src/main/scala/izumi/distage/effect/modules/PolymorphicCatsDIEffectModule.scala
@@ -0,0 +1,48 @@
+package izumi.distage.effect.modules
+
+import cats.effect.{Async, Bracket, Concurrent, ConcurrentEffect, Effect, Sync, Timer}
+import cats.{Applicative, Functor, Monad, MonadError, Parallel}
+import distage.{ModuleDef, TagK}
+import izumi.distage.model.effect.{DIApplicative, DIEffect, DIEffectAsync, DIEffectRunner}
+import izumi.functional.mono.SyncSafe
+
+/**
+  * Module definition that binds effect TC instances for an arbitrary F[_].
+  */
+class PolymorphicCatsDIEffectModule[F[_]: TagK] extends ModuleDef {
+  make[DIEffectRunner[F]].from {
+    implicit F: Effect[F] => DIEffectRunner.catsAndMonix
+  }
+
+  make[DIApplicative[F]].from {
+    implicit F: Applicative[F] => DIApplicative.fromCats
+  }
+
+  make[DIEffect[F]].from {
+    implicit F: Sync[F] => DIEffect.fromCatsEffect
+  }
+
+  make[DIEffectAsync[F]].from {
+    (P0: Parallel[F], T0: Timer[F], C0: Concurrent[F]) =>
+      implicit val P: Parallel[F] = P0
+      implicit val T: Timer[F] = T0
+      implicit val C: Concurrent[F] = C0
+      DIEffectAsync[F]
+  }
+
+  make[Functor[F]].using[ConcurrentEffect[F]]
+  make[Applicative[F]].using[ConcurrentEffect[F]]
+  make[Monad[F]].using[ConcurrentEffect[F]]
+  make[MonadError[F, Throwable]].using[ConcurrentEffect[F]]
+  make[Bracket[F, Throwable]].using[ConcurrentEffect[F]]
+  make[Sync[F]].using[ConcurrentEffect[F]]
+  make[Async[F]].using[ConcurrentEffect[F]]
+  make[SyncSafe[F]].from {
+    implicit F: Sync[F] => SyncSafe.fromSync
+  }
+  make[Effect[F]].using[ConcurrentEffect[F]]
+}
+
+object PolymorphicCatsDIEffectModule {
+  final def apply[F[_]: TagK]: PolymorphicCatsDIEffectModule[F] = new PolymorphicCatsDIEffectModule[F]
+}
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOAsync3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOAsync3.scala
index f7386b7fb..4db9defea 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOAsync3.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOAsync3.scala
@@ -19,6 +19,8 @@ trait BIOAsync3[F[-_, +_, +_]] extends BIO3[F] with BIOParallel3[F] {
 
   /** Race two actions, the winner is the first action to TERMINATE, whether by success or failure */
   def race[R, E, A](r1: F[R, E, A], r2: F[R, E, A]): F[R, E, A]
+
+  /** Race two actions, the winner is the first action to TERMINATE, whether by success or failure */
   def racePair[R, E, A, B](fa: F[R, E, A], fb: F[R, E, B]): F[R, E, Either[(A, BIOFiber3[F, E, B]), (BIOFiber3[F, E, A], B)]]
 
   def uninterruptible[R, E, A](r: F[R, E, A]): F[R, E, A]
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOExit.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOExit.scala
index 6ac9aa1bf..a420c2ba2 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOExit.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOExit.scala
@@ -1,6 +1,7 @@
 package izumi.functional.bio
 
 import zio.{Cause, Exit, FiberFailure}
+import monix.bio
 
 sealed trait BIOExit[+E, +A] {
   def map[B](f: A => B): BIOExit[E, B]
@@ -119,4 +120,21 @@ object BIOExit {
     override final def leftMap[R, E, A, E2](r: BIOExit[E, A])(f: E => E2): BIOExit[E2, A] = r.leftMap(f)
   }
 
+  object MonixExit {
+    @inline def toIzBIO[E, A](exit: Either[Option[bio.Cause[E]], A]): BIOExit[E, A] = {
+      exit match {
+        case Left(None) => Termination(new Throwable("The task was cancelled."), Trace.empty)
+        case Left(Some(error)) => fromMonixCause(error)
+        case Right(value) => Success(value)
+      }
+    }
+
+    @inline def fromMonixCause[E](cause: bio.Cause[E]): BIOExit.Failure[E] = {
+      cause match {
+        case bio.Cause.Error(value) => BIOExit.Error(value, Trace.empty)
+        case bio.Cause.Termination(value) => BIOExit.Termination(value, Trace.empty)
+      }
+    }
+  }
+
 }
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFiber.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFiber.scala
index 11c64d6ba..a73d1b76c 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFiber.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFiber.scala
@@ -1,12 +1,14 @@
 package izumi.functional.bio
 
+import izumi.functional.bio.BIOExit.MonixExit._
 import izumi.functional.bio.BIOExit.ZIOExit
+import monix.bio
 import zio.{Fiber, IO, ZIO}
 
 trait BIOFiber3[F[-_, +_, +_], +E, +A] {
   def join: F[Any, E, A]
   def observe: F[Any, Nothing, BIOExit[E, A]]
-  def interrupt: F[Any, Nothing, BIOExit[E, A]]
+  def interrupt: F[Any, Nothing, Unit]
 }
 
 object BIOFiber3 {
@@ -14,17 +16,24 @@ object BIOFiber3 {
     new BIOFiber3[ZIO, E, A] {
       override val join: IO[E, A] = f.join
       override val observe: IO[Nothing, BIOExit[E, A]] = f.await.map(ZIOExit.toBIOExit[E, A])
-      override def interrupt: IO[Nothing, BIOExit[E, A]] = f.interrupt.map(ZIOExit.toBIOExit[E, A])
+      override val interrupt: IO[Nothing, Unit] = f.interrupt.void
     }
 
   implicit final class ToCats[FR[-_, +_, +_], A](private val bioFiber: BIOFiber3[FR, Throwable, A]) extends AnyVal {
     def toCats(implicit F: BIOFunctor3[FR]): cats.effect.Fiber[FR[Any, Throwable, ?], A] = new cats.effect.Fiber[FR[Any, Throwable, ?], A] {
       override def cancel: FR[Any, Throwable, Unit] = F.void(bioFiber.interrupt)
+
       override def join: FR[Any, Throwable, A] = bioFiber.join
     }
   }
+
 }
 
 object BIOFiber {
   @inline def fromZIO[E, A](f: Fiber[E, A]): BIOFiber3[ZIO, E, A] = BIOFiber3.fromZIO(f)
+  @inline def fromMonix[E, A](f: bio.Fiber[E, A]): BIOFiber[bio.IO, E, A] = new BIOFiber[bio.IO, E, A] {
+    override val join: bio.IO[E, A] = f.join
+    override val observe: bio.IO[Nothing, BIOExit[E, A]] = f.join.redeemCause(c => fromMonixCause(c), a => BIOExit.Success(a))
+    override val interrupt: bio.IO[Nothing, Unit] = f.cancel.void
+  }
 }
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFork3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFork3.scala
index 4d6a25f0f..6c27b4889 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFork3.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOFork3.scala
@@ -7,6 +7,7 @@ trait BIOFork3[F[-_, +_, +_]] extends BIOForkInstances {
 }
 
 private[bio] sealed trait BIOForkInstances
+
 object BIOForkInstances extends LowPriorityBIOForkInstances {
   implicit val BIOForkZio: BIOFork3[ZIO] = new BIOFork3[ZIO] {
     override def fork[R, E, A](f: ZIO[R, E, A]): ZIO[R, Nothing, BIOFiber3[ZIO, E, A]] =
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIORoot.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIORoot.scala
index 31847c8ef..adb6324c6 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIORoot.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIORoot.scala
@@ -1,10 +1,11 @@
 package izumi.functional.bio
 
 import cats.data.Kleisli
+import izumi.functional.bio.BIORootInstancesLowPriority6._BIOAsyncMonixBIO
 import izumi.functional.bio.DivergenceHelper.{Divergent, Nondivergent}
 import izumi.functional.bio.PredefinedHelper.{NotPredefined, Predefined}
 import izumi.functional.bio.SpecificityHelper._
-import izumi.functional.bio.impl.BIOAsyncZio
+import izumi.functional.bio.impl.{BIOAsyncMonix, BIOAsyncZio}
 import izumi.fundamentals.platform.language.unused
 import zio.ZIO
 
@@ -13,9 +14,8 @@ import scala.language.implicitConversions
 trait BIORoot extends DivergenceHelper with PredefinedHelper
 
 object BIORoot extends BIORootInstancesLowPriority1 {
-  @inline implicit final def BIOConvertFromBIOMonadAsk[FR[-_, +_, +_]](implicit BIOMonadAsk: NotPredefined.Of[BIOMonadAsk[FR]]): BIOMonad3[FR] with S1 = S1(
-    BIOMonadAsk.InnerF
-  )
+  @inline implicit final def BIOConvertFromBIOMonadAsk[FR[-_, +_, +_]](implicit BIOMonadAsk: NotPredefined.Of[BIOMonadAsk[FR]]): BIOMonad3[FR] with S1 =
+    S1(BIOMonadAsk.InnerF)
 
   @inline implicit final def AttachBIOLocal[FR[-_, +_, +_], R](@unused self: BIOFunctor3[FR])(implicit BIOLocal: BIOLocal[FR]): BIOLocal.type = BIOLocal
   @inline implicit final def AttachBIOPrimitives3[FR[-_, +_, +_]](@unused self: BIOFunctor3[FR])(implicit BIOPrimitives: BIOPrimitives3[FR]): BIOPrimitives.type =
@@ -30,9 +30,8 @@ object BIORoot extends BIORootInstancesLowPriority1 {
 }
 
 sealed trait BIORootInstancesLowPriority1 extends BIORootInstancesLowPriority2 {
-  @inline implicit final def BIOConvertFromBIOParallel[FR[-_, +_, +_]](implicit BIOParallel: NotPredefined.Of[BIOParallel3[FR]]): BIOMonad3[FR] with S2 = S2(
-    BIOParallel.InnerF
-  )
+  @inline implicit final def BIOConvertFromBIOParallel[FR[-_, +_, +_]](implicit BIOParallel: NotPredefined.Of[BIOParallel3[FR]]): BIOMonad3[FR] with S2 =
+    S2(BIOParallel.InnerF)
 
   @inline implicit final def AttachBIOArrowChoice[FR[-_, +_, +_]](@unused self: BIOFunctor3[FR])(implicit BIOArrowChoice: BIOArrowChoice[FR]): BIOArrowChoice.type =
     BIOArrowChoice
@@ -52,23 +51,39 @@ sealed trait BIORootInstancesLowPriority2 extends BIORootInstancesLowPriority3 {
 }
 
 sealed trait BIORootInstancesLowPriority3 extends BIORootInstancesLowPriority4 {
-  @inline implicit final def BIOConvertFromBIOProfunctor[FR[-_, +_, +_]](implicit BIOProfunctor: NotPredefined.Of[BIOProfunctor[FR]]): BIOFunctor3[FR] with S4 = S4(
-    BIOProfunctor.InnerF
-  )
+  @inline implicit final def BIOConvertFromBIOProfunctor[FR[-_, +_, +_]](implicit BIOProfunctor: NotPredefined.Of[BIOProfunctor[FR]]): BIOFunctor3[FR] with S4 =
+    S4(BIOProfunctor.InnerF)
 }
 
 sealed trait BIORootInstancesLowPriority4 extends BIORootInstancesLowPriority5 {
-  @inline implicit final def BIOConvertFromBIOBifunctor[FR[-_, +_, +_]](implicit BIOBifunctor: NotPredefined.Of[BIOBifunctor3[FR]]): BIOFunctor3[FR] with S5 = S5(
-    BIOBifunctor.InnerF
-  )
+  @inline implicit final def BIOConvertFromBIOBifunctor[FR[-_, +_, +_]](implicit BIOBifunctor: NotPredefined.Of[BIOBifunctor3[FR]]): BIOFunctor3[FR] with S5 =
+    S5(BIOBifunctor.InnerF)
 }
 
 sealed trait BIORootInstancesLowPriority5 extends BIORootInstancesLowPriority6 {
-  @inline implicit final def BIOZIO: Predefined.Of[BIOAsync3[ZIO]] = Predefined(BIOAsyncZio)
   @inline implicit final def BIOLocalZIO: Predefined.Of[BIOLocal[ZIO]] = Predefined(BIOAsyncZio)
+  @inline implicit final def BIOZIO: Predefined.Of[BIOAsync3[ZIO]] = Predefined(BIOAsyncZio)
+}
+
+sealed trait BIORootInstancesLowPriority6 extends BIORootInstancesLowPriority7 {
+  /**
+    * This instance uses 'no more orphans' trick to provide an Optional instance
+    * only IFF you have monix-bio as a dependency without REQUIRING a monix-bio dependency.
+    *
+    * Optional instance via https://blog.7mind.io/no-more-orphans.html
+    */
+  // for some reason ZIO instances do not require no-more-orphans machinery and do not create errors when zio is not on classpath...
+  @inline implicit final def BIOMonix[A](implicit @unused M: _BIOAsyncMonixBIO[A]): Predefined.Of[A] = BIOAsyncMonix.asInstanceOf[Predefined.Of[A]]
+}
+
+object BIORootInstancesLowPriority6 {
+  final abstract class _BIOAsyncMonixBIO[A]
+  object _BIOAsyncMonixBIO {
+    @inline implicit final def get: _BIOAsyncMonixBIO[BIOAsync[monix.bio.IO]] = null
+  }
 }
 
-sealed trait BIORootInstancesLowPriority6 {
+sealed trait BIORootInstancesLowPriority7 {
   @inline implicit final def BIOConvert3To2[C[f[-_, +_, +_]] <: DivergenceHelper with BIOFunctor3[f], FR[-_, +_, +_], R0](
     implicit BIOFunctor3: C[FR] { type Divergence = Nondivergent }
   ): C[Lambda[(`-R`, `+E`, `+A`) => FR[R0, E, A]]] with DivergenceHelper { type Divergence = Divergent } =
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOTemporal3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOTemporal3.scala
index b0cddeada..f18a3e167 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOTemporal3.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOTemporal3.scala
@@ -1,6 +1,7 @@
 package izumi.functional.bio
 
-import izumi.functional.bio.impl.BIOTemporalZio
+import izumi.functional.bio.impl.{BIOTemporalMonix, BIOTemporalZio}
+import monix.bio
 import zio.ZIO
 
 import scala.concurrent.duration.{Duration, FiniteDuration}
@@ -32,3 +33,7 @@ private[bio] sealed trait BIOTemporalInstances
 object BIOTemporalInstances {
   @inline implicit final def BIOTemporal3Zio(implicit clockService: zio.clock.Clock): BIOTemporal3[ZIO] = new BIOTemporalZio(clockService)
 }
+
+sealed trait BIOTemporalInstancesLowPriority1 {
+  @inline implicit final def BIOTemporal2Monix(implicit clock: Clock2[bio.IO]): BIOTemporal[bio.IO] = new BIOTemporalMonix(clock)
+}
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala
new file mode 100644
index 000000000..04dd781b1
--- /dev/null
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala
@@ -0,0 +1,131 @@
+package izumi.functional.bio.impl
+
+import java.util.concurrent.CompletionStage
+
+import izumi.functional.bio.BIOExit.MonixExit._
+import izumi.functional.bio.{BIOAsync, BIOExit, BIOFiber}
+import monix.bio.{Cause, IO, Task}
+
+import scala.concurrent.{ExecutionContext, Future}
+import scala.util.Try
+
+object BIOAsyncMonix extends BIOAsyncMonix
+
+class BIOAsyncMonix extends BIOAsync[IO] {
+  override final def pure[A](a: A): IO[Nothing, A] = IO.pure(a)
+  override final def sync[A](effect: => A): IO[Nothing, A] = IO.evalTotal(effect)
+  override final def syncThrowable[A](effect: => A): IO[Throwable, A] = IO.eval(effect)
+  override final def suspend[R, A](effect: => IO[Throwable, A]): IO[Throwable, A] = IO.suspendTotal(effect)
+
+  override final def fail[E](v: => E): IO[E, Nothing] = IO.raiseError(v)
+  override final def terminate(v: => Throwable): IO[Nothing, Nothing] = IO.terminate(v)
+
+  override final def fromEither[E, V](effect: => Either[E, V]): IO[E, V] = IO.fromEither(effect)
+  override final def fromOption[E, A](errorOnNone: => E)(effect: => Option[A]): IO[E, A] = IO.fromEither(effect.toRight(errorOnNone))
+  override final def fromTry[A](effect: => Try[A]): IO[Throwable, A] = IO.fromTry(effect)
+
+  override final def void[R, E, A](r: IO[E, A]): IO[E, Unit] = r.void
+  override final def map[R, E, A, B](r: IO[E, A])(f: A => B): IO[E, B] = r.map(f)
+  override final def as[R, E, A, B](r: IO[E, A])(v: => B): IO[E, B] = r.map(_ => v)
+
+  override final def tapError[R, E, A, E1 >: E](r: IO[E, A])(f: E => IO[E1, Unit]): IO[E1, A] = r.tapError(f)
+  override final def leftMap[R, E, A, E2](r: IO[E, A])(f: E => E2): IO[E2, A] = r.mapError(f)
+  override final def leftFlatMap[R, E, A, E2](r: IO[E, A])(f: E => IO[Nothing, E2]): IO[E2, A] = r.flipWith(_ flatMap f)
+  override final def flip[R, E, A](r: IO[E, A]): IO[A, E] = r.flip
+  override final def bimap[R, E, A, E2, B](r: IO[E, A])(f: E => E2, g: A => B): IO[E2, B] = r.bimap(f, g)
+
+  override final def flatMap[R, E, A, B](r: IO[E, A])(f: A => IO[E, B]): IO[E, B] = r.flatMap(f)
+  override final def tap[R, E, A](r: IO[E, A])(f: A => IO[E, Unit]): IO[E, A] = r.flatMap(a => f(a).map(_ => a))
+
+  override final def tapBoth[R, E, A, E1 >: E](r: IO[E, A])(err: E => IO[E1, Unit], succ: A => IO[E1, Unit]): IO[E1, A] =
+    r.tapError(err).flatMap(a => succ(a).map(_ => a))
+
+  override final def flatten[R, E, A](r: IO[E, IO[E, A]]): IO[E, A] = r.flatten
+  override final def *>[R, E, A, B](r: IO[E, A], next: => IO[E, B]): IO[E, B] = r.flatMap(_ => next)
+  override final def <*[R, E, A, B](r: IO[E, A], next: => IO[E, B]): IO[E, A] = r.flatMap(a => next.map(_ => a))
+  override final def map2[R, E, A, B, C](r1: IO[E, A], r2: => IO[E, B])(f: (A, B) => C): IO[E, C] = r1.flatMap(a => r2.map(f(a, _)))
+
+  override final def redeem[R, E, A, E2, B](r: IO[E, A])(err: E => IO[E2, B], succ: A => IO[E2, B]): IO[E2, B] = r.redeemWith(err, succ)
+  override final def catchAll[R, E, A, E2](r: IO[E, A])(f: E => IO[E2, A]): IO[E2, A] = r.onErrorHandleWith(f)
+  override final def catchSome[R, E, A, E1 >: E](r: IO[E, A])(f: PartialFunction[E, IO[E1, A]]): IO[E1, A] = r.onErrorRecoverWith(f)
+  override final def withFilter[R, E, A](r: IO[E, A])(predicate: A => Boolean)(implicit ev: NoSuchElementException <:< E): IO[E, A] =
+    r.flatMap {
+      a =>
+        if (predicate(a)) IO.pure(a)
+        else IO.raiseError(ev(new NoSuchElementException("The value doesn't satisfy the predicate")))
+    }
+
+  override final def guarantee[R, E, A](f: IO[E, A], cleanup: IO[Nothing, Unit]): IO[E, A] = f.guarantee(cleanup)
+  override final def attempt[R, E, A](r: IO[E, A]): IO[Nothing, Either[E, A]] = r.attempt
+  override final def redeemPure[R, E, A, B](r: IO[E, A])(err: E => B, succ: A => B): IO[Nothing, B] = r.redeem(err, succ)
+
+  override final def bracket[R, E, A, B](acquire: IO[E, A])(release: A => IO[Nothing, Unit])(use: A => IO[E, B]): IO[E, B] = acquire.bracket(use)(release)
+  override final def bracketCase[R, E, A, B](acquire: IO[E, A])(release: (A, BIOExit[E, B]) => IO[Nothing, Unit])(use: A => IO[E, B]): IO[E, B] = {
+    acquire.bracketE(use)((a, exit) => release(a, toIzBIO(exit)))
+  }
+  override final def guaranteeCase[R, E, A](f: IO[E, A], cleanup: BIOExit[E, A] => IO[Nothing, Unit]): IO[E, A] = f.bracketE(IO.pure)((_, exit) => cleanup(toIzBIO(exit)))
+
+  override final def traverse[R, E, A, B](l: Iterable[A])(f: A => IO[E, B]): IO[E, List[B]] = IO.traverse(l)(f).map(_.toList)
+  override final def sequence[R, E, A, B](l: Iterable[IO[E, A]]): IO[E, List[A]] = IO.sequence(l)
+  override final def traverse_[R, E, A](l: Iterable[A])(f: A => IO[E, Unit]): IO[E, Unit] = IO.traverse(l)(f).void
+  override final def sequence_[R, E](l: Iterable[IO[E, Unit]]): IO[E, Unit] = IO.sequence(l).void
+
+  override final def sandbox[R, E, A](r: IO[E, A]): IO[BIOExit.Failure[E], A] = r.redeemCauseWith(cause => IO.raiseError(fromMonixCause(cause)), a => IO.pure(a))
+  override final def yieldNow: IO[Nothing, Unit] = IO.unit
+  override final def never: IO[Nothing, Nothing] = IO.never
+
+  override final def race[R, E, A](r1: IO[E, A], r2: IO[E, A]): IO[E, A] = IO.raceMany(List(r1, r2))
+
+  override final def racePair[R, E, A, B](fa: IO[E, A], fb: IO[E, B]): IO[E, Either[(A, BIOFiber[IO, E, B]), (BIOFiber[IO, E, A], B)]] = {
+    IO.racePair(fa, fb).flatMap {
+      case Left((a, fiberB)) => fiberB.cancel.void.map(_ => Left((a, BIOFiber.fromMonix(fiberB))))
+      case Right((fiberA, b)) => fiberA.cancel.void.map(_ => Right((BIOFiber.fromMonix(fiberA), b)))
+    }
+  }
+
+  override final def async[E, A](register: (Either[E, A] => Unit) => Unit): IO[E, A] = {
+    IO.async(cb => register(cb apply Try(_)))
+  }
+  override final def asyncF[R, E, A](register: (Either[E, A] => Unit) => IO[E, Unit]): IO[E, A] = {
+    IO.asyncF(cb => register(cb apply Try(_)))
+  }
+  override final def asyncCancelable[E, A](register: (Either[E, A] => Unit) => Canceler): IO[E, A] = {
+    IO.cancelable[E, A](cb => register(cb apply Try(_)))
+  }
+  override final def fromFuture[A](mkFuture: ExecutionContext => Future[A]): IO[Throwable, A] = {
+    IO.deferFutureAction(mkFuture)
+  }
+  //this approach I shamelessly ported from javaz
+  override final def fromFutureJava[A](javaFuture: => CompletionStage[A]): IO[Throwable, A] = {
+    lazy val cs: CompletionStage[A] = javaFuture
+    IO.deferTotal {
+      val cf = cs.toCompletableFuture
+      if (cf.isDone) {
+        Try(cf.get()) match {
+          case util.Failure(exception) => Task.terminate(exception)
+          case util.Success(value) => Task.pure(value)
+        }
+      } else {
+        IO.async {
+          cb =>
+            cs.handle[Unit] {
+              (v: A, t: Throwable) =>
+                val io = Option(t).fold[Either[Cause[Throwable], A]](Right(v))(_ => Left(Cause.Error(t)))
+                cb(io)
+            }; ()
+        }
+      }
+    }
+  }
+  override final def uninterruptible[R, E, A](r: IO[E, A]): IO[E, A] = r.uncancelable
+
+  override final def parTraverseN[R, E, A, B](maxConcurrent: Int)(l: Iterable[A])(f: A => IO[E, B]): IO[E, List[B]] = IO.parTraverseN(maxConcurrent)(l)(f)
+  override final def parTraverseN_[R, E, A, B](maxConcurrent: Int)(l: Iterable[A])(f: A => IO[E, B]): IO[E, Unit] = IO.parTraverseN(maxConcurrent)(l)(f).void
+  override final def parTraverse[R, E, A, B](l: Iterable[A])(f: A => IO[E, B]): IO[E, List[B]] = IO.parTraverse(l)(f)
+  override final def parTraverse_[R, E, A, B](l: Iterable[A])(f: A => IO[E, B]): IO[E, Unit] = IO.parTraverse(l)(f).void
+
+  override final def zipWithPar[R, E, A, B, C](fa: IO[E, A], fb: IO[E, B])(f: (A, B) => C): IO[E, C] = IO.mapBoth(fa, fb)(f)
+  override final def zipPar[R, E, A, B](fa: IO[E, A], fb: IO[E, B]): IO[E, (A, B)] = IO.parZip2(fa, fb)
+  override final def zipParLeft[R, E, A, B](fa: IO[E, A], fb: IO[E, B]): IO[E, A] = IO.parZip2(fa, fb).map { case (a, _) => a }
+  override final def zipParRight[R, E, A, B](fa: IO[E, A], fb: IO[E, B]): IO[E, B] = IO.parZip2(fa, fb).map { case (_, b) => b }
+}
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala
index 5c114ab3f..799bf56c2 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala
@@ -84,6 +84,7 @@ class BIOAsyncZio extends BIOAsync3[ZIO] with BIOLocal[ZIO] {
   @inline override final def race[R, E, A](r1: ZIO[R, E, A], r2: ZIO[R, E, A]): ZIO[R, E, A] = {
     r1.interruptible.raceFirst(r2.interruptible)
   }
+
   @inline override final def racePair[R, E, A, B](
     r1: ZIO[R, E, A],
     r2: ZIO[R, E, B],
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOTemporalMonix.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOTemporalMonix.scala
new file mode 100644
index 000000000..007e2f56b
--- /dev/null
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOTemporalMonix.scala
@@ -0,0 +1,27 @@
+package izumi.functional.bio.impl
+
+import izumi.functional.bio.{BIOTemporal, Clock2}
+import monix.bio.IO
+
+import scala.concurrent.duration._
+
+class BIOTemporalMonix(clock: Clock2[IO]) extends BIOAsyncMonix with BIOTemporal[IO] {
+  @inline override def sleep(duration: Duration): IO[Nothing, Unit] = IO.sleep(FiniteDuration(duration.length, duration.unit))
+  @inline override def retryOrElse[R, E, A, E2](r: IO[E, A])(duration: FiniteDuration, orElse: => IO[E2, A]): IO[E2, A] = {
+    val boundary = clock.now().map(_.toEpochSecond + duration.toSeconds)
+    def loop: IO[E2, A] = {
+      r.redeemCauseWith(
+        _ =>
+          for {
+            now <- clock.now()
+            time <- boundary
+            res <- if (now.toEpochSecond < time) loop else orElse
+          } yield res,
+        res => IO.pure(res),
+      )
+    }
+    loop
+  }
+
+  @inline override def timeout[R, E, A](r: IO[E, A])(duration: Duration): IO[E, Option[A]] = r.timeout(FiniteDuration(duration.length, duration.unit))
+}
diff --git a/project/Deps.sc b/project/Deps.sc
index 5f8564ea8..c6bce89c1 100644
--- a/project/Deps.sc
+++ b/project/Deps.sc
@@ -15,6 +15,7 @@ object Izumi {
     val cats_effect = Version.VExpr("V.cats_effect")
     val zio = Version.VExpr("V.zio")
     val zio_interop_cats = Version.VExpr("V.zio_interop_cats")
+    val monix_bio = Version.VExpr("V.monix_bio")
     val circe = Version.VExpr("V.circe")
     val circe_generic_extras = Version.VExpr("V.circe_generic_extras")
     val circe_derivation = Version.VExpr("V.circe_derivation")
@@ -92,6 +93,7 @@ object Izumi {
       zio_core,
       zio_interop_cats,
     )
+    final val monix_bio = Library("io.monix", "monix-bio", V.monix_bio, LibraryType.Auto)
 
     final val typesafe_config = Library("com.typesafe", "config", V.typesafe_config, LibraryType.Invariant) in Scope.Compile.all
     final val jawn = Library("org.typelevel", "jawn-parser", V.jawn, LibraryType.AutoJvm)
@@ -394,8 +396,8 @@ object Izumi {
     defaultPlatforms = Targets.cross,
   )
 
-  final val allMonadsOptional = (cats_all ++ Seq(zio_core)).map(_ in Scope.Optional.all)
-  final val allMonadsTest = (cats_all ++ Seq(zio_core)).map(_ in Scope.Test.all)
+  final val allMonadsOptional = (cats_all ++ Seq(zio_core, monix_bio)).map(_ in Scope.Optional.all)
+  final val allMonadsTest = (cats_all ++ Seq(zio_core, monix_bio)).map(_ in Scope.Test.all)
 
   final lazy val distage = Aggregate(
     name = Projects.distage.id,
diff --git a/project/Versions.scala b/project/Versions.scala
index 56d3346e7..432451fec 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -16,6 +16,8 @@ object V {
   val zio = "1.0.1"
   val zio_interop_cats = "2.1.4.0"
 
+  val monix_bio = "1.0.0"
+
   val circe = "0.13.0"
   val circe_generic_extras = "0.13.0"
   val circe_derivation = "0.13.0-M4"


Test patch:
diff --git a/distage/distage-extension-config/src/test/scala/izumi/distage/impl/OptionalDependencyTest.scala b/distage/distage-extension-config/src/test/scala/izumi/distage/impl/OptionalDependencyTest.scala
index 4a1c63254..153949e7e 100644
--- a/distage/distage-extension-config/src/test/scala/izumi/distage/impl/OptionalDependencyTest.scala
+++ b/distage/distage-extension-config/src/test/scala/izumi/distage/impl/OptionalDependencyTest.scala
@@ -5,14 +5,50 @@ import java.io.ByteArrayInputStream
 import distage.DIResource
 import izumi.distage.model.definition.ModuleDef
 import izumi.distage.model.effect.{DIEffect, LowPriorityDIEffectInstances}
-import izumi.functional.bio.{BIO, BIOAsync, BIOTemporal}
-import izumi.fundamentals.platform.functional.Identity
+import izumi.functional.bio.{BIO, BIO3, BIOAsync, BIOMonad, BIOMonadAsk, BIOPrimitives, BIORef3, BIOTemporal, F}
+import izumi.fundamentals.platform.functional.{Identity, Identity2, Identity3}
 import org.scalatest.GivenWhenThen
 import org.scalatest.wordspec.AnyWordSpec
 
 class OptionalDependencyTest extends AnyWordSpec with GivenWhenThen {
 
-  "Using DIResource & DIEffect objects succeeds event if there's no cats or zio on the classpath" in {
+  "test 2" in {
+    // update ref from the environment and return result
+    def adderEnv[F[-_, +_, +_]: BIOMonadAsk](i: Int): F[BIORef3[F, Int], Nothing, Int] = {
+      F.access {
+        ref =>
+          for {
+            _ <- ref.update(_ + i)
+            res <- ref.get
+          } yield res
+      }
+    }
+
+    locally {
+      implicit val ask: BIOMonadAsk[Identity3] = null
+      intercept[NullPointerException](adderEnv[Identity3](0))
+    }
+  }
+
+  "test 1" in {
+    def adder[F[+_, +_]: BIOMonad: BIOPrimitives](i: Int): F[Nothing, Int] = {
+      F.mkRef(0)
+        .flatMap(ref => ref.update(_ + i) *> ref.get)
+    }
+
+    locally {
+      implicit val bioMonad: BIOMonad[Identity2] = null
+      implicit val primitives: BIOPrimitives[Identity2] = null
+      intercept[NullPointerException](adder[Identity2](0))
+    }
+  }
+
+  "Using DIResource & DIEffect objects succeeds event if there's no cats/zio/monix on the classpath" in {
+    When("There's no cats/zio/monix on classpath")
+    assertCompiles("import scala._")
+    assertDoesNotCompile("import cats._")
+    assertDoesNotCompile("import zio._")
+    assertDoesNotCompile("import monix._")
 
     Then("DIEffect methods can be called")
     def x[F[_]: DIEffect] = DIEffect[F].pure(1)
@@ -20,6 +56,16 @@ class OptionalDependencyTest extends AnyWordSpec with GivenWhenThen {
     And("DIEffect in DIEffect object resolve")
     assert(x[Identity] == 1)
 
+    type EitherFn[-R, +E, +A] = R => Either[E, A]
+    implicit val bioEitherFn: BIO3[EitherFn] = null
+    try threeTo2[EitherFn]
+    catch { case _: NullPointerException => }
+
+    def threeTo2[FR[-_, +_, +_]](implicit FR: BIO3[FR]): FR[Any, Nothing, Unit] = {
+      val F: BIO[FR[Any, +?, +?]] = implicitly // must use `BIOConvert3To2` instance to convert FR -> F
+      F.unit
+    }
+
     try DIEffect.fromBIO(null)
     catch { case _: NullPointerException => }
     try BIO[Either, Unit](())(null)
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
index f063c8911..92596e831 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
@@ -4,7 +4,7 @@ import distage.config.ConfigModuleDef
 import izumi.distage.docker.Docker.AvailablePort
 import izumi.distage.docker.bundled._
 import izumi.distage.docker.modules.DockerSupportModule
-import izumi.distage.effect.modules.{CatsDIEffectModule, ZIODIEffectModule}
+import izumi.distage.effect.modules.{CatsDIEffectModule, MonixDIEffectModule, ZIODIEffectModule}
 import izumi.distage.framework.model.IntegrationCheck
 import izumi.distage.model.definition.Id
 import izumi.distage.model.definition.StandardAxis.Env
@@ -30,7 +30,7 @@ class PgSvcExample(
   }
 }
 
-object MonadPlugin extends PluginDef with CatsDIEffectModule with ZIODIEffectModule
+object MonadPlugin extends PluginDef with CatsDIEffectModule with MonixDIEffectModule with ZIODIEffectModule
 
 object DockerPlugin extends PluginDef {
   include(DockerSupportModule[Task])
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
index 1450e75a0..9272f560e 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
@@ -3,7 +3,7 @@ package izumi.distage.roles.test.fixtures
 import cats.effect.IO
 import distage.Id
 import izumi.distage.config.ConfigModuleDef
-import izumi.distage.effect.modules.CatsDIEffectModule
+import izumi.distage.effect.modules.{CatsDIEffectModule, MonixDIEffectModule}
 import izumi.distage.model.definition.StandardAxis._
 import izumi.distage.plugins.PluginDef
 import izumi.distage.roles.bundled.{ConfigWriter, Help}
@@ -15,7 +15,7 @@ import izumi.distage.roles.test.fixtures.roles.TestRole00.{IntegrationOnlyCfg, I
 import izumi.fundamentals.platform.resources.ArtifactVersion
 import izumi.reflect.TagK
 
-class TestPluginBase[F[_]: TagK] extends PluginDef with CatsDIEffectModule with ConfigModuleDef with RoleModuleDef {
+class TestPluginBase[F[_]: TagK] extends PluginDef with CatsDIEffectModule with MonixDIEffectModule with ConfigModuleDef with RoleModuleDef {
   tag(Env.Prod)
 
   private def version = Option(System.getProperty(TestPlugin.versionProperty)) match {
diff --git a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala
index c4e043f1d..269bb4ed3 100644
--- a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala
+++ b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala
@@ -4,7 +4,7 @@ import java.util.concurrent.atomic.AtomicInteger
 
 import cats.effect.{IO => CIO}
 import distage.TagK
-import izumi.distage.effect.modules.{CatsDIEffectModule, ZIODIEffectModule}
+import izumi.distage.effect.modules.{CatsDIEffectModule, MonixDIEffectModule, ZIODIEffectModule}
 import izumi.distage.framework.model.IntegrationCheck
 import izumi.distage.model.definition.DIResource
 import izumi.distage.model.definition.StandardAxis.Env
@@ -16,7 +16,7 @@ import zio.Task
 
 import scala.collection.mutable
 
-object MonadPlugin extends PluginDef with CatsDIEffectModule with ZIODIEffectModule
+object MonadPlugin extends PluginDef with CatsDIEffectModule with MonixDIEffectModule with ZIODIEffectModule
 
 object MockAppCatsIOPlugin extends MockAppPlugin[CIO]
 object MockAppZioPlugin extends MockAppPlugin[Task]
diff --git a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/parallel/DistageParallelLevelTest.scala b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/parallel/DistageParallelLevelTest.scala
index c5f9547e8..84e54dff0 100644
--- a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/parallel/DistageParallelLevelTest.scala
+++ b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/parallel/DistageParallelLevelTest.scala
@@ -11,6 +11,7 @@ import izumi.distage.testkit.distagesuite.memoized.MemoizationEnv.MemoizedInstan
 import izumi.distage.testkit.scalatest.DistageSpecScalatest
 import izumi.fundamentals.platform.functional.Identity
 import izumi.logstage.api.Log
+import monix.bio
 import org.scalatest.Assertion
 import zio.Task
 
@@ -18,6 +19,7 @@ object DistageParallelLevelTest {
   val idCounter = new AtomicInteger(0)
   val cioCounter = new AtomicInteger(0)
   val zioCounter = new AtomicInteger(0)
+  val monixCounter = new AtomicInteger(0)
 }
 
 abstract class DistageParallelLevelTest[F[_]: TagK](suitesCounter: AtomicInteger)(implicit F: DIEffect[F]) extends DistageSpecScalatest[F] {
@@ -79,3 +81,12 @@ final class DistageParallelLevelTestZIO5 extends DistageParallelLevelTest[Task](
 final class DistageParallelLevelTestZIO6 extends DistageParallelLevelTest[Task](DistageParallelLevelTest.zioCounter) {
   override protected def config: TestConfig = super.config.copy(logLevel = Log.Level.Info)
 }
+
+final class DistageParallelLevelTestBIO1 extends DistageParallelLevelTest[bio.Task](DistageParallelLevelTest.monixCounter)
+final class DistageParallelLevelTestBIO2 extends DistageParallelLevelTest[bio.Task](DistageParallelLevelTest.monixCounter)
+final class DistageParallelLevelTestBIO3 extends DistageParallelLevelTest[bio.Task](DistageParallelLevelTest.monixCounter)
+final class DistageParallelLevelTestBIO4 extends DistageParallelLevelTest[bio.Task](DistageParallelLevelTest.monixCounter)
+final class DistageParallelLevelTestBIO5 extends DistageParallelLevelTest[bio.Task](DistageParallelLevelTest.monixCounter)
+final class DistageParallelLevelTestBIO6 extends DistageParallelLevelTest[bio.Task](DistageParallelLevelTest.monixCounter) {
+  override protected def config: TestConfig = super.config.copy(logLevel = Log.Level.Info)
+}
diff --git a/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala b/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala
index 9abc7d5b3..c3e6d9688 100644
--- a/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala
+++ b/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala
@@ -1,6 +1,7 @@
 package izumi.fundamentals.bio.test
 
 import izumi.functional.bio.{BIO, BIOArrow, BIOAsk, BIOBifunctor3, BIOFork, BIOFork3, BIOFunctor, BIOLocal, BIOMonad, BIOMonad3, BIOMonadAsk, BIOMonadError, BIOPrimitives, BIOPrimitives3, BIOProfunctor, BIOTemporal, BIOTemporal3, F}
+import monix.bio
 import org.scalatest.wordspec.AnyWordSpec
 
 import scala.concurrent.duration._
@@ -46,6 +47,7 @@ class BIOSyntaxTest extends AnyWordSpec {
     }
 
     x[zio.IO](zio.UIO.succeed(()), zio.UIO.succeed(()))
+    x[bio.IO](bio.UIO.evalTotal(()), bio.UIO.evalTotal(()))
   }
 
   "BIOAsync3.race is callable along with all BIOParallel syntax" in {
@@ -69,6 +71,7 @@ class BIOSyntaxTest extends AnyWordSpec {
     }
 
     assert(new X[zio.IO].hello != null)
+    assert(new X[bio.IO].hello != null)
   }
 
   "BIO.widen/widenError is callable" in {
@@ -118,6 +121,11 @@ class BIOSyntaxTest extends AnyWordSpec {
     y[zio.IO]
     z[zio.IO]
     zz[zio.IO]
+
+    x[bio.IO]
+    y[bio.IO]
+    z[bio.IO]
+    zz[bio.IO]
   }
 
   "BIO.when/unless/ifThenElse have nice inference" in {
@@ -168,13 +176,20 @@ class BIOSyntaxTest extends AnyWordSpec {
       F.fork(F.mkRef(4).flatMap(r => r.update(_ + 5) *> r.get.map(_ - 1))).flatMap(_.join) *>
       F.mkRef(4).flatMap(r => r.update(_ + 5) *> r.get.map(_ - 1)).fork.flatMap(_.join)
     }
-    lazy val _ = (
+    lazy val zioTest = (
       x[zio.IO],
       y[zio.IO](_: BIOTemporal[zio.IO]),
       z[zio.IO],
       `attach BIOPrimitives & BIOFork methods even when they aren't imported`[zio.IO],
       `attach BIOPrimitives & BIOFork3 methods to a trifunctor BIO even when not imported`[zio.ZIO],
     )
+
+    lazy val monixTest = (
+      x[bio.IO],
+      y[bio.IO](_: BIOTemporal[bio.IO]),
+      z[bio.IO],
+    )
+    lazy val _ = (zioTest, monixTest)
   }
 
   "Support BIO syntax for ZIO with wildcard import" in {