Search code examples
scalafunctional-programmingreactive-programmingfrp

How to implement FRP using implicit parameter?


I am studying Martin Odersky's Principles of Reactive Programming. When talking about the implementation of a simple FRP framework, he at the beginning gave one that uses StackableVariable(i.e. DynamicVairable) to keep track of the currently updated signal, which I can understand. But at the end of the slides, he mentioned that a cleaner solution is to use implicit parameter instead of DynamicVariable. Could anyone please show me how this can be done?


Solution

  • The link to the slides didn't work for me. As I tried googling it, I now use 1 as reference.

    The dynamic variable is a thread-local variable that holds the state of what Signal is currently evaluated. This is needed so that the apply method of Signal can access this information. Let's consider the following example code:

    val a: Signal[Int] = ???
    val b: Signal[Int] = ???
    val xPlusY: Signal[Int] = Signal(a() + b())
    

    Here, when a() is called, it adds itself to the list of dependencies to the currently evaluating Signal. As this information is not accessible anywhere else, we basically use a thread-local a.k.a. global variable.

    This solution has a few problems. For example, we can also call a() if we're not inside any Signal(). Also, well, we have to use a global variable.

    The solution would be to give this information to a() via an implicit argument: We change the signature from

    Signal[T]#apply(): T
    

    to

    Signal[T]#apply()(implicit triggeredBy: Signal[_])
    

    (Note that we'd probably want to use some new type TriggeredBy that encapsulates a Signal instead)

    This way, the implementation of this method will have access to its originating Signal without a global/thread-local variable.

    But now we have to supply this implicit somehow. One option is to also change the signature of the Signal-creation function from

    def Signal.apply[T](fun: => T): Signal[T]
    

    to

    def Signal.apply[T](fun: Signal[_] => T): Signal[T]
    

    Unfortunately, the syntax of our example code has to change then, because we have to supply a function instead of a body:

    val xPlusY: Signal[Int] = Signal { implicit triggeredBy => a() + b() }
    

    There are a few solutions to this problem:

    • Wait until implicit function types will have been implemented 2. This probably won't happen anytime soon, but it will allow us to write the Signal.apply signature as follows:

      def Signal.apply[T](fun: implicit Signal[_] => T): Signal[T]
      

      and then be able to write Signal(a() + b()) again.

    • Use some macro magic to transform code of the form Signal(a() + b()) to Signal.internalApply(implicit triggeredBy => a() + b()) code. This means, that Signal.apply is now a macro. This is the road that scala.rx3 has gone and it works well from a usage point of view. This also allows us to write Signal(a() + b()) again.

    Update: updated link to implicit functions explanation to a more detailed blog artikle