The manual serves as an introduction of the concepts in REScala. The full API is covered in the scaladoc especially for Signals and Events. More details can be found in [7, 3]. The manual introduces the concepts related to functional reactive programming and event-based programming from a practical perspective.
- The chapter The Basics covers how to get started and integrate REScala into a program, and
- The chapter Common combinators presents REScalas most common features for composing signals and events.
- The chapter Combinators describes other combinators of REScalas.
- If you encounter any problems, check out the chapter Common Pitfalls.
- The readers interested in a more general presentation of these topics can find thee essential references in the section related work.
Create a build.sbt
file in an empty folder with the following contents:
scalaVersion := "2.12.6"
resolvers += Resolver.bintrayRepo("stg-tud", "maven")
libraryDependencies += "de.tuda.stg" %% "rescala" % "0.24.0"
Install sbt and run sbt console
inside the folder,
this should allow you to follow along the following examples.
The code examples in the manual serve as a self contained Scala REPL session, all code is executed and results are annotated as comments using tut. Most code blocks can be executed on their own when adding this import, but some require definitions from the prior blocks. To use all features of REScala the only required import is:
import rescala.default._
This chapter is about using Var and Evt, the imperative subtypes of Signal and Event.
A Var[T]
holds a value of type T
.
Var[T]
is a subtype of Signal[T]
. See also the chapter about Signals.
In contrast to declarative signals, Var
s can be read and written to.
val a = Var(0)
val b = Var("Hello World")
val c = Var(List(1,2,3))
val d = Var((x: Int) => x * 2)
Vars enable the framework to track changes of input values. Vars can be changed directly, via set and transform, which will trigger a propagation:
a.set(10)
println(a.now)
// 10
a.transform( val => val + 1 )
println(a.now)
// 11
c.transform( list => 0 :: list )
println(c.now)
// List(0, 1, 2, 3)
Imperative events are defined by the Evt[T]
type.
Evt[T]
are a subtype of Event[T]
.
The value of the parameter T
defines the value that is attached to the event.
If you do not care about the value, you can use an Evt[Unit]
.
If you need more than one value to the same event, you can use tuples.
The following code snippet shows some valid events definitions:
val e1 = Evt[Int]()
val e2 = Evt[Unit]()
val e3 = Evt[(Boolean, String, Int)]()
Events can be fired with the method fire
, which will start a propagation.
e1.fire(5)
e2.fire(())
e3.fire((false, "Hallo", 5))
The current value of a signal can be accessed using the now
method.
It is useful for debugging and testing, and sometimes inside onclick handlers.
If possible, use observers or even better combinators instead.
println((a.now, s.now, t.now))
// (0, 2, false)
The observe
attaches a handler function to the event.
Every time the event is fired, the handler function is applied to the current value of the event.
val e = Evt[String]()
val o = e.observe({ x =>
val string = "hello " + x + "!"
println(string)
})
e.fire("annette")
// hello annette!
e.fire("tom")
// hello tom!
If multiple handlers are registered, all of them are executed when the event is fired. Applications should not rely on the order of handler execution.
val e = Evt[Int]()
val o1 = e observe { x => println(x) }
val o2 = e observe { x => println(f"n: $x") }
e.fire(10)
// n: 10
// 10
e.fire(10)
// n: 10
// 10
Note that events without arguments still need an argument in the handler.
val e = Evt[Unit]()
e observe { x => println("ping") }
e observe { _ => println("pong") }
Scala allows one to refer to a method using the partially applied function syntax. This approach can be used to directly register a method as an event handler.
def m1(x: Int) = {
val y = x + 1
println(y)
}
val e = Evt[Int]
val o1 = e observe m1 _
e.fire(10)
Handlers can be unregistered from events with the remove
operator.
When a handler is unregistered, it is not executed when the event is fired.
If you create handlers, you should also think about removing them, when they are no longer needed.
val e = Evt[Int]()
val handler1 = e observe println
e.fire(10)
// n: 10
// 10
handler1.remove()
Signals are defined by the syntax Signal{
sigexpr}
, where sigexpr is a side effect-free expression.
A signal that carries integer values has the type Signal[Int]
.
Inside a signal expression other signals should be accessed with the ()
operator.
In the following code, the signal c
is defined to be a + b
.
When a
or b
are updated, the value of c
is updated as well.
val a = Var(2)
val b = Var(3)
val c = Signal { a() + b() }
println((a.now, b.now, c.now))
// (2,3,5)
a set 4
println((a.now, b.now, c.now))
// (4,3,7)
b set 5
println((a.now, b.now, c.now))
// (4,5,9)
The signal c
is a dependent / derivative of the vars a
and b
, meaning that the values of s
depends on both a
and b
.
Here are some more example of using signal expressions:
val a = Var(0)
val b = Var(2)
val c = Var(true)
val s = Signal{ if (c()) a() else b() }
def factorial(n: Int) = Range.inclusive(1,n).fold(1)(_ * _)
val a = Var(0)
val s: Signal[Int] = Signal {
val tmp = a() * 2
val k = factorial(tmp)
k + 2
}
Now, we have introduced enough features of REScala to give a simple example.
The following example computes the displacement space
of a particle that is moving at constant speed SPEED
.
The application prints all the values associated to the displacement over time.
val SPEED = 10
val time = Var(0)
val space = Signal{ SPEED * time() }
val o1 = space observe ((x: Int) => println(x))
// 0
while (time.now < 5) {
Thread sleep 20
time set time.now + 1
}
// 10
// 20
// 30
// 40
// 50
o1.remove()
The application behaves as follows.
Every 20 milliseconds, the value of the time
var is increased by 1 (Line 9).
When the value of the time
var changes,
the signal expression at Line 3 is reevaluated and the value of space
is updated.
Finally, the current value of the space
signal is printed every time the value of the signal changes.
Note that using println(space.now)
would also print the value of the signal, but only at the point in time in which the print statement is executed.
Instead, the approach described so far prints all values of the signal.
Combinators express functional dependencies among values. Intuitively, the value of a combinator is computed from one or multiple input values. Whenever any inputs changes, the value of the combinator is also updated.
Conversion between signals and events are fundamental to introduce time-changing values into OO applications -- which are usually event-based.
This section covers the basic conversions between signals and events.
Figure 1 shows how basic conversion functions can bridge signals and events.
Events (Figure 1, left) occur at discrete point in time (x axis) and
have an associate value (y axis).
Signals, instead, hold a value for a continuous interval of time (Figure 1, right).
The latest
conversion functions creates a signal from an event.
The signal holds the value associated to an event.
The value is hold until the event is fired again and a new value is available.
The changed
conversion function creates an event from a signal.
The function fires a new event every time a signal changes its value.
The latest
function applies to a event and returns and a signal
holding the latest value of the event e
.
The initial value of the signal is set to init
.
latest[T](e: Event[T], init: T): Signal[T]
Example:
val e = Evt[Int]()
val s: Signal[Int] = e.latest(10)
assert(s.now == 10)
e.fire(1)
assert(s.now == 1)
e.fire(2)
assert(s.now == 2)
e.fire(1)
assert(s.now == 1)
The changed
function applies to a signal and returns an event
that is fired every time the signal changes its value.
changed[U >: T]: Event[U]
Example:
var test = 0
val v = Var(1)
val s = Signal{ v() + 1 }
val e: Event[Int] = s.changed
val o1 = e observe ((x:Int)=>{test+=1})
v.set(2)
assert(test == 1)
v.set(3)
assert(test == 2)
The reactive r.map f
is obtained by applying f
to the value carried by r
.
The map function must take the parameter as a formal parameter.
The return type of the map function is the type parameter value of the resulting event.
If r
is a signal, then r map f
is also a signal.
If r
is an event, then r map f
is also an event.
val s = Var[Int](0)
val s_MAP: Signal[String] = s map ((x: Int) => x.toString)
val o1 = s_MAP observe ((x: String) => println(s"Here: $x"))
s set 5
// Here: 5
s set 15
// Here: 15
val e = Evt[Int]()
val e_MAP: Event[String] = e map ((x: Int) => x.toString)
val o1 = e_MAP observe ((x: String) => println(s"Here: $x"))
e.fire(5)
// Here: 5
e.fire(15)
// Here: 15
The fold
function creates a signal by folding events with a
given function. Initially the signal holds the init
value. Every time a new event arrives, the function f
is
applied to the previous value of the signal and to the value
associated to the event. The result is the new value of the signal.
fold[T,A](e: Event[T], init: A)(f :(A,T)=>A): Signal[A]
Example:
val e = Evt[Int]()
val f = (x:Int,y:Int)=>(x+y)
val s: Signal[Int] = e.fold(10)(f)
e.fire(1)
e.fire(2)
assert(s.now == 13)
The event e_1 || e_2
is fired upon the occurrence of one among e_1
or e_2
. Note that the events that appear in the event expression
must have the same parameter type (Int
in the next example).
The or combinator is left-biased, so if both e_1 and e_2 fire in the same
transaction, the left value is returned.
val e1 = Evt[Int]()
val e2 = Evt[Int]()
val e1_OR_e2 = e1 || e2
val o1 = e1_OR_e2 observe ((x: Int) => println(x))
e1.fire(1)
// 1
e2.fire(2)
// 2
The event e && p
(or the alternative syntax e filter p
) is fired if e
occurs and the predicate p
is satisfied.
The predicate is a function that accepts the event parameter as a formal parameter and returns Boolean
.
In other words the filter operator filters the events according to their parameter and a predicate.
val e = Evt[Int]()
val e_AND: Event[Int] = e filter ((x: Int) => x>10)
val o1 = e_AND observe ((x: Int) => println(x))
e.fire(5)
e.fire(3)
e.fire(15)
// 15
e.fire(1)
e.fire(2)
e.fire(11)
// 11
{::comment}
The dropParam
operator transforms an event into an event with
Unit
parameter. In the following example the dropParam
operator transforms an Event[Int]
into an Event[Unit]
.
val e = Evt[Int]()
val e_drop: Event[Unit] = e.dropParam
val o1 = e_drop observe (_ => println("*"))
e.fire(10)
// *
e.fire(10)
// *
The typical use case for the dropParam
operator is to make events
with different types compatible. For example the following snippet is
rejected by the compiler since it attempts to combine two events of
different types with the ||
operator.
scala> /* WRONG - DON'T DO THIS */
| val e1 = Evt[Int]()
e1: rescala.default.Evt[Int] = rescala.interface.RescalaInterfaceRequireSerializer#Evt:51
scala> val e2 = Evt[Unit]()
e2: rescala.default.Evt[Unit] = rescala.interface.RescalaInterfaceRequireSerializer#Evt:51
scala> val e1_OR_e2 = e1 || e2 // Compiler error
<console>:17: warning: a type was inferred to be `AnyVal`; this may indicate a programming error.
val e1_OR_e2 = e1 || e2 // Compiler error
^
e1_OR_e2: rescala.reactives.Event[AnyVal,rescala.parrp.ParRP] = (or rescala.interface.RescalaInterfaceRequireSerializer#Evt:51 rescala.interface.RescalaInterfaceRequireSerializer#Evt:51)
The following example is correct. The dropParam
operator allows
one to make the events compatible with each other.
val e1 = Evt[Int]()
// e1: rescala.default.Evt[Int] = rescala.interface.RescalaInterfaceRequireSerializer#Evt:51
val e2 = Evt[Unit]()
// e2: rescala.default.Evt[Unit] = rescala.interface.RescalaInterfaceRequireSerializer#Evt:51
val e1_OR_e2: Event[Unit] = e1.dropParam || e2
// e1_OR_e2: rescala.default.Event[Unit] = (or e1_OR_e2:17 rescala.interface.RescalaInterfaceRequireSerializer#Evt:51)
{:/comment}
Returns a signal that counts the occurrences of the event. Initially, when the event has never been fired yet, the signal holds the value 0. The argument of the event is simply discarded.
count(e: Event[_]): Signal[Int]
val e = Evt[Int]()
val s: Signal[Int] = e.count
assert(s.now == 0)
e.fire(1)
e.fire(3)
assert(s.now == 2)
The last
function generalizes the latest
function and
returns a signal which holds the last n
events.
last[T](e: Event[T], n: Int): Signal[List[T]]
Initially, an empty list is returned. Then the values are progressively filled up to the size specified by the programmer. Example:
val e = Evt[Int]()
val s: Signal[scala.collection.LinearSeq[Int]] = e.last(5)
val o1 = s observe println
// Queue()
e.fire(1)
// Queue(1)
e.fire(2)
// Queue(1, 2)
e.fire(3);e.fire(4);e.fire(5)
// Queue(1, 2, 3)
// Queue(1, 2, 3, 4)
// Queue(1, 2, 3, 4, 5)
e.fire(6)
// Queue(2, 3, 4, 5, 6)
Collects the event values in a (growing) list. This function should be used carefully. Since the entire history of events is maintained, the function can potentially introduce a memory overflow.
list[T](e: Event[T]): Signal[List[T]]
The latestOption
function is a variant of the latest
function which uses the Option
type to distinguish the case in
which the event did not fire yet. Holds the latest value of an event
as Some(val)
or None
.
latestOption[T](e: Event[T]): Signal[Option[T]]
Example:
val e = Evt[Int]()
val s: Signal[Option[Int]] = e.latestOption()
assert(s.now == None)
e.fire(1)
assert(s.now == Option(1))
e.fire(2)
assert(s.now == Option(2))
e.fire(1)
assert(s.now == Option(1))
The fold
Match
construct allows to match on one of multiple events.
For every firing event, the corresponding handler function is executed,
to compute the new state.
If multiple events fire at the same time,
the handlers are executed in order.
The acc parameter reflects the current state.
val word = Evt[String]
val count = Evt[Int]
val reset = Evt[Unit]
val result = Events.foldAll(""){ acc => Events.Match(
reset >> (_ => ""),
word >> identity,
count >> (acc * _),
)}
val o1 = result.observe(r => println(r))
//
count.fire(10)
reset.fire()
word.fire("hello")
// hello
count.fire(2)
// hellohello
word.fire("world")
// world
update(count -> 2, word -> "do them all!", reset -> (()))
// do them all!do them all!
Returns a signal holding the value computed by f
on the
occurrence of an event. Differently from fold
, there is no
carried value, i.e. the value of the signal does not depend on the
current value but only on the accumulated value.
iterate[A](e: Event[_], init: A)(f: A=>A): Signal[A]
Example:
var test: Int = 0
val e = Evt[Int]()
val f = (x:Int)=>{test=x; x+1}
val s: Signal[Int] = e.iterate(10)(f)
e.fire(1)
assert(test == 10)
assert(s.now == 11)
e.fire(2)
assert(test == 11)
assert(s.now == 12)
e.fire(1)
assert(test == 12)
assert(s.now == 13)
The change
function is similar to changed
, but it
provides both the old and the new value of the signal in a tuple.
change[U >: T]: Event[(U, U)]
Example:
val s = Var(5)
val e = s.change
val o1 = e observe println
s.set(10)
// Diff(Value(5), Value(10))
s.set(20)
// Diff(Value(10), Value(20))
The changedTo
function is similar to changed
, but it
fires an event only when the signal changes its value to a given
value.
changedTo[V](value: V): Event[Unit]
var test = 0
val v = Var(1)
val s = Signal{ v() + 1 }
val e: Event[Unit] = s.changedTo(3)
val o1 = e observe ((x:Unit)=>{test+=1})
assert(test == 0)
v set(2)
assert(test == 1)
v set(3)
assert(test == 1)
The flatten
function is used to ``flatten'' nested reactives.
It can, for instance, be used to detect if any signal within a collection of signals fired a changed event:
val v1 = Var(1)
val v2 = Var("Test")
val v3 = Var(true)
val collection: List[Signal[_]] = List(v1, v2, v3)
val innerChanges = Signal {collection.map(_.changed).reduce((a, b) => a || b)}
val anyChanged = innerChanges.flatten
val o1 = anyChanged observe println
// res105: rescala.reactives.Observe[rescala.parrp.ParRP] = res105:17
v1.set(10)
// 10
v2.set("Changed")
// Changed
v3.set(false)
// false
In this section we collect the most common pitfalls for users that are new to reactive programming and REScala.
The ()
operator used on a signal or a var, inside a signal expression,
returns the signal/var value and creates a dependency. The
now
operator returns the current value but does not
create a dependency. For example the following signal declaration
creates a dependency between a
and s
, and a dependency
between b
and s
.
val s = Signal{ a() + b() }
The following code instead establishes only a dependency between
b
and s
.
val s = Signal{ a.now + b() }
// <console>:17: warning: Using `now` inside a reactive expression does not create a dependency, and can result in glitches. Use `apply` instead.
// val s = Signal{ a.now + b() }
// ^
// s: rescala.default.Signal[Int] = s:17
In other words, in the last example, if a
is updated, s
is not automatically updated. With the exception of the rare cases in
which this behavior is desirable, using now
inside a signal
expression is almost certainly a mistake. As a rule of dumb, signals
and vars appear in signal expressions with the ()
operator.
Signals are not assignable. Signal depends on other signals and vars, the dependency is expressed by the signal expression. The value of the signal is automatically updated when one of the values it depends on changes. Any attempt to set the value of a signal manually is a mistake.
Signal expressions should be pure. i.e. they should not modify external variables.
For example the following code is conceptually wrong because the variable
c
is imperatively assigned form inside the signal expression (Line 4).
var c = 0 /* WRONG - DON'T DO IT */
// c: Int = 0
val s = Signal{
val sum = a() + b();
c = sum * 2
}
// s: rescala.default.Signal[Unit] = s:18
// …
println(c)
// 4
A possible solution is to refactor the code above to a more functional
style. For example, by removing the variable c
and replacing it
directly with the signal.
val c = Signal{
val sum = a() + b();
sum * 2
}
// c: rescala.default.Signal[Int] = c:17
// …
println(c.now)
// 4
When a signal s
is defined, a dependency is establishes with each of the
signals or vars that appear in the signal expression of s
.
Cyclic dependencies produce a runtime error and must be avoided.
For example the following code:
val a = Var(0) /* WRONG - DON'T DO IT */
// a: rescala.default.Var[Int] = a:15
val s = Signal{ a() + t() }
// <console>:17: error: overloaded method value + with alternatives:
// (x: Double)Double <and>
// (x: Float)Float <and>
// (x: Long)Long <and>
// (x: Int)Int <and>
// (x: Char)Int <and>
// (x: Short)Int <and>
// (x: Byte)Int <and>
// (x: String)String
// cannot be applied to (Boolean)
// val s = Signal{ a() + t() }
// ^
val t = Signal{ a() + s() + 1 }
// <console>:17: error: overloaded method value + with alternatives:
// (x: Double)Double <and>
// (x: Float)Float <and>
// (x: Long)Long <and>
// (x: Int)Int <and>
// (x: Char)Int <and>
// (x: Short)Int <and>
// (x: Byte)Int <and>
// (x: String)String
// cannot be applied to (Unit)
// val t = Signal{ a() + s() + 1 }
// ^
creates a mutual dependency between s
and
t
. Similarly, indirect cyclic dependencies must be avoided.
Vars and signals may behave unexpectedly with mutable objects. Consider the following example.
/* WRONG - DON'T DO THIS */
class Foo(init: Int) {
var x = init
}
val foo = new Foo(1)
val varFoo = Var(foo)
val s = Signal{ varFoo().x + 10 }
println(s.now)
// 11
foo.x = 2
println(s.now)
// 11
One may expect that after increasing the value of foo.x
in
Line 9, the signal expression is evaluated again and updated
to 12. The reason why the application behaves differently is that
signals and vars hold references to objects, not the objects
themselves. When the statement in Line 9 is executed, the
value of the x
field changes, but the reference hold by the
varFoo
var is the same. For this reason, no change is detected
by the var, the var does not propagate the change to the signal, and
the signal is not reevaluated.
A solution to this problem is to use immutable objects. Since the objects cannot be modified, the only way to change a filed is to create an entirely new object and assign it to the var. As a result, the var is reevaluated.
class Foo(val x: Int){}
val foo = new Foo(1)
val varFoo = Var(foo)
val s = Signal{ varFoo().x + 10 }
println(s.now)
// 11
varFoo set (new Foo(2))
println(s.now)
// 12
Alternatively, one can still use mutable objects but assign again the var to force the reevaluation. However this style of programming is confusing for the reader and should be avoided when possible.
/* WRONG - DON'T DO THIS */
class Foo(init: Int) {
var x = init
}
val foo = new Foo(1)
val varFoo = Var(foo)
val s = Signal{ varFoo().x + 10 }
println(s.now)
// 11
foo.x = 2
varFoo set foo
println(s.now)
// 11
Functions that operate on traditional values are not automatically transformed to operate on signals. For example consider the following functions:
def increment(x: Int): Int = x + 1
The following code does not compile because the compiler expects an
integer, not a var as a parameter of the increment
function. In
addition, since the increment
function returns an integer,
b
has type Int
, and the call b()
in the signal
expression is also rejected by the compiler.
val a = Var(1) /* WRONG - DON'T DO IT */
// a: rescala.default.Var[Int] = a:15
val b = increment(a)
// <console>:17: error: type mismatch;
// found : rescala.default.Var[Int]
// (which expands to) rescala.reactives.Var[Int,rescala.parrp.ParRP]
// required: Int
// val b = increment(a)
// ^
val s = Signal{ b() + 1 }
// s: rescala.default.Signal[Int] = s:16
The following code snippet is syntactically correct, but the signal has a constant value 2 and is not updated when the var changes.
val a = Var(1)
val b: Int = increment(a.now) // b is not reactive!
val s = Signal{ b + 1 } // s is a constant signal with value 2
The following solution is syntactically correct and the signal
s
is updated every time the var a
is updated.
val a = Var(1)
val s = Signal{ increment(a()) + 1 }
{: #related }
A more academic presentation of REScala is in [7]. A complete bibliography on reactive programming is beyond the scope of this work. The interested reader can refer to [1] for an overview of reactive programming and to[8] for the issues concerning the integration of RP with object-oriented programming.
REScala builds on ideas originally developed in EScala 3 -- which supports event combination and implicit events. Other reactive languages directly represent time-changing values and remove inversion of control. Among the others, we mention FrTime [2] (Scheme), FlapJax [6] (Javascript), AmbientTalk/R [4] and Scala.React [5] (Scala).
Several people contributed to this manual,
among the others David Richter, Gerold Hintz and Pascal Weisenburger.
{: #ref}
[1] A survey on reactive programming.
E. Bainomugisha, A. Lombide Carreton, T. Van Cutsem, S. Mostinckx, and W. De Meuter.
ACM Comput. Surv. 2013.
[2] Embedding dynamic dataflow in a call-by value language.
G. H. Cooper and S. Krishnamurthi.
In ESOP, pages 294–308, 2006.
[3] EScala: modular event-driven object interactions in Scala.
V. Gasiunas, L. Satabin, M. Mezini, A. Ńũnez, and J. Noýe.
AOSD ’11, pages 227–240. ACM, 2011.
[4] Loosely-coupled distributed reactive programming in mobile ad hoc networks.
A. Lombide Carreton, S. Mostinckx, T. Cutsem, and W. Meuter.
In J. Vitek, editor, Objects, Models, Components, Patterns, volume 6141 of Lecture Notes in Computer Science, pages 41–60. Springer Berlin Heidelberg, 2010.
[5] Deprecating the Observer Pattern with Scala.react.
I. Maier and M. Odersky.
Technical report, 2012.
[6] Flapjax: a programming language for ajax applications.
L. A. Meyerovich, A. Guha, J. Baskin, G. H. Cooper, M. Greenberg, A. Bromfield, and S. Krishnamurthi.
OOPSLA ’09, pages 1–20. ACM, 2009.
[7] REScala: Bridging between objectoriented and functional style in reactive applications.
G. Salvaneschi, G. Hintz, and M. Mezini.
AOSD ’14, New York, NY, USA, Accepted for publication, 2014. ACM.
[8] Reactive behavior in object-oriented applications: an analysis and a research roadmap.
G. Salvaneschi and M. Mezini.
AOSD ’13, pages 37–48, New York, NY, USA, 2013. ACM.