FRPの基本となるSignal
前ページでは、Elmの基本的な文法を用いて静的なWebページを生成しました。 ここからは動的なページを生成するのに必要なSignalの概念を、Signalを利用したサンプルコードを見ながら学んでいきます。
なお、以下に挙げるサンプルコードは全てTry Elmにコピー&ペーストして実行できます。
Signal型と代表的な関数たち
Elmで扱うSignalは実に様々です。 マウスの動きやクリック、フォーム要素への入力、WebSocket通信などは全てSignalとして扱います。 ここでは例として、マウスの座標やクリック回数を表示するプログラムを、Elmの代表的な関数を使って記述してみます。
以降の説明では、モジュール名を省略せずにSignal.xxxと記述しています。これは異なるモジュールに定義されている同名の関数と区別するためです(Signal.mapとList.mapなど)。関数xxxを直接使用するためには、プログラムの先頭にimport Signal exposing (xxx)と記述します。
Signal.map関数
Signal.map関数は、Signalを流れる値をそれぞれ別の値に変換します。 使い方はList.map関数と同じです。 Signal.map関数の型と動作のイメージは次に示すとおりです。
Signal.map : (a -> b) -> Signal a -> Signal b
次の例は、マウスの座標を画面に表示するコードです。
import Html exposing (Html, text) import Mouse main : Signal Html main = Signal.map (\pos -> text (toString pos)) Mouse.position
Mouse.positionはSignal (Int, Int)型、言い換えれば値が(Int, Int)型のSignalです。マウスの動きに合わせて、X座標とY座標が組になって流れてきます。 mainの型はSignal Htmlにする必要があるため、Signal.map関数で変換を行っています。
イメージを膨らませるために、上記のコードを少し変更してみましょう。
import Html exposing (Html, text)
import Mouse
message : (Int, Int) -> String
message (x, y) =
let
x' = toString x
y' = toString y
in
"x: " ++ x' ++ " y:" ++ y'
main : Signal Html
main =
Signal.map (\pos -> text (message pos)) Mouse.position
Signal.foldp関数
Signal.foldp関数は「fold from the past」を意味し、Signalにおける畳み込みを可能にします。 畳み込みとは、供給される一連の値を使い、元の値に対して次々に処理を加えていくという動作です。 Signal.foldp関数の型との動作イメージは次に示す通りです。
Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b
次の例は、マウスをクリックした回数を数えて画面に表示するコードです。
import Html exposing (Html, text) import Mouse main : Signal Html main = Signal.map (\count -> text (toString count)) countClick countClick : Signal Int countClick = Signal.foldp (\clk count -> count + 1) 0 Mouse.clicks
countClickは、初期値を0、イベントの供給源をMouse.clicksとし、イベントのたびに前の値に1を加えることで、常に最新の値がクリック数の合計を表すSignalになっています。
イメージを膨らませるために、上記のコードを少し変更してみましょう。
import Html exposing (Html, text, ul, li)
import Mouse
main : Signal Html
main =
Signal.map messagesView messages
messagesView : List String -> Html
messagesView messages =
let
line mes = li [] [text mes]
in
ul [] (List.map line messages)
messages : Signal (List String)
messages =
Signal.foldp (\clk list -> "click!" :: list) [] Mouse.clicks
Signal.map2関数
Signal.map2関数は、2つのSignalに演算を施すことで新しいSignalを生成します。 この関数の型と動作イメージは次に示すとおりです。
Signal.map2 : (a -> b -> c) -> Signal a -> Signal b -> Signal c
次のコードではSignal.map2関数を使い、Mouse.isDownとMouse.positionのそれぞれの値を組にした新しいSignalを生成しています。
import Html exposing (Html, text) import Mouse main : Signal Html main = Signal.map (\pos -> text (toString pos)) position position : Signal (Bool, (Int, Int)) position = Signal.map2 (,) Mouse.isDown Mouse.position -- (,) : a -> b -> (a, b)
なお、Signal.map2関数にとどまらず、Signal.map3関数、Signal.map4関数……もあります。
Signal.sampleOn関数
Signal.sampleOn関数は1つ目のSignalのタイミングで、2つ目のSignalの値を拾います。関数の型と動作イメージは次に示すとおりです。
Signal.sampleOn : Signal a -> Signal b -> Signal b
次の例は、マウスをクリックしたタイミングで、その座標を拾うコードです。
import Html exposing (Html, text) import Mouse main : Signal Html main = Signal.map (\pos -> text (toString pos)) positionClicked positionClicked : Signal (Int, Int) positionClicked = Signal.sampleOn Mouse.clicks Mouse.position
Signal.merge関数
Signal.merge関数は、2つのSignalの値を発生順に両方拾います。
Signal.merge : Signal a -> Signal a -> Signal a
次のコードでは、1秒ごとに時刻を刻むイベントとクリックイベントの両方を拾うSignalを生成しています。
import Html exposing (..)
import Time exposing (..)
import Mouse
type Event = Click | Tick Time
main : Signal Html
main =
Signal.map view eventList
view : List Event -> Html
view list =
let
line i e = li [] [text (toString e)]
in
ul [] (List.indexedMap line list)
eventList : Signal (List Event)
eventList = Signal.foldp (::) [] events
events : Signal Event
events = Signal.merge clicks ticks
clicks = Signal.map (\clk -> Click) Mouse.clicks
ticks = Signal.map (\sec -> Tick sec) (every second)
Signal専用の演算子
ここから説明するのは、Signalを簡単に扱うために用意された演算子です。 糖衣構文なのでなくても特に困りませんが、いきなり目にした際に驚かないようにしておきます。
(<~)
Signal.mapの代わりです。 次のコードにあるaとbは同じものです。
a = map sqrt Mouse.x b = sqrt <~ Mouse.x
(~)
<~と組み合わせると、Signal.map2の代わりになります。さらにつなげるとSignal.map3、Signal.map4……になります。
次のaとbは同じものです。
a = map2 (,) Mouse.x Mouse.y b = (,) <~ Mouse.x ~ Mouse.y
「SignalのSignal」は存在しない
Elmには、値がSignalであるSignalは存在しません。
Signal (Signal a) -- 存在しない
SignalのSignalは、例えば複数のSignalを動的に切り替えたり、新しいSignalを動的に増やすような用途が考えられるでしょう。 Elmはメモリリークを防ぐなどの目的でこれを意図的に禁止しています。この設計上の選択は論文や動画で説明されていますので、興味があればのぞいてみてください。この言語仕様のおかげで、覚えなければいけない関数のバリエーションは格段に減り、何気ないコードが予想外の振る舞いをすることも少なくなっています。
なお、この制限が原因でアプリケーションの機能に限界を生じることは全くないと考えて大丈夫です。 これまでWebSocketやWebGLなど複雑なアプリケーションが多く実装されていますが、今のところ筆者はそのような事例を聞いたことがありません。
まとめ
今回は、Elmの基礎とSignalの取り扱いについて解説しました。
FRPとはどのようなものかというイメージをざっくりとでもつかめたでしょうか。 新しいパラダイムの言語という割には、そこまで難しいことをしていないような気もしませんか?
今の段階では「これをどうすれば現実的なアプリケーションが作れるのか分からないし、難しそうだ」という感想をお持ちかもしれません。 しかし、安心してください。実はほとんどのWebアプリ開発において、常にこのようなテクニックが必要になるわけではありません。
次回は現実的なアプリケーションを素早く作るために、Elm公式のアーキテクチャを導入します。 お楽しみに。
