raquo / Airstream

State propagation and event streams with mandatory ownership and no glitches
MIT License
245 stars 28 forks source link

Documentation: Observers Feeding into Var #89

Closed juweoncom closed 3 years ago

juweoncom commented 3 years ago

Observers Feeding into Var documentation seems to be wrong or at least be confusing to me.

This doesn't compile:

val v = Var(List(1, 2, 3))
val adder = v.updater[Int]((currValue, nextInput) => currValue :+ nextInput)

adder.onNext(4)
v.now() // List(1, 2, 3, 4)

val inputStream: EventStream[Int] = ???

inputStream.foreach(adder) // !!! Compile Error
inputStream --> adder // Laminar syntax

Whereas this compiles:

val v = Var(List(1, 2, 3))
val adder = v.updater[Int]((currValue, nextInput) => currValue :+ nextInput)

adder.onNext(4)
v.now() // List(1, 2, 3, 4)

val inputStream: EventStream[Int] = ???

inputStream.foreach(_ => adder)(unsafeWindowOwner) // !!! Changes are here
inputStream --> adder // Laminar syntax

But still inputStream --> adder // Laminar syntax seems to have no effect. I assume because no owner is provided. I had to wrap it in something like:

inContext { _ =>
        inputStream --> adder
      }
yurique commented 3 years ago

This is going to be a no-op, I think

inputStream.foreach(_ => adder)
raquo commented 3 years ago

inputStream.foreach(adder) // !!! Compile Error

Airstream documentation often assumes that you have an implicit Owner in scope to reduce boilerplate (but it does explain that the foreach method needs one).

inputStream.foreach(_ => adder)

As Iurii said, this is a noop because the value you return inside the foreach callback is discarded. It needs to be inputStream.foreach(adder.onNext), or better yet inputStream.addObserver(adder).

If you're actually using Laminar, inputStream --> adder inside an element will work just fine. But on its own, this expression returns a Laminar Modifier which will not create a subscription until it's added to a Laminar element that is mounted. inContext does not change that. So if you are not actually using Laminar, you need to provide your own instances of Owner instead of trying to use Laminar instances. You can read more about how to create custom owners in the Ownership docs section.

glueware commented 3 years ago

Thank you for your good explanations! Yes, somehow I understood from documentation that it has something to do with ownership.

Nevertheless, even with implicit Owner in place the snippet of the documentation does not compile and this was confusing.

val v = Var(List(1, 2, 3))
val adder = v.updater[Int]((currValue, nextInput) => currValue :+ nextInput)

adder.onNext(4)
v.now() // List(1, 2, 3, 4)

val inputStream: EventStream[Int] = ???

implicit val owner: Owner = ??? // required implicit owner
inputStream.foreach(adder) // Compile error!!!

On the other hand it was confusing that inputStream --> adder works inside inContext but not outside. But no implicit parameter for the owner was required. So I understood that there has(!) to be some side effect that makes it work. But that mechanism was hidden to me. Somehow I knew from documentation that this has something to do ownership but I could not grasp it.

Some clear words about that mechanism in the documentation would help.

PS: I appreciate the philosophy of Airstream and Laminar very much and would like more people to use it. But perhaps some people had the same misunderstanding as me and put it aside.

glueware commented 3 years ago

Perhaps the pattern I'm using is illuminating:

val $varA = Var("Text")

// this doesn't work here
// val $streamA: EventStream[String] = ???
// $streamA --> $varA

div (
  inContext { _ => 
   // here it works producing a comment in UI
    val $streamA: EventStream[String] = ???
    $streamA --> $varA
  },
  children <-- $varA.signal.map(??? /*render*/)
)

Do you have a better idea?

raquo commented 3 years ago

inputStream.foreach(adder) // Compile error!!!

Ah, I missed it the first time. adder is an Observer, not a Function, so you need to use addObserver instead of foreach:

inputStream.addObserver(adder) // An implicit owner is still required.

On a side note, if you're facing a compiler error that you have a question about, it's easier for me to help if you tell me what the compiler actually says. There can be several reasons why something might not compile.


On the other hand it was confusing that inputStream --> adder works inside inContext but not outside

That's just not the case. Wrapping into inContext makes no difference. In your latest code snippet (it does help, thanks), the important difference between "this doesn't work here" and "here it works" is not inContext, it's that you put $streamA --> $varA inside of the div. This will work just as well:

val $streamA: EventStream[String] = ???
div(
  $streamA --> $varA,
  children <-- $varA.signal.map(??? /*render*/)
)

In Laminar terms, $streamA --> $varA is a Modifier. If it's not inside an element like div(), it's not gonna do anything. That's expected. Even if it's inside a div, it will only work if that div is mounted into the DOM. This is all explained in Laminar docs, for example, from the Binding observables section:

By now you must have realized that --> and <-- methods are the main syntax to connect / bind / subscribe observables to observers in Laminar.

To be more precise, such methods always return a Modifier that needs to be applied to an element. And when it is applied, a dynamic subscription is created such that it activates when the element is mounted, and deactivates when the element gets unmounted.

And then it goes on to explain how the ownership lifecycle activates and deactivates these subscriptions.

There's also a video which gives a broad introduction to Laminar concepts, make sure you've watched it if you haven't yet. It does talk about modifiers, and explains several code examples.

If you have more how-to / why questions, our gitter is a better channel for that, more people watching over that.