Elmは静的型付けで、さらに制約の強い型システムを採用したプログラミング言語です。こう聞くと、同じWEBフロントで活躍するJavaScript等と比較すると、何やらとっつきづらそうで書くのが大変そうなイメージを持ちます。確かにElmは実行時に安全に動かすために、コンパイラが危険なコードを防ぐため、コードを書くのが煩わしい部分があります。今回では誰もが一度はそう思うであろうHTMLのイベントに関する煩わしいコードの回避法と逆に一度、扱いやすい型が確定すると逆に堅苦しさが消えて良いということをお見せしたいと思います。今回は以下の二つのコードを通して説明を行っていきたいと思います。なおスライダーの例は、とりさんの例を使用させていただきました。ありがとうございます!
書き手が絶対ありえないと思う煩わしい分岐
まず初めに以下のような、年齢を選択するセレクトボックスを考えてみましょう。
selectとoptionのリストを組み合わせることでセレクトボックスを構築します。せっかくElmなので、セレクトボックスには必ず年齢を表す整数(Int)しか来ないように、ageOptions
という内部関数を定義します。これで書き手は、必ず整数しか選択されないことを知っています。テストなんて書きようがありません。勝ちました。ご視聴ありがとうございました。
view : Model -> Html Msg
view model =
let
ageOptions : List Int -> List (Html Msg)
ageOptions intList =
intList |> List.map
(\age ->
option [ value <| String.fromInt age ] [ text <| String.fromInt age ]
)
in
div []
[ select [ onChange UpdateAge ]
(ageOptions <| List.range 0 50)
, span [] [ text <| "あんたは" ++ String.fromInt model.age ++ "歳" ]
]
・・・がしかし、現実は非常です。UpdateAgeが受け取るのはStringです。String.toIntでMaybeをパターンマッチし、Nothingには来ないことを知りつつも無駄な分岐を書くことになります。これはElmが間違っているわけではありません。Htmlのtarget.value
には制約上Stringしか渡すことができません。Stringが来る以上、整数の変換は失敗を考慮しなければなりません。しかし、そうなると気合を入れて内部関数を定義した部分が無駄に感じてしまいます。これは煩わしいですね・・・。
type Msg
= UpdateAge String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateAge ageText ->
case String.toInt ageText of
Just age ->
{ model | age = age }
Nothing ->
model
イベントが渡してくる値を加工して型を確定させよう
ElmでHTMLのイベントに詳しくなろうの記事にも書いた内容ですが、selectboxが持っている値(状態)はJavaScriptがchange
イベントの発火時にコールバック関数よりtarget.value
が運んでくる内容です。それをElmがただ乗りしているためStringが値として運ばれてきます。その値は、Html.Events.targetValue関数の返す値をJson.Decoderで任意のmsg
に加工することができ、Html.Events.on関数で自由に発火することができるのです。Stringの値をIntに変換し分岐を書く部分は同じですが、正常な処理を行うmsgをintMsg
として注入してあげ、起こり得ない処理はnoOp
(msg)として空振りさせて上げ、msgを大きく分岐させることで関心ごとの分離を行っています。しかも、この関数は一度定義してしまえば、Intとして扱いたいときにどこにでも使い回すことができます。さらにどのイベントでも使えるように一般化することができますが、それは次のスライダーで扱います。
onIntChange : msg -> (Int -> msg) -> Html.Attribute msg
onIntChange noOp intMsg =
Html.Events.targetValue
|> Json.map
(\inputText ->
case String.toInt inputText of
Just inputInt ->
intMsg inputInt
Nothing ->
noOp
)
|> Html.Events.on "change"
それでは完成品のModelとViewになります。先ほどの違いとしては、onIntChange関数にNoOpとUpdateAgeを渡すというイベントに変わっただけです。
type alias Model =
{ age : Int }
initialModel : Model
initialModel =
{ age = 0 }
view : Model -> Html Msg
view model =
let
ageOptions : List Int -> List (Html Msg)
ageOptions intList =
intList |> List.map
(\age ->
option [ value <| String.fromInt age ] [ text <| String.fromInt age ]
)
in
div []
[ select [ onIntChange NoOp UpdateAge ]
(ageOptions <| List.range 0 50)
, span [] [ text <| "あんたは" ++ String.fromInt model.age ++ "歳" ]
]
それでは煩わしかったupdate
関数を見てみましょう。そうです。UpdateAgeの中は何もすることがなくなりました。これでageが晴れてIntであることが確定したため、自由に計算することができ柔軟で安全な世界が訪れます。
type Msg
= UpdateAge Int
| NoOp
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateAge age ->
{ model | age = age }
NoOp ->
model
おまけ(もっともっと安全に)
今回はNoOpで異常な値を空振りさせましたが、型安全で保証した上で、万が一の異常を検知したい場合は、ポート経由でSentryエラー通知をすることもできます。滅多に異常検知をする必要がないElmですが、不安な分岐が多い場合には有効活用すると良いでしょう。
システムが間違った値を送ってくるわけないはずでは? スライダー
それでは、スライダーについてみていきます。Htmlでは、<input type="range">
(MDN)を使うことでスライダーを用意できます。しかしイベント発火時に送られてくるtarget.value
の値はやはり、Stringです。スライダーは不動小数点を扱え、さらにinput
(動かしている間)とchange
(動かし終わり)の二つのイベントを扱うため、加工処理を抽出したonFloatEvent
関数で一般化しています。今回はスライダーが100%数値以外を送ってこないと確信した上で、NoOpではなくMaybe.withDefault
で0.0に丸め込みます。
onFloatEvent : String -> (Float -> msg) -> Html.Attribute msg
onFloatEvent eventName floatMsg =
let
inputText2FloatMsg : String -> msg
inputText2FloatMsg inputText =
floatMsg <| Maybe.withDefault 0.0 <| String.toFloat <| inputText
in
Html.Events.targetValue
|> Json.map inputText2FloatMsg
|> Html.Events.on eventName
onFloatChange : (Float -> msg) -> Html.Attribute msg
onFloatChange floatMsg =
onFloatEvent "change" floatMsg
onFloatInput : (Float -> msg) -> Html.Attribute msg
onFloatInput floatMsg =
onFloatEvent "input" floatMsg
ModelとViewの定義になります。
type alias Model =
{ value : Float }
initialModel : Model
initialModel =
{ value = 0.0 }
view : Model -> Html Msg
view model =
Html.div []
[ sliderFloatView -3.14 3.14 0.01 model.value SliderChanged
, text <| String.fromFloat model.value
]
sliderFloatView min max step value change =
Html.input
[ Attr.type_ "range"
, Attr.min <| String.fromFloat min
, Attr.max <| String.fromFloat max
, Attr.step <| String.fromFloat step
, Attr.value <| String.fromFloat value
, onFloatChange change
, onFloatInput change
]
[]
update関数です。スライダーを扱っているのですから、updateは素直にFloatの値を受け取るだけにしたいですね。全然煩わしくないです!
type Msg
= SliderChanged Float
update : Msg -> Model -> Model
update msg model =
case msg of
SliderChanged value ->
{ model | value = value }
まとめ
Elmは他の言語からすると一見、慎重すぎる部分があるとっつきづらい言語に見えます。しかし、一度扱いたい型に固定されると豊富な関数群がデフォルトで用意されているため、とても柔軟で表現力豊かになります。今回はHtml Eventにフォーカスして異常な値の変換を一極集中させることで煩わしさの回避をしました。それでは楽しいElmライフを!