SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Webアプリケーション開発技術の新潮流スタディーズ

関数型リアクティブプログラミング言語Elmに学ぶ フロントエンド開発の新しい形 【前編】

Webアプリケーション開発技術の新潮流スタディーズ 第4回


FRPの基本となるSignal

前ページでは、Elmの基本的な文法を用いて静的なWebページを生成しました。 ここからは動的なページを生成するのに必要なSignalの概念を、Signalを利用したサンプルコードを見ながら学んでいきます。

なお、以下に挙げるサンプルコードは全てTry Elmにコピー&ペーストして実行できます。

Signal型と代表的な関数たち

Elmで扱うSignalは実に様々です。 マウスの動きやクリック、フォーム要素への入力、WebSocket通信などは全てSignalとして扱います。 ここでは例として、マウスの座標やクリック回数を表示するプログラムを、Elmの代表的な関数を使って記述してみます。

以降の説明では、モジュール名を省略せずにSignal.xxxと記述しています。これは異なるモジュールに定義されている同名の関数と区別するためです(Signal.mapList.mapなど)。関数xxxを直接使用するためには、プログラムの先頭にimport Signal exposing (xxx)と記述します。

Signal.map関数

Signal.map関数は、Signalを流れる値をそれぞれ別の値に変換します。 使い方はList.map関数と同じです。 Signal.map関数の型と動作のイメージは次に示すとおりです。

Signal.map : (a -> b) -> Signal a -> Signal b
Signal.map関数の動作イメージ
Signal.map関数の動作イメージ

次の例は、マウスの座標を画面に表示するコードです。

import Html exposing (Html, text)
import Mouse

main : Signal Html
main =
  Signal.map (\pos -> text (toString pos)) Mouse.position

Mouse.positionSignal (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
Signal.foldpの動作イメージ
Signal.foldpの動作イメージ

次の例は、マウスをクリックした回数を数えて画面に表示するコードです。

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関数の動作イメージ
Signal.map2関数の動作イメージ

次のコードではSignal.map2関数を使い、Mouse.isDownMouse.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
Signal.sampleOn関数の動作イメージ
Signal.sampleOn関数の動作イメージ

次の例は、マウスをクリックしたタイミングで、その座標を拾うコードです。

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
Signal.merge関数の動作イメージ
Signal.merge関数の動作イメージ

次のコードでは、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の代わりです。 次のコードにあるabは同じものです。

a = map sqrt Mouse.x
b = sqrt <~ Mouse.x

(~)

<~と組み合わせると、Signal.map2の代わりになります。さらにつなげるとSignal.map3Signal.map4……になります。

次のabは同じものです。

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公式のアーキテクチャを導入します。 お楽しみに。

修正履歴

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
Webアプリケーション開発技術の新潮流スタディーズ連載記事一覧

もっと読む

この記事の著者

鳥居 陽介(株式会社ワークスアプリケーションズ)(トリイ ヨウスケ)

株式会社ワークスアプリケーションズ所属。イケてるアプリケーションを死ぬほど楽に作るために研究を続ける日々。社内での立ち位置は「フロントエンドのナウい人」。最近エバンジェリストという肩書きが付いた。趣味は作曲とスノーボード。 Blog: http://jinjor-labo.hatenablog.com/ ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/8873 2017/03/02 16:39

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー