7mind / izumi

Productivity-oriented collection of lightweight fancy stuff for Scala toolchain
https://izumi.7mind.io
BSD 2-Clause "Simplified" License
613 stars 66 forks source link

Is there any complete example of Distage usage that doesn't involve ZIO? #2043

Open OOPMan opened 9 months ago

OOPMan commented 9 months ago

Hi there, I'm curious as to whether there is any example code that demonstrates using Distage without ZIO?

All the code in the example application and on the site involve ZIO but according to the page the framework is actually agnostic about this kind of thing?

I played with some code on my side and got a basic plugin injection system working with minimal issues but with the caveat that whenever I want to use the injector I have to do an unsafeGet. Obviously littering my code with stuff labelled unsafe makes me feel...well...unsafe, so I was wondering if there is another accepted way to do things "safely" in a basic Scala application that is not using ZIO, Cats or any other similar frameworks.

neko-kai commented 9 months ago

@OOPMan Not first-party, but there's an example project using cats-effect (and tofu) + distage - https://github.com/terjokhin/helpful - it might be outdated though. It might be a bit too large a project to use as an example, but the https://github.com/BlueBrain/nexus/ uses distage with cats-effect. Actually, if you look closely, many examples on the site do use cats-effect instead of ZIO, and the distage-example also uses cats-effect typeclasses and doobie (although it does run on ZIO)

For actually running on cats IO instead of ZIO, there aren't many changes you have to make compared to distage-example project:

As for unsafeGet - you should be able to use the .use method instead, most of the time. But, if you're stuck, you can use .toCats[IO] method on the Lifecycle value you get from Injector.produce to get a cats.effect.Resource – you can then use the Resource value as usual with cats.effect.IOApp or otherwise.

EDIT: If you're not using either ZIO or Cats Effect, then the prescription is simpler:

You can find Identity in izumi.fundamentals.platform.functional.Identity

OOPMan commented 9 months ago

@neko-kai I think maybe you misunderstood. I'm trying to use distage without using ZIO, Cats or anything like that.

As mentioned, I can get stuff with unsafeGet or unsafeAllocate and that's nice but in this scenario if I try to make use of use I get errors messages like so:

[error] 36 |    }
[error]    |     ^
[error]    |No given instance of type izumi.functional.quasi.QuasiPrimitives[[X0] =>> Object] was found for parameter F of method use in class SyntaxUse.
[error]    |I found:
[error]    |
[error]    |    izumi.functional.quasi.QuasiPrimitives.fromCats[[X0] =>> Object, Sync](
[error]    |      /* missing */summon[izumi.fundamentals.orphans.cats.effect.kernel.Sync[Sync]]
[error]    |        ,
[error]    |    ???)
[error]    |
[error]    |But no implicit values were found that match type izumi.fundamentals.orphans.cats.effect.kernel.Sync[Sync].
[error]    |
[error]    |The following import might make progress towards fixing the problem:
[error]    |
[error]    |  import izumi.functional.quasi.QuasiIO.fromCats
[error]    |
[error] one error found

Do I HAVE to use something like ZIO or Cats to make this work?

From what I saw in the documentation it seemed like this wasn't necessarily the case?

OOPMan commented 9 months ago

Sorry, meant to include some sample code as well

import distage._

trait A:
  def run(s: String) = s + " " + s

class anA extends A

trait B:
  val a: A
  def run(s: String) = s + a.run(s)

class aB(override val a: A) extends B

val testModule = new ModuleDef:
  make[A].from[anA]
  make[B].from[aB]

object Test:
  val injector = Injector()

  def main(args: Array[String]): Unit =
   // This works
    val (a, b) = injector.produceGet[B](testModule).unsafeAllocate()
    println(a.run("test"))
    b.apply()
    // This doesn't, failing when I attempt to make use of the .use pathway due to missing implicit
    val p = injector.plan(testModule, Activation.empty, Roots.target[B]).getOrThrow()
    val r = injector.produce(p)
    r.use {
      os => os.get[B].run("zing")
    }
neko-kai commented 9 months ago

@OOPMan This seems to be an inference issue with Scala 3... Using produceRun instead of use avoids it, since most examples use it, I guess we managed not to run into it ourselves yet. https://scastie.scala-lang.org/5HZmALDBQke5uzVXiltr6w

import distage._

trait A:
  def run(s: String) = s + " " + s

class anA extends A

trait B:
  val a: A
  def run(s: String) = s + a.run(s)

class aB(override val a: A) extends B

val testModule = new ModuleDef:
  make[A].from[anA]
  make[B].from[aB]

object Test:
  val injector = Injector()

  def main(args: Array[String]): Unit =
    injector.produceRun(testModule) {
      (b: B) => println(b.run("zing"))
    }

I'll see if I can fix it. In the meantime you should be able to avoid it by passing [Identity] type parameter to methods such as .use explicitly.

OOPMan commented 9 months ago

@neko-kai Oh man, to think it was so simple XD

Thanks a lot :-)

neko-kai commented 9 months ago

@OOPMan This is actually a pretty weird bug. If I add : Unit to your expression, it now works [1]. Same as if it's : Boolean. It only triggers if a java type such as String or java.lang.Boolean is returned [2]. I'll try to minimize and report it to Dotty team.

OOPMan commented 9 months ago

@neko-kai Oh sweet, I found a weird Scala bug :-)