Elmを使っていると、ときどき全てのView関数に共通の情報を渡したいことがあります。
たとえば多言語対応のウェブアプリで現在どの言語が選択されているかなどの情報です。
import I18n
type Model =
{ lang : Lang
, ...
}
view : Model -> Html Msg
view model =
column
[ header model.lang model.header
, body model.lang model.body
, footer model.lang model.footer
]
body : I18n.Lang -> Body -> Html Msg
body lang model =
column
[ sectionGoatName lang model.goatName
, sectionGoatNum lang model.goatNum
, ...
]
sectionGoatName : I18n.Lang -> String -> Html Msg
sectionGoatName lang name =
column
[ row
[ goatNameLabel lang
, goatNameInput lang name
]
, goatNamePreview lang name
]
goatNamePreview : I18n.Lang -> String -> Html Msg
goatNamePreview lang name =
preview
[ Html.text <| I18n.label lang (I18n.TheGoatNameIs name)
]
type Lang
= En
| Ja
type Label
= TheGoatNameIs String
| ThereIsGoats Int
...
label : Lang -> Label -> String
label lang =
case lang of
En ->
labelEn
Ja ->
labelJa
labelJa : Label -> String
labelJa label =
case label of
TheGoatNameIs str ->
"ヤギさんの名前は" ++ str ++ "です。"
ThereIsGoats n ->
"ヤギさんが" ++ String.fromInt n ++ "匹います。"
...
このように、子のViewに常に引き回す必要がある値のことをここでは「グローバルな状態」と呼んでいます。
上の例をご覧になってお分かりの通り、子のViewにバケツリレーをする必要があり、煩雑な気がします。
この記事では、この多言語対応を例にグローバルな状態をElmでうまくあつかう方法について複数の方針を示して考察します。
方法1. 愚直にやる
「めんどくさいからいい方法ないかな」と考え始めた瞬間に人間は視野が狭くなり、とても狭い世界で生きることになります。
終わった後で見返してみると「あれ?これって本当にやる必要あったっけ?」と後悔するのが愚かな人間という生き物です。
普段は視野が300度くらいあるヤギさんも葉っぱを前にすると周りが見えなくなるのと一緒です。

そうなる前にまずは現状どのような具体的な問題があるのかを知ることが非常に重要です。
では、まずは冒頭の例の通り愚直にそのまま引き回すことによってもたらされる具体的な弊害を考えましょう。
タイプ数が増えること以外に特にありませんね!
どうせエディタとかが補完してくれることですし、大したデメリットはありません。
メタプログラミングで何やってるか分からなくしたり、型レベルプログラミングを持ち込んで無駄に学習障壁をあげるなんて必要なさそうですね!
もうこのままでもいいと思いますが、ここで終わったら「終わりかよ!」って言われるのは分かってるんです。
仕方ないのでもうちょっと続けます。
僕はただ、世界一かわいいさくらちゃんの姿を世界に届けたいだけなんですが...
方法2. 技巧をこらす
そんなことやめといたほうがいいのに...
いいじゃんか、そのまま愚直にやれば。
一応技巧をこらせばある程度解決できます。
elm-html-with-context を使うとグローバルな状態をシュッとスマートにあつかうことができます。
でも、エレガントにやれて喜んでるのは今の自分だけで、他にそのコードを触る人とか、3ヶ月後の自分が見たら文句言ってきますよ?
import I18n
import WithContext exposing (WithContext)
type Model =
{ lang : Lang
, ...
}
type alias Html_ msg =
WithContext Lang msg
view : Model -> Html Msg
view model =
WithContext.toHtml model.lang <|
column
[ header model.header
, body model.body
, footer model.footer
]
body : Body -> Html_ Msg
body model =
column
[ sectionGoatName model.goatName
, sectionGoatNum model.goatNum
, ...
]
sectionGoatName : String -> Html_ Msg
sectionGoatName name =
column
[ row
[ goatNameLabel
, goatNameInput name
]
, goatNamePreview name
]
goatNamePreview : String -> Html_ Msg
goatNamePreview name =
preview
[ WithContext.fromHtml <|
\lang ->
Html.text <| I18n.label lang (I18n.TheGoatNameIs name)
]
は〜い、よかったですね〜 みんなが大好きなエレガントなやつですよ〜
文字数減らすためだけに新しいライブラリのドキュメントを読んで使い方を覚えましょうね〜
かっこいいよ〜
方法3. やらないでいい方法を考える
「どうやったらグローバルな状態をうまく渡せるかな〜」じゃないんですよ。
「どうやったらグローバルな状態を渡さないで済むかな〜」ってまず考えるのがデキる大人ってもんです。
ここではCSSを使ったやり方を検討してみましょう。
type Lang
= En
| Ja
toString : Lang -> String
toString lang =
case lang of
En ->
"en"
Ja ->
"ja"
text : List Attributes -> Html msg
text attrs =
Html.span attrs []
:lang(ja) {
.theGoatNameIs::before {
content: "ヤギさんの名前は" attr(data-goat-name) "です。";
}
.thereIsGoats::before {
content: "ヤギさんが" attr(data-goat-num) "匹います。";
}
...
}
import I18n
import Html.Attributes as Attributes exposing (attribute, class)
type Model =
{ lang : Lang
, ...
}
view : Model -> Html Msg
view model =
div
[ Attributes.lang <| I18n.toString model.lang
]
[ column
[ header model.header
, body model.body
, footer model.footer
]
]
body : Body -> Html Msg
body model =
column
[ sectionGoatName model.goatName
, sectionGoatNum model.goatNum
, ...
]
sectionGoatName : String -> Html Msg
sectionGoatName name =
column
[ row
[ goatNameLabel
, goatNameInput name
]
, goatNamePreview name
]
goatNamePreview : String -> Html Msg
goatNamePreview name =
preview
[ I18n.text
[ class "theGoatNameIs"
, attribute "data-goat-name" name
]
]
ほら、Elmでグローバルな状態をViewに渡す必要なんかなかったでしょ?
まとめ
何か困ったことがあったら、まず愚直にやって何が具体的に問題なのか冷静になって考えた上で、やらないでいい方法を考え、最終的にどうしても必要だったら技巧をこらしましょう。
あと、CSSを悪く言わないで |> _ <| うぇーん...

