This is the second (and last) part of the tutorial. All those Scala live examples have been compiled to JavaScript and are running inside your browser. The first part of the tutorial is here.
-----
While
You can get the current value of a
This is an
The two simplest
You can create a
Just as you can react to events with
Just like you can
Just like you can
Another example: filtering a list based on an entered string.
Similarly, you can pass
For instance, suppose you have an application that swaps the meaning of the mouse buttons while the
Passing down state:
If you need to derive a signal based on memory of previous state, use
A method that can be convenient sometimes is
When you have two interdependent signals, you need a way to prevent infinite loops (signal A caused signal B to change which causes signal A to change).
Another example is where rounding errors are not symmetric. For such scenarios, call
Similar to
Here is one of the most important and powerful features of
For this reason, there is a special subtype of
Remember that
Creating a
How do you get a
One way is subtype
The other way is to use the
-----
Now that we know the basic blocks of reactive programming, it's time to create some useful applications.
-----
Signal
Introduction
While
EventStream
represents a stream of discrete
values over time — that is, each value only exists instantaneously (in
practice that means that you can never say "what is the current
value?"), Signal
represents a continuous value. In
practical terms, a Signal
has a current value, and an EventStream
that, whenever the Signal
's value changes, fires the new
value.
now
You can get the current value of a
Signal
by calling
now
. However, functional style means that most of the time
it should be avoided, and you should find a way to have Signal
call your code with the value rather than you asking the Signal
for it.
change
This is an
EventStream
that fires an event for every
change to the Signal
's value.signal.change.foreach {v => assert(v == signal.now) }
Creating a Signal
The two simplest
Signal
classes share the names of
two important Scala keywords. They are Val
and Var
.
Val
lets you create an immutable Signal
.
Calling now
will always return the value it was created
with. change
will never fire an event. Var
lets you create a mutable Signal
. You can update it by writing
myVar() = newValue
, which is syntactic sugar for
myVar.update(newValue)
.object MyApp extends Observing { val myVal = Val(72) val myVar = Var(31) myVar.change foreach println myVar() = 29 // prints 29 }
You can create a
Signal
from an existing EventStream
by calling its hold
method. And you can create a
Signal
by transforming another Signal
via
map
and flatMap
.
foreach
Just as you can react to events with
EventStream#foreach
, you can
do something for all values of a Signal
with foreach
.
Passing a function to Signal#foreach
is equivalent to executing
the function and then calling foreach
on the change
EventStream
with the function.
map
Just like you can
map
an EventStream
to
get a new, transformed, EventStream
, you can map
Signal
s too. The resulting Signal
will reflect
the transformation expressed in the mapping function, both in its
definition of now
and in the events change
fires.val myVar = Var(3) val mapped = myVar.map(_ * 10) println(mapped.now) // prints 30 myVar() = 62 println(mapped.now) // prints 620
flatMap
Just like you can
flatMap
an EventStream
to get an EventStream
that "switches" between several EventStream
s,
so too you can create a Signal
whose value depends on
several other Signal
s. However, there are several
differences from EventStream
's flatMap
, and
its usage is slightly different. These differences stem from the fact
that a Signal
always has a value. So the semantics are,
that initially the resulting Signal
has the value of the Signal
created by applying the function passed to flatMap
to the
current value of the parent Signal
. This is reflected
in now
as well as in change
.val myVar1 = Var(72) val myVar2 = Var(69) val myVar3 = Var(false) val flatMapped = myVar3 flatMap { case true => myVar1 case false => myVar2 } println(flatMapped.now) // prints 72 myVar3() = true println(flatMapped.now) // prints 69 myVar2() = 2 myVar1() = 1 println(flatMapped.now) // prints 2 myVar3() = false println(flatMapped.now) // prints 1
Another example: filtering a list based on an entered string.
def filteredList(filterSignal: Signal[String], itemsSignal: Signal[Seq[String]]) = for { filter <- filterSignal items <- itemsSignal } yield items.filter(s => s.indexOf(filter) >= 0) /* The above desugars to: filterSignal.flatMap{ filter => itemsSignal.map{ items => items.filter(s => s.indexOf(filter) >= 0) } } */
Similarly, you can pass
flatMap
a function that returns
EventStream
s, and flatMap
will return an
EventStream
that “strings together”
the event streams that correspond to the signal’s values.For instance, suppose you have an application that swaps the meaning of the mouse buttons while the
Alt
key is
depressed. You have two EventStream
s, one firing left mouse button clicks,
and one firing right mouse button clicks, and a Signal
representing
the Alt
key's state.val selectClicks = altKey flatMap (if(_) leftButtonClicks else rightButtonClicks) val contextClicks = altKey flatMap (if(_) rightButtonClicks else leftButtonClicks)
Passing down state: foldLeft
If you need to derive a signal based on memory of previous state, use
foldLeft
. It works similar to EventStream.foldLeft.
zip
A method that can be convenient sometimes is
zip
. A parallel to the collections' zip
method,
it allows you to create a Tuple2
-valued Signal
from two Signal
s.def nameAndAge(name: Signal[String], age: Signal[Int]): Signal[(String,Int)] = name zip age
Preventing infinite loops
When you have two interdependent signals, you need a way to prevent infinite loops (signal A caused signal B to change which causes signal A to change).
Signal
has two methods that return a new Signal
identical to the parent Signal
but with an added safety filter. distinct
returns a new Signal
that filters out change events that are equal
to the signal's previous value. This suffices in most cases. But what if when signal A causes signal B
to change signal A, it sets to another value, infinitely? A silly illustration:myVar.map(_ + 1) >> myVar
Another example is where rounding errors are not symmetric. For such scenarios, call
nonrecursive
, which
uses a DynamicVariable
(Scala's ThreadLocal
) to prevent recursion.Time-consuming handling of values
Similar to
EventStream
, Signal
has nonblocking
and zipWithStaleness
methods.SeqSignal
Here is one of the most important and powerful features of
reactive
.
Suppose you have a list of contacts displayed. Whenever a contact is
added, removed, or updated, you need to update the display. Well, you
keep the list in a Signal[Seq[Contact]]
, listen to change
events, and update the display in response. You could have the display
be represented by a map
ped Signal
. But it's
expensive, especially with a lot of contacts, to update the entire
display whenever a single row changes.Signal
,
SeqSignal
. It adds, on top of Signal
's
members, an additional EventStream[SeqDelta]
called deltas
.
Depending on how you create the first SeqSignal
in the
chain, it will fire SeqDelta
events representing inserts,
removes, and updates. In addition multiple operations may be batched in
a single event, so for example multiple deletions do not need to result
in multiple, separate repaints. When you map
or flatMap
a SeqSignal
, the resulting SeqSignal
fires
delta events whose relationship to the delta events fired by the orignal
SeqSignal
is defined by the function you pass to map
or flatMap
.Remember that
SeqSignal[T]
extends Signal[Seq[T]]
.
So if you want to transform the elements a SeqSignal
, you
need to nest one map
inside another: you have to pass the map
method on SeqSignal[T]
/ Signal[Seq[T]]
a
function that operates on a Seq[T]
, and since you want to
operate on the elements inside that Seq
, you need to call map
on it.// When in.deltas fires an Insert of 3, // the returned SeqSignal's deltas will // fire an Insert of 30, // besides that now and change will // have the behavior inherited from Signal def timesTen(in: SeqSignal[Int]) = in.map { _ map (_ * 10)}
Creating a SeqSignal
How do you get a
SeqSignal
?One way is subtype
BufferSignal
. The easiest way to
create a BufferSignal
is with its factory (see example
below). You can modify it in two ways: Either use def value
,
which returns an ArrayBuffer
which you can mutate directly,
with each mutation resulting in the corresponding deltas being fired; or
use the same syntax you use to update Var
s (see example),
which will result in diff being calculated between the old and the new
values. You can override def comparator
to change the
equality test used by the diffing algorithm.val bufferSignal = BufferSignal(1, 2, 3, 4, 5) bufferSignal.value += 6 // fires an Insert bufferSignal() = List(2, 3, 4, 5, 6, 7) // fires a Remove and an Insert
The other way is to use the
SeqSignal
factory, which
takes a Signal[Seq[T]]
and returns a SeqSignal[T]
whose deltas are calculated by running the diffing algorithm on every
change. To create an immutable SeqSignal
, just pass it a Val
.val simpleSignal = Var(List(1,2,3)) val seqSignal = SeqSignal(simpleSignal) simpleSignal() = List(2,3,4) // seqSignal.deltas fires // a Remove and an Insert.
-----
Now that we know the basic blocks of reactive programming, it's time to create some useful applications.
But first some comments on the differences between JVM and GWT versions this library. JavaScript doesn't have WeakReference equivalent, so underlying listeners will not be garbage collected so automatically in GWT version. Instead an instance of Observing registers all listeners, and they all can be removed at once by the call to observing.removeAllListeners. It's a good idea to integrate this call into a higher level framework, so that we don't need to call it by hand. For example, when the user navigates to a different view, all listeners could automatically be removed. This is precicely what I did. We'll see it in real applications later.
Another difference is that method "
nonblocking
" is not yet implemented in reactive-gwt. It is not difficult to implement it with similar semantics, but with JavaScript limitations. I just didn't have enough time and a justifying use-case.
It is a good idea to become more familiar with GWT before creating useful applications with it. Let's evaluate GWT pros and cons.
No comments:
Post a Comment