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
Signals 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 EventStreams,
so too you can create a Signal whose value depends on
several other Signals. 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
EventStreams, 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 EventStreams, 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 Signals.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 mapped 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 Vars (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