BETA

JSXやテンプレートではない内部DSLを用いるElmのView表現の魅力

投稿日:2020-04-15
最終更新:2020-04-15

Qrunch初投稿です。Elmのハイライトが効くことがわかったので早速投稿してみました。

Elmはフロントエンドに特化した純粋関数型言語です。特徴として言語とフレームワークが一体化していることが挙げられます。フロントエンドのフレームワークを吟味する上で気になることと言えば、アーキテクチャにおけるViewの表現がどんな記述でどんな特徴を備えていると思います。

Elm以外のフレームワークのView表現

テンプレート(Angular, Vue)

例えば、AngularやVueでは、以下のようなHTML表現をそのままにプログラマブルなルールを属性や文字列の書き方で追加する方法です。この表現は、やはりデザイナやプログラミング初学者などにとっつきやすさという点で選ばれ易いのではないでしょうか。最近よく聞くようになってきたSvelteなどもテンプレートを採用していますね。

<div>  
  <span v-for="n in 10">{{ n }} </span>  
</div>  

JSX(React)

JSXは主にReactが採用しているJSXはJavaScriptにXMLライクのシンタックスを追加する言語拡張です。JavaScriptの表現力を活かしたままテンプレートのとっつきやすさも残すことができる方法です。テンプレートの静的な表現とロジックは分離して書きたいと思うエンジニアには好まれる手法だと思います。Reactの専売特許というわけではなく、あくまでJavaScriptに変換される表現なためVueなどで使うこともできるようです。

import React, { useState } from 'react';  

function Example() {  
  // Declare a new state variable, which we'll call "count"  
  const [count, setCount] = useState(0);  

  return (  
    <div>  
      <p>You clicked {count} times</p>  
      <button onClick={() => setCount(count + 1)}>  
        Click me  
      </button>  
    </div>  
  );  
}  

また、JSXは以下のようなJSのAPI呼び出しのシンタックスシュガーです。このようなカラクリがわかり易いのもエンジニアに好まれる特徴でしょうか。

render() {  
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);  
  }  

他にもこのフレームワークではこういう表現をしているよなどもあれば、是非コメントにて教えてください。

ElmでのViewの表現

Elmの説明まで長くなってしまいましたが、ご覧ください。以下が、ElmのViewの表現になります。一見読みづらい文法に見えますがキチンとトレードオフがあり、優れた点が多い手法になります。いくつかの特徴を踏まえて順に説明を行なっていきます。キモ!とならずに、よければ説明を見ていってください。

view model =  
    div []  
        [ button [ onClick Increment ] [ text "+1" ]  
        , div [] [ text <| String.fromInt model.count ]  
        , button [ onClick Decrement ] [ text "-1" ]  
        ]  

ElmのViewは単なる関数

Elmは関数型言語のため、関数呼び出しと関数の評価(1 + 1 --(評価)-> 2 このように式を計算した結果、値に変化することを評価と呼びます。) 関数呼び出しは、関数名 引数1 引数2 ... のようにスペース区切りで引数を渡すことができます。例えば、より大きい数値を返すmax関数に1と5を渡したら、5が評価値として返ります。Listの長さを返すlengthに長さ3のリストを渡したら、3が返ります。

max 1 5 -- == 5  
List.length [1, 2, 3] -- 3  

ルールは全く同じです。pタグを表現するpという名前の関数に、Attributeのリストと子要素のリストを引数として渡すことができます。子要素には、HTML上でのプレーンテキストと同等の意味を持つtext関数にElmの文字列の"hello"を渡したものが入ります。Elm上ではこれ以上評価はされないのですが、フロントエンドという意味では最終的にはHTML上のpタグとして評価されるはずです。

p [] [ text "hello" ] -- <p>hello</p>  

それでは、p関数の定義を見てみましょう。確かに今まで説明したことと全く同じ意味を持つシグネチャになります。他のタグを表す関数も見てみると全く同じ定義であることがわかります。つまり第2引数にネストする形でHTMLを表現できるというわけです。

p : List (Attribute msg) -> List (Html msg) -> Html msg  

初めて見た方にはとっつき易いとは言いにくいElmの関数呼び出しにおける表現ですが、とっても良い利点があります。それはElmが型に厳格であり間違いを起こすことが不可能であるという点です。つまり、以下のようなものはコンパイルが通しません。テンプレートやJSXなどの場合は、ある程度はテンプレートパーサが間違いを察して警告やエラーを出してくれるかもしれませんが、それは全てとは限りません。なぜか素通りし期待通りの結果が得られなかったりすることも多々あります。例えば、属性にタグを書いてしまったり、classの属性を子要素に書いたりすることはできません。また、数値の計算と文字列の結合を取り違えることなどは絶対ありません。

div [ p ] [ class "xxx" ] -- <div p>class="xxx"</div>  
p [] [ text "1" + 1 ] -- <p>11</p> <p>2</p>を期待していた。  

値は関数にして抽象化が簡単

関数呼び出しは評価されると単なる値になります。Viewを表現している関数だって例外ではありません。本当に手軽に関数に切り出すことができます。この特徴を上手く使うことで読みづらいと思っていた構文は、複雑であれば複雑であるほどその抽象度の高さが武器になります。

view : Model -> Html Msg  
view model =  
    ul []  
        [ li [] (link "top")  
        , li [] (link "hobby")  
        , li [] (link "about")  
        ]  


link : Html Msg  
link pageName =  
    a [ href ("/" ++ pageName) ] [ text pageName ]  

Elmの関数がそのまま呼び出せる

Elmの拡張子は、.elmになります。テンプレートだと、.html, JSXだと.jsxのように拡張子が分かれていますが、ElmはViewもロジックも全てが一つのElmの文法上で完結がしています。つまりViewを生成するためのロジックもElmでは等しくロジックとなります。例えば、子要素はリストのためリストの高階関数などはViewの生成ロジックとしてそのまま利用することができます。JavaScriptでもここら辺の高階関数はお馴染みだと思いますし、React等では常套手段のためすんなり慣れることができると思います。

view : Model -> Html Msg  
view model =  
    ul [] <|  
        List.map (\pageName -> li [] <| link pageName)  
            [ "top", "hobby", "about" ]  


link : Html Msg  
link pageName =  
    a [ href ("/" ++ pageName) ] [ text pageName ]  

単体テストとして扱える

Elmは純粋関数型言語のため、ほとんどの関数がテスト可能になります。Viewに関するテストは少し特殊ですが、テスト機構がしっかり備わっています。Viewを生成するロジックが複雑であれば複雑であるほど、このViewのテスト機構は効果を発揮します。Elmはコンパイル速度もテストも爆速です。Viewのテストはブラウザを利用したテストではなく、あくまでVirtualDOMのデータ構造とその計算によるテストのため爆速具合がそのままになります。私はテストが大好きで数万行ではありますが、実装コードと同量(かそれ以上)のテストが書いてあるにもかかわらず数秒で完了してしまいます。これは継続して開発を行っていく場合にはこれだけで採用に踏み切れるほどに大きな利点になるため、是非検討してみてください。

import Html  
import Test.Html.Query as Query  
import Test exposing (test)  
import Test.Html.Selector exposing (text)  


test "Button has the expected text" <|  
    \() ->  
        button [] [ text "I'm a button!" ]  
            |> Query.fromHtml  
            |> Query.has [ text "I'm a button!" ]  

まとめ

見た目のとっつきやすさや書きやすさでは非常に苦しいElmですが、それを補ってあまりある利点があるため是非毛嫌いせずに検討してみてはいかがでしょうか? Elm自体は採用せずともその考えは、テンプレートでもJSXでも十分に活かすことができます。それでは良いElmライフを!

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@vFwVY6p2UBYatzTAの技術ブログ

よく一緒に読まれる記事

2件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
04/30 12:57

分かりやすくて素晴らしい記事ありがとうございます!

1つ気になる部分があるんですが、記事中に出てくるlink関数の型は

link : Html Msg

ではなくて

link : String -> Html Msg

じゃないですか?

まだ初心者なので間違ってたらすいません。

05/05 07:35

その通りですね! ご指摘ありがとうございました!