reactive-web: Lift をシンプルに

Signal

はじめに

EventStream は時間軸上の離散値のストリームを表します。すなわち、EventStream では各値は瞬間的にしか存在しません (実質上、これは「現在の値は何か?」という問いを発することができないことを意味します)。これに対し、Signal は連続値を表します。実用面から言えば、Signal は現在の値と、Signal の値が変わるたびに新しい値を発火する EventStream を持っています。

now

Signal の現在の値は、now を呼び出すことで取得できます。ただし、関数型スタイルの観点から言えば、ほとんどの場合、この方法は避けた方がよく、プログラマが Signal に値を尋ねるのではなく、Signal の側がその値を使ってプログラマのコードを呼び出す方法を用意する必要があります。

change

これは、Signal の値が変わるたびにイベントを発火する EventStream です。

signal.change.foreach {v =>
  assert(v == signal.now)
}

Signal の作成

2 つの最もシンプルな Signal クラスは、2 つの重要な Scala キーワードの名前を共有しています。具体的には、ValVar です。Val では、immutable な Signal を作成できます。now を呼び出すと、作成されたときの値が常に返されます。change は、決してイベントを発火しません。Var では、mutable な Signal を作成できます。この Signal は、myVar () = newValue と記述することで更新できます。この記述方法は、myVar.update(newValue) の糖衣構文です。

object MyApp extends Observing {
  val myVal = Val(72)
  val myVar = Var(31)
  myVar.change foreach println
  myVar ()= 29  // prints 29
}

既存の EventStreamhold メソッドを呼び出すことで、既存の EventStream から Signal を作成することができます。また、mapflatMap によってほかの Signal を変換することで、Signal を作成することができます。

foreach

EventStream#foreach でイベントに反応することができるように、foreachSignal のすべての値に対して処理を行うことができます。Signal#foreach に関数を渡すことは、関数を実行し、ついでこの関数を指定して change EventStream に対して foreach を呼び出すことと等価です。

map

EventStreammap すれば、新しい変換された EventStream を取得できるのと同様に、Signalmap することができます。その結果得られる Signal では、now の定義と、change が発火するイベントの両方に、マップ時の関数によって表現された変換が反映されます。

val myVar = Var(3)
val mapped = myVar.map(_ * 10)
println(mapped.now)  // prints 30
myVar ()= 62
println(mapped.now)  // prints 620

flatMap

EventStreamflatMap すれば、複数の EventStream の切り替えを行う EventStream を取得できるのと同様に、ほかのいくつかの Signal に依存する値を持つ Signal を作成することができます。ただし、EventStreamflatMap との違いがいくつかあり、使い方も若干異なります。これらの違いは、Signal が常に値を持っていることに起因するものです。具体的には、最初に得られる Signal は、 flatMap に渡された関数を親 Signal の現在の値に適用することで作成された Signal の値を持ちます。これは、nowchange の両方に反映されます。

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

次に示すのは、入力された文字列に基づいてリストをフィルタリングする例です。

def filteredList(filterSignal: Signal[String], itemsSignal: Signal[Seq[String]]) =
  for {
    filter <- filterSignal
    items <- itemsSignal
  } yield items.filter(s => s.indexOf(filter) >= 0)
/* 上の糖衣構文に対応する記述
filterSignal.flatMap{ filter =>
  itemsSignal.map{ items =>
    items.filter(s => s.indexOf(filter) >= 0)
  }
}
*/

同様に、EventStream を返す関数を flatMap に渡すことができ、その場合、flatMap は、シグナルの複数の値に対応する複数のイベントストリームを「縒り合わせた」 1 つの EventStream を返します。

たとえば、Alt キーが押されている間、マウスのボタンの意味を入れ替えるアプリケーションがあるとします。このアプリケーションの場合、2 つの EventStream があり、1 つは左のマウスボタンのクリックを発火し、もう 1 つは右のマウスボタンのクリックを発火します。また、Alt キーの状態を表す Signal があります。

val selectClicks = altKey flatMap (if(_) leftButtonClicks else rightButtonClicks)
val contextClicks = altKey flatMap (if(_) rightButtonClicks else leftButtonClicks)

状態を渡す: foldLeft

以前の状態の記憶に基づいてシグナルを派生させる必要がある場合は、foldLeft を使用します。foldLeft は、EventStream.foldLeft と同様に動作します。

zip

時として便利なメソッドに、zip があります。コレクションの zip メソッドと同様、このメソッドを使うと、2 つの Signal から、Tuple2 の値を持った 1 つの Signal を作成することができます。

def nameAndAge(name: Signal[String], age: Signal[Int]): Signal[(String,Int)] = name zip age

無限ループの防止

相互に依存する 2 つのシグナルがある場合は、無限ループ (シグナル A が シグナル B の変更を引き起こし、これがさらにシグナル A の変更を引き起こす状態) を防ぐ手段が必要です。Signal には、親 Signal と同じで、安全のためのフィルタが追加された新しい Signal を返すメソッドが 2 つ用意されています。このうち、distinct は、シグナルの以前の値と等しい change イベントをフィルターアウトした新しい Signal を返します。ほとんどの場合はこのメソッドで用が足ります。しかし、シグナル A の側がシグナル B にシグナル A 自身を変更させているために、シグナル A の値が変わってしまい、これが無限に続くような場合はどうすればよいでしょうか。馬鹿げた例ですが、次のような場合です。

myVar.map(_ + 1) >> myVar

また、別の例として、丸め誤差が対称的ではない場合があります。そのような場合には、nonrecursive を呼び出します。nonrecursive は、DynamicVariable (Scala の ThreadLocal) を使って再帰を防止します。

時間のかかる値の処理

EventStream の場合と同様、Signal にも nonblocking メソッドと zipWithStaleness メソッドがあります。