読者です 読者をやめる 読者になる 読者になる

アラタナエンジニアブログ

aratana Engineer's Blog

関数型プログラミング言語Elmのススメ

アラタナ Advent Calendar 2016

ECテクノロジー事業部の桑畑です。

今回の記事は アラタナ Advent Calendar 2016 3日目の記事です。 最近気になっているElmという言語について触れてみようと思います。

Elmとは?

最近、JavaScriptのトランスパイラが色々ありますよね。CoffeeScriptとかTypeScriptとか、更にそれを取り巻くwebpack, gulpなどのエコシステム…すごい勢いですね。

そんな中で、Elmという言語はとても硬派な部類に入ると思います。他の軟弱なJavaScriptライブラリなどとは無縁のままで一匹狼を貫き通すような…。 そんな僕の感想は置いておいて、ElmはHaskellライクな文法と厳密な関数型プログラミングでバグのないクライアントサイドアプリケーションを構築できます。

ひとまず実際に構築されたアプリに触れてみたい方は以下のToDoアプリやサンプル集を試してみて下さい。

Elm • TodoMVC

Elm Examples

ElmはFluxフレームワークの中で現在人気のあるReduxでも言及されているように、そのアーキテクチャは学ぶべきものがありますので多少触れてみるとコーディングの質の向上につながるかもしれません。

Elmのアーキテクチャ

今回BMIを計算するサンプルコードを作ってみたので、ソースコードに触れながらElmのアーキテクチャを見ていきましょう。

BMI計算機 by Elm 0.18 · GitHub

動作するサンプルはこちら

オンラインで試す場合は オンラインエディタ の左側にコードを貼り付けて Compile を実行すると動作します。

ローカルマシン上でコンパイルする場合は、インストール作業後、以下のコマンドでJavaScriptを含むHTMLファイルに変換できます。 生成される index.html をブラウザで開いて下さい。

$ elm-make BMI.elm --output index.html

main

プログラムを動かす構成要素は model, view, update の3つ。シンプルですね。

main = 
  Html.beginnerProgram
    { model  = initialModel
    , view   = view
    , update = update
    }
  • model は本当にただのデータです。振る舞いなどは一切含んでいません。
  • view : model -> Html msg はモデルのデータをHTMLに変換します。表現のみを担当していて、モデルのデータを更新することはできません。
  • update : msg -> model -> model は何らかのアクションに応じて、現在のモデルから新しいモデルへと変換を行います。HTMLなどの表現には一切触れません。

この3つが組み合わさることで、次のようにプログラムは動作します。

f:id:mather314:20161129180930p:plain

注目すべき点はデータを直接いじって変更するのではなく新しいモデルを生成していることで、モデルそのものは不変(immutable)であることです。 モデルで定義可能な状態がアプリケーションが持ちうる状態の全てであり、それ以外の状態は起こりません。また、 update 以外の方法でモデルが新しいモデルになることもありません。cf. 有限オートマトン - Wikipedia

モデル

今回の場合、モデルの中に含まれる情報は2つ、身長と体重だけです。

type alias Model =
  { height : Float
  , weight : Float
  }
  
initialModel : Model
initialModel = 
  { height = 171.5
  , weight = 65.5
  }

JavaScriptとは違い、 undefined などは指定できません。「型は Float だが値がない可能性がある」という場合は、 Maybe Float という型になります。

View

view : Model -> Html Msg
view model =
  let
    h = model.height / 100 -- メートルに変換
    bmi = model.weight / ( h * h ) 
  in
    div [] [
      form [] [
        heightInput model.height,
        weightInput model.weight
      ],
      p [] [ text ("BMI: " ++ (toString bmi)) ]
    ]
    
heightInput : Float -> Html Msg
heightInput h =
  let
    heightAttributes t = 
      [type_ t
      , value <| toString h
      , A.min "100.0"
      , A.max "250.0"
      , step "0.1"
      , onInput (updateFloat UpdateHeight)
      ]
  in
    p [] [
      label [] [text "身長"],
      input (heightAttributes "range") [],
      input (heightAttributes "number") [], text "cm"
    ]

一部省略しましたが、 view 関数の定義です。 BMI値の計算は let の中で行われ、変数代入ではなく「束縛」されます。hbmi の値は更新することができません。

続いて in で、束縛した値を用いてHTMLのパーツを定義しています。 身長と体重の入力部分はそれぞれ身長と体重の値しか用いないので、1変数のコンポーネントとして分解しました。

heightAttributes 関数はHTMLの <input> タグで使われる属性値のリストを表しています。数値の入力に関して制限をしているのと、 onInput によって「入力値が変更された場合に送信する msg 」を定義しており、updateFloat UpdateHeight は最終的に UpdateHeight 175.0 のような値を送信する想定です。

ところが、この onInput で渡される値は、数値に制限しているにも関わらず文字列 String となってしまうので、 UpdateHeight に渡す前に Float に変換する必要があります。 そのための関数が updateFloat です。

updateFloat : (Float -> Msg) -> String -> Msg
updateFloat updater str =
  case decodeString float str of
    Ok f ->
      updater f
    Err _ ->
      NoOp

一般に文字列から浮動小数に変換する場合は "abc" など変換できない場合が存在するため、エラーを考慮に入れる必要があります。 decodeString float では文字列を受け取って Result String Float という型の値を返します。 これは HaskellScalaにおける Either 型と同じです。

Result 型は具体的には2つのケースが値として得られます。

  • Ok Float : Float への変換が成功した。
  • Err String : 変換が失敗したためエラーメッセージを返した。

これをパターンマッチングで受け取り、成功した場合は変換に成功した値を使って UpdateHeight を、失敗した場合は NoOp (何もしない)を送ります。

Update

送信された msg の値は update 関数で処理されます。

type Msg =
  NoOp
  | UpdateHeight Float
  | UpdateWeight Float

update : Msg -> Model -> Model
update msg model =
  case msg of
    NoOp ->
      model
    UpdateHeight newHeight ->
      { model | height = newHeight }
    UpdateWeight newWeight ->
      { model | weight = newWeight }

{ model | height = newHeight }height の値だけを変更した新しいモデルを返す構文です。 そのため、この関数内で model 引数自体の値が書き換わることはありません。

また、はじめにも説明しましたが Msg 型で定義されたアクション以外はアプリケーション上発生させることができません。 そのため、 update で全てのケースに対して処理を記述できた以上は考慮漏れによるバグは発生しないのです。

debuggerの紹介

最新のElm 0.18では高機能なデバッガが利用可能になりました。コンパイル時に --debug を指定するだけで自動的に適用されます。

$ elm-make BMI.elm --debug --output debug.html

動作するサンプルはこちら

先程の図にあるような各 model の状態を時間を遡って確認することができるようになります。 詳しくは以下の公式ブログの動画を御覧ください。パないです。

the perfect bug report

まとめ

Elmの小規模なアプリケーションを見てきましたが、いかがだったでしょうか。JSで書く場合だったらもっと素早く作れると思いますが、これまで見てきたポイントで

  • BMI値や2つの入力部分の更新漏れ
  • 浮動小数に変換できない場合の考慮漏れ

などが起こりやすくなりますし、それに加えてスコープをきちんと管理しないと関数内の一時的な変数の管理が非常に煩雑になります。

AngularやReactなどのフレームワークではこの辺のモデルデータに基づくビューの更新などが容易にはなっていますが、如何せん素のJavaScriptに依存するため起こりうる状態の数が膨れ上がってしまいます。その点では Elm はとても堅牢です。

今回Elmに学ぶことは「アプリケーションの取りうる状態を定義・把握すること」「データと振る舞いとの分離」「データが不変であること」がとても堅牢なアプリケーションを作る秘訣であることです。 普段作っているアプリケーション全体をこのように管理するのは言語の違いもあって困難かもしれませんが、小さい部品を構築するときに頭に入れておくと良いでしょう。

次回

明日は木目沢さんの「オブジェクト指向設計についてあえてもう一度考えよう!」です。ご期待下さい!