raquo / Laminar

Simple, expressive, and safe UI library for Scala.js
https://laminar.dev
MIT License
746 stars 52 forks source link

ZIO / FS2 / etc. integration examples #140

Open raquo opened 1 year ago

raquo commented 1 year ago

Laminar does not and will not have built-in integrations with ZIO / FS2 / etc., but anyone who actually uses these libraries on the frontend can easily publish their own packages or even just share a gist. If you need help developing such a package, feel free to ask in our Discord – although I personally don't use ZIO / FS2 / etc. especially on the frontend, a few other people do.

I'm opening this ticket as a reference for any such integration examples until I have a more permanent place to put them. Please feel free to comment with more example code or links.

To start off, here is an example ZIO integration from @sherpal that he posted on discord:

package ziojs
import com.raquo.laminar.api.L.*
import zio.{Unsafe, ZIO}

package object laminarinterrop {

  /** Executes asynchronously the effect when the element is mounted. */
  def onMountZIO[El <: Element](zio: ZIO[services.GlobalEnv, Nothing, Unit]): Modifier[El] =
    onMountZIOWithContext(_ => zio)

  def onUnmountZIO[El <: Element](effect: ZIO[services.GlobalEnv, Nothing, Unit]): Modifier[El] =
    onUnmountCallback(_ =>
      Unsafe.unsafe { implicit unsafe =>
        services.runtime.unsafe.runToFuture(effect)
        ()
      }
    )

  /** Executes asynchronously the effect when the element is mounted. */
  def onMountZIOWithContext[El <: Element](
      effect: MountContext[El] => ZIO[services.GlobalEnv, Nothing, Unit]
  ): Modifier[El] =
    onMountCallback[El](ctx =>
      Unsafe.unsafe { implicit unsafe: Unsafe =>
        services.runtime.unsafe.runToFuture(effect(ctx))
        ()
      }
    )

  def onClickZIO[El <: Element](zio: ZIO[services.GlobalEnv, Throwable, Unit]): Binder[El] = {
    import Implicits.ObserverEnhanced
    onClick.mapTo(()) --> Observer.zio(zio)
  }

}

and

package ziojs.laminarinterrop

import com.raquo.laminar.api.A.*
import zio.stream.*
import zio.{CancelableFuture, URIO, Unsafe, ZIO}
import services.GlobalEnv

import scala.concurrent.Future

object Implicits {

  import services.runtime

  type InnerForGlobalEnv[A] = URIO[GlobalEnv, A]

  implicit val zioFlattenStrategy: FlattenStrategy[Observable, InnerForGlobalEnv, EventStream] =
    new FlattenStrategy[Observable, InnerForGlobalEnv, EventStream] {
      def flatten[A](parent: Observable[InnerForGlobalEnv[A]]): EventStream[A] =
        parent.flatMap(task =>
          EventStream.fromFuture(
            Unsafe.unsafe { implicit unsafe =>
              services.runtime.unsafe.runToFuture(task)
            },
            emitFutureIfCompleted = true
          )
        )
    }

  implicit class EventStreamObjEnhanced(es: EventStream.type) {

    /** Retrieve the result of the zio effect and send it through the laminar stream */
    def fromZIOEffect[A](effect: ZIO[GlobalEnv, Throwable, A]): EventStream[A] =
      EventStream.fromFuture(
        Unsafe.unsafe { implicit unsafe =>
          runtime.unsafe.runToFuture(effect)
        }: Future[A],
        emitFutureIfCompleted = true
      )

    /** Passes the outputs of the incoming [[zio.stream.ZStream]] into a laminar stream. /!\ The
      * ZStream will continue to run, even after the laminar stream is over with it. The returned
      * [[zio.CancelableFuture]] allows you to cancel it.
      *
      * I think this is not fantastic. We should do it in another way, probably.
      */
    def fromZStream[A](ztream: Stream[Nothing, A]): (CancelableFuture[Unit], EventStream[A]) = {
      val bus = new EventBus[A]

      val f: zio.CancelableFuture[Unit] = Unsafe.unsafe { implicit unsafe =>
        zio.Runtime.default.unsafe
          .runToFuture(ztream.foreach(elem => ZIO.attempt(bus.writer.onNext(elem))))
      }

      f -> bus.events
    }

  }

  implicit class EventStreamEnhanced[A](es: EventStream[A]) {
    def flatMapZIO[B](effect: ZIO[GlobalEnv, Nothing, B]): EventStream[B] =
      es.flatMap(_ => effect)
  }

  implicit class ObserverEnhanced(val observer: Observer.type) extends AnyVal {
    def zio(effect: ZIO[GlobalEnv, Throwable, Unit]): Observer[Any] = observer.apply[Any] { _ =>
      Unsafe.unsafe { implicit unsafe =>
        runtime.unsafe.runToFuture(effect)
        ()
      }
    }

    def zioFromFunction[A](effect: A => ZIO[GlobalEnv, Throwable, Unit]): Observer[A] =
      observer[A] { elem =>
        Unsafe.unsafe { implicit unsafe =>
          runtime.unsafe.runToFuture(effect(elem))
          ()
        }
      }
  }

  implicit class RichLaminarZIO[R, E, A](val zio: ZIO[R, E, A]) extends AnyVal {

    /** Takes the output of this ZIO effect and pipe it to the [[Var]], keeping the value. */
    def setToVar(laminarVar: Var[A]): ZIO[R, E, A] = zio.tap(a => ZIO.succeed(laminarVar.set(a)))
  }

  implicit class RichVar[A](val theVar: Var[A]) extends AnyVal {
    def setZIO(a: => A): ZIO[Any, Nothing, Unit] = ZIO.succeed(theVar.set(a))
  }

}
armanbilge commented 1 year ago

Self-aggrandized snake-oil coming up, sorry 😁

If you want to take this idea to the extreme: the main motivation for Calico is to answer the question, what if we built Laminar (or something Laminar-like, anyway) from the ground up with effects?

(It is effect-agnostic, so it can work with effect types other than Cats Effect IO.)

sherpal commented 1 year ago

(Random comment: this will work with ZIO 2.x. You need to adapt the Unsafe shenanigans if you still use ZIO 1.x.)

valentin-panalyt commented 8 months ago

Question just out of interes: would it theoretically be possible to rewrite Laminar using ZIO or cats-effect - or is that not possible due to things like "No FRP Glitches" or other aspects in how the effect systems differ?

raquo commented 8 months ago

@valentin-panalyt Laminar only uses Airstream as a reactive layer. The underlying DOM operations work on low level observable-agnostic callbacks.

So, yes, in principle you can make a similar library with IO and ZIO instead of Airstream. And if you actually want Laminar-like observable API, you might want to use a streaming library, not an effect tracking library. So, more like FS2 / ZIO streams I guess. Either way, you will have to deal with various kinds of friction like FRP glitches because those libraries are designed for use in concurrent backend systems, and make different design decisions with different tradeoffs.

You might be interested to see how Tyrian and Calico are made, they both are FP-centric designs.

armanbilge commented 6 months ago

Laminar does not and will not have built-in integrations with ZIO / FS2 / etc.

We have actually gotten much closer to this now! Laminar and FS2 can now be integrated like this, without needing any other adapter library.

import cats.effect.unsafe.implicits._ // imports implicit IORuntime
EventStream.fromPublisher(fs2Stream.unsafeToPublisher())

"Creating Observables from Other Streaming APIs"