Elmプログラミング言語入門

公開: 2022-03-11

非常に興味深く革新的なプロジェクトのリード開発者がAngularJSからElmへの切り替えを提案したとき、私の最初の考えは次のとおりでした。なぜですか?

ソリッドステートで、十分にテストされ、本番環境で実証された、うまく記述されたAngularJSアプリケーションがすでにありました。 そして、AngularJSからの価値のあるアップグレードであるAngular 4は、書き換えの自然な選択であった可能性があります。ReactまたはVueも同様です。 Elmは、人々がほとんど聞いたことのない奇妙なドメイン固有言語のように見えました。

Elmプログラミング言語

ええと、それは私がエルムについて何も知る前でした。 さて、特にAngularJSから移行した後は、ある程度の経験があれば、その「理由」に答えることができると思います。

この記事では、Elmの長所と短所、およびそのエキゾチックな概念のいくつかがフロントエンドWeb開発者のニーズに完全に適合する方法について説明します。 よりチュートリアルのようなElm言語の記事については、このブログ投稿をご覧ください。

Elm:純粋に関数型プログラミング言語

JavaまたはJavaScriptでのプログラミングに慣れていて、それがコードを書く自然な方法であると感じている場合、Elmを学ぶことはうさぎの穴に落ちるようなものです。

最初に気付くのは、奇妙な構文です。中かっこがなく、矢印や三角形がたくさんあります。

中括弧なしで生活することを学ぶかもしれませんが、コードのブロックをどのように定義してネストしますか? または、 forループ、またはその他のループはどこにありますか? 明示的なスコープはletブロックで定義できますが、古典的な意味でのブロックはなく、ループもまったくありません。

Elmは、純粋関数型で、強く型付けされ、反応的で、イベント駆動型のWebクライアント言語です。

この方法でプログラミングが可能かどうか疑問に思うかもしれません。

実際には、これらの品質が合わさって、優れたソフトウェアのプログラミングと開発の驚くべきパラダイムが得られます。

純粋関数型

新しいバージョンのJavaまたはECMAScript6を使用することで、関数型プログラミングを実行できると思うかもしれません。 しかし、それはその表面にすぎません。

これらのプログラミング言語では、言語構造の武器にアクセスでき、機能しない部分に頼りたいという誘惑に駆られます。 違いに本当に気付くのは、関数型プログラミング以外に何もできないときです。 その時点で、最終的に関数型プログラミングがいかに自然であるかを感じ始めます。

Elmでは、ほとんどすべてが関数です。 レコード名は関数であり、共用体タイプの値は関数です。すべての関数は、引数に部分的に適用された関数で構成されています。 プラス(+)やマイナス(-)のような演算子でさえ関数です。

プログラミング言語をそのような構造の存在ではなく純粋に機能的であると宣言するには、他のすべてが存在しないことが最も重要です。 そうして初めて、純粋関数型の方法で考え始めることができます。

Elmは関数型プログラミングの成熟した概念に基づいてモデル化されており、HaskellやOCamlなどの他の関数型言語に似ています。

強いタイプ

JavaまたはTypeScriptでプログラムする場合は、これが何を意味するかを知っています。 すべての変数は正確に1つのタイプでなければなりません。

もちろん、いくつかの違いがあります。 TypeScriptと同様に、型宣言はオプションです。 存在しない場合は、推測されます。 しかし、「任意の」タイプはありません。

Javaはジェネリック型をサポートしていますが、より良い方法です。 Javaのジェネリックは後で追加されたため、特に指定がない限り、型はジェネリックではありません。 そして、それらを使用するには、醜い<>構文が必要です。

Elmでは、特に指定がない限り、型は汎用です。 例を見てみましょう。 特定のタイプのリストを取得して数値を返すメソッドが必要だとします。 Javaでは次のようになります。

 public static <T> int numFromList(List<T> list){ return list.size(); }

そして、エルム語で:

 numFromList list = List.length list

オプションですが、常に型宣言を追加することを強くお勧めします。 Elmコンパイラは、間違ったタイプの操作を許可しません。 人間にとって、特に言語を学んでいる間は、そのような間違いを犯すのははるかに簡単です。 したがって、タイプアノテーションを使用した上記のプログラムは次のようになります。

 numFromList: List a -> Int numFromList list = List.length list

最初は別の行で型を宣言するのは珍しいように思えるかもしれませんが、しばらくすると自然に見え始めます。

Webクライアント言語

これが意味するのは、ElmがJavaScriptにコンパイルされるため、ブラウザーがWebページで実行できるということです。

そのため、Node.jsを使用したJavaやJavaScriptのような汎用言語ではなく、Webアプリケーションのクライアント部分を作成するためのドメイン固有言語です。 それ以上に、Elmには、ビジネスロジック(JavaScriptが行うこと)とプレゼンテーション部分(HTMLが行うこと)の両方をすべて1つの関数型言語で記述することが含まれています。

これらはすべて、TheElmArchitectureと呼ばれる非常に特殊なフレームワークのような方法で行われます。

反応性

Elm Architectureは、リアクティブなWebフレームワークです。 モデルの変更は、明示的なDOM操作なしで、ページにすぐにレンダリングされます。

このように、AngularやReactに似ています。 しかし、エルムも独自の方法でそれを行います。 その基本を理解するための鍵は、 viewおよびupdate機能の署名にあります。

 view : Model -> Html Msg update : Msg -> Model -> Model

Elmビューは、モデルのHTMLビューだけではありません。 これは、種類Msgのメッセージを生成できるHTMLです。ここで、 Msgは、定義した正確な共用体タイプです。

標準のページイベントであれば、メッセージを生成できます。 また、メッセージが生成されると、Elmはそのメッセージを使用して更新関数を内部的に呼び出し、メッセージと現在のモデルに基づいてモデルを更新し、更新されたモデルが再び内部でビューにレンダリングされます。

イベント駆動型

JavaScriptとよく似て、Elmはイベント駆動型です。 ただし、たとえば、非同期アクションに個別のコールバックが提供されるNode.jsとは異なり、Elmイベントは、1つのメッセージタイプで定義された個別のメッセージセットにグループ化されます。 また、他の共用体型と同様に、個別の型の値が運ぶ情報は何でもかまいません。

メッセージを生成できるイベントのソースは3つありますHtmlビューでのユーザーアクション、コマンドの実行、およびサブスクライブした外部イベントです。 そのため、 HtmlCmd 、およびSubの3つのタイプすべてに、引数としてmsgが含まれています。 また、汎用msgタイプは、3つの定義すべてで同じである必要があります。つまり、更新関数に提供されるものと同じであり(前の例では、大文字のMが付いたMsgタイプでした)、すべてのメッセージ処理が集中化されます。

現実的な例のソースコード

完全なElmWebアプリケーションの例は、このGitHubリポジトリにあります。 シンプルですが、RESTエンドポイントからのデータの取得、JSONデータのデコードとエンコード、ビュー、メッセージ、その他の構造の使用、JavaScriptとの通信、コンパイルとパッケージ化に必要なすべての機能など、日常のクライアントプログラミングで使用されるほとんどの機能を示しています。 Webpackを使用したElmコード。

ELMWebの例

アプリケーションは、サーバーから取得したユーザーのリストを表示します。

セットアップ/デモプロセスを簡単にするために、Webpackの開発サーバーを使用してElmを含むすべてをパッケージ化し、ユーザーのリストを提供します。

一部の機能はElmにあり、一部はJavaScriptにあります。 これは、相互運用性を示すという1つの重要な理由で意図的に行われます。 Elmを起動してみるか、既存のJavaScriptコードを徐々に切り替えるか、Elm言語で新しい機能を追加することをお勧めします。 相互運用性により、アプリは引き続きElmコードとJavaScriptコードの両方で動作します。 これは、Elmでアプリ全体を最初から開始するよりもおそらく優れたアプローチです。

サンプルコードのElm部分は、最初にJavaScriptからの構成データで初期化され、次にユーザーのリストが取得され、Elm言語で表示されます。 JavaScriptにすでにいくつかのユーザーアクションが実装されているとしましょう。そのため、Elmでユーザーアクションを呼び出すと、呼び出しがJavaScriptにディスパッチされます。

このコードでは、次のセクションで説明する概念と手法の一部も使用しています。

Elmコンセプトの適用

実際のシナリオで、Elmプログラミング言語のエキゾチックな概念のいくつかを見ていきましょう。

共用体タイプ

これはエルム語の純金です。 構造的に異なるデータを同じアルゴリズムで使用する必要がある状況をすべて覚えていますか? これらの状況をモデル化することは常に困難です。

次に例を示します。リストのページ付けを作成していると想像してください。 各ページの最後に、前のページ、次のページ、およびすべてのページへのリンクが番号で示されているはずです。 ユーザーがクリックしたリンクの情報を保持するようにどのように構成しますか?

前、次、ページ番号のクリックに複数のコールバックを使用したり、1つまたは2つのブールフィールドを使用してクリックした内容を示したり、負の数やゼロなどの特定の整数値に特別な意味を与えたりすることができます。これらのソリューションは、まさにこの種のユーザーイベントをモデル化できます。

エルムでは、それは非常に簡単です。 共用体タイプを定義します。

 type NextPage = Prev | Next | ExactPage Int

そして、それをメッセージの1つのパラメーターとして使用します。

 type Msg = ... | ChangePage NextPage

最後に、関数を更新して、 nextPageのタイプをチェックするcaseを作成します。

 update msg model = case msg of ChangePage nextPage -> case nextPage of Prev -> ... Next -> ... ExactPage newPage -> ...

それは物事を非常にエレガントにします。

<|を使用して複数のマップ関数を作成する

多くのモジュールにはmap関数が含まれており、さまざまな数の引数に適用するためのいくつかのバリエーションがあります。 たとえば、 Listにはmapmap2 、…、 map5までがあります。 しかし、6つの引数を取る関数がある場合はどうなるでしょうか。 map6はありません。 しかし、それを克服するためのテクニックがあります。 <|を使用しますパラメータとしての関数、および部分関数。一部の引数は中間結果として適用されます。

簡単にするために、 Listmapmap2のみがあり、3つのリストに3つの引数を取る関数を適用するとします。

実装は次のようになります。

 map3 foo list1 list2 list3 = let partialResult = List.map2 foo list1 list2 in List.map2 (<|) partialResult list3

次のように定義された数値引数を乗算するfooを使用するとします。

 foo abc = a * b * c

したがって、 map3 foo [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5]の結果は[1,8,27,64,125] : List number

ここで何が起こっているのかを分解してみましょう。

まず、 partialResult = List.map2 foo list1 list2では、 foolist1list2のすべてのペアに部分的に適用されます。 結果は[foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5]であり、1つのパラメーター(最初の2つはすでに適用されているため)を取り、数値を返す関数のリストです。

次にList.map2 (<|) partialResult list3で、実際にはList.map2 (<|) [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3です。 これら2つのリストのすべてのペアに対して、 (<|)関数を呼び出しています。 たとえば、最初のペアの場合、これは(<|) (foo 1 1) 1であり、これはfoo 1 1 <| 1と同じです。 foo 1 1 <| 1は、 1を生成するfoo 1 1 1と同じです。 2番目の場合は、 (<|) (foo 2 2) 2になります。これは、 foo 2 2 2であり、 8と評価されます。

このメソッドは、 map8Json.Decodeまで提供するため、多くのフィールドを持つJSONオブジェクトをデコードするためのmapN関数で特に役立ちます。

多分リストからすべての値を削除する

Maybe値のリストがあり、それを持っている要素から値だけを抽出したいとします。 たとえば、リストは次のとおりです。

 list : List (Maybe Int) list = [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]

そして、 [1,3,6,7] : List Intを取得したいと思います。 解決策は、次の1行の式です。

 List.filterMap identity list

これが機能する理由を見てみましょう。

List.filterMapは、最初の引数が関数(a -> Maybe b)であると想定します。これは、指定されたリストの要素(2番目の引数)に適用され、結果のリストは、すべてのNothing値を除外するようにフィルター処理され、次に実際の値はMaybe sから抽出されます。

この場合、 identityを指定したので、結果のリストは再び[ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]なります。 フィルタリングすると、 [ Just 1, Just 3, Just 6, Just 7 ]になり、値を抽出すると、必要に応じて[1,3,6,7]になります。

カスタムJSONデコード

JSONデコード(または逆シリアル化)のニーズがJson.Decodeモジュールで公開されているものを超え始めると、新しいエキゾチックなデコーダーの作成で問題が発生する可能性があります。 これは、これらのデコーダーがデコードプロセスの途中から、たとえばHttpメソッド内で呼び出され、特に提供されたJSONに多くのフィールドがある場合、それらの入力と出力が常に明確であるとは限らないためです。

このような場合の処理​​方法を示す2つの例を次に示します。

最初のフィールドでは、着信JSONにabの2つのフィールドがあり、長方形の領域の辺を示しています。 ただし、Elmオブジェクトでは、その領域のみを保存します。

 import Json.Decode exposing (..) areaDecoder = map2 (*) (field "a" int) (field "b" int) result = decodeString areaDecoder """{ "a":7,"b":4 }""" -- Ok 28 : Result.Result String Int

フィールドはfield intデコーダーで個別にデコードされ、両方の値がmap2で提供される関数に提供されます。 乗算( * )も関数であり、2つのパラメーターを受け取るため、このように使用できます。 結果のareaDecoderは、適用されたときに関数の結果(この場合はa*b )を返すデコーダーです。

2番目の例では、nullまたは空を含む任意の文字列である可能性のある乱雑なステータスフィールドを取得していますが、操作が成功したのは「OK」の場合のみです。 その場合はTrueとして保存し、他のすべての場合はFalseとして保存します。 デコーダーは次のようになります。

 okDecoder = nullable string |> andThen (\ms -> case ms of Nothing -> succeed False Just s -> if s == "OK" then succeed True else succeed False )

それをいくつかのJSONに適用してみましょう:

 decodeString (field "status" okDecoder) """{ "a":7, "status":"OK" }""" -- Ok True decodeString (field "status" okDecoder) """{ "a":7, "status":"NOK" }""" -- Ok False decodeString (field "status" okDecoder) """{ "a":7, "status":null }""" -- Ok False

ここで重要なのは、 andThenに提供された関数です。これは、以前のnull許容文字列デコーダー( Maybe String )の結果を取得し、それを必要なものに変換し、successを使用して結果をsucceedとして返します。

重要なポイント

これらの例からわかるように、機能的な方法でのプログラミングは、JavaおよびJavaScript開発者にとってあまり直感的ではない場合があります。 慣れるまでには少し時間がかかり、試行錯誤もたくさんあります。 それを理解しやすくするために、 elm-replを使用して、式の戻り型を実行および確認できます。

この記事の前半にリンクされているサンプルプロジェクトには、カスタムデコーダーとエンコーダーの例がさらにいくつか含まれており、それらを理解するのにも役立ちます。

しかし、それでも、なぜエルムを選ぶのですか?

他のクライアントフレームワークとは大きく異なるため、Elm言語は確かに「まだ別のJavaScriptライブラリ」ではありません。 このように、それはそれらと比較したときにポジティブまたはネガティブと見なすことができる多くの特徴を持っています。

まずはプラス面から始めましょう。

HTMLとJavaScriptを使用しないクライアントプログラミング

最後に、あなたはそれをすべて行うことができる言語を持っています。 これ以上の分離、およびそれらの混合の厄介な組み合わせはありません。 JavaScriptでHTMLを生成したり、いくつかの簡略化されたロジックルールを使用したカスタムテンプレート言語を使用したりする必要はありません。

Elmを使用すると、構文と言語が1つだけになります。

均一

ほとんどすべての概念は関数といくつかの構造に基づいているため、構文は非常に簡潔です。 あるメソッドがインスタンスまたはクラスレベルで定義されているのか、それとも単なる関数であるのかを心配する必要はありません。 これらはすべて、モジュールレベルで定義された関数です。 そして、リストを反復するための100の異なる方法はありません。

ほとんどの言語では、コードがその言語の方法で記述されているかどうかについて、常にこの議論があります。 多くのイディオムを習得する必要があります。

Elmでは、コンパイルする場合、それはおそらく「Elm」の方法です。 そうでなければ、まあ、それは確かにそうではありません。

表現力

簡潔ですが、Elm構文は非常に表現力豊かです。

これは主に、共用体型、正式な型宣言、および関数型を使用することで実現されます。 これらはすべて、より小さな関数の使用を促します。 最後に、ほとんど自己文書化されたコードを取得します。

ヌルなし

JavaまたはJavaScriptを非常に長い間使用すると、 nullは完全に自然なものになります。これは、プログラミングの必然的な部分です。 また、 NullPointerExceptionやさまざまなTypeErrorが常に発生しますが、それでも本当の問題はnullの存在であるとは考えていません。 それはとても自然なことです。

エルムとしばらくするとすぐに明らかになります。 nullがないことで、実行時のnull参照エラーが何度も発生するのを防ぐだけでなく、実際の値がない可能性があるすべての状況を明確に定義して処理することで、より良いコードを記述できるようになります。したがって、 nullを延期しないことで、技術的負債を減らすことができます。何かが壊れるまで処理します。

それが機能するという自信

構文的に正しいJavaScriptプログラムの作成は非常に迅速に行うことができます。 しかし、それは実際に機能しますか? さて、ページをリロードして徹底的にテストした後、見てみましょう。

エルムの場合、それは反対です。 静的型チェックと強制nullチェックでは、特に初心者がプログラムを作成する場合、コンパイルに時間がかかります。 ただし、コンパイルすると、正しく機能する可能性が高くなります。

速い

これは、クライアントフレームワークを選択する際の重要な要素になる可能性があります。 大規模なWebアプリの応答性は、多くの場合、ユーザーエクスペリエンス、つまり製品全体の成功にとって非常に重要です。 そして、テストが示すように、Elmは非常に高速です。

Elmと従来のフレームワークの長所

従来のWebフレームワークのほとんどは、Webアプリを作成するための強力なツールを提供します。 しかし、その力には代償が伴います。複雑すぎるアーキテクチャでは、さまざまな概念とルールをいつどのように使用するかが決まります。 すべてをマスターするには多くの時間がかかります。 コントローラ、コンポーネント、およびディレクティブがあります。 次に、コンパイルフェーズと構成フェーズ、および実行フェーズがあります。 次に、提供されたディレクティブで使用されるサービス、ファクトリ、およびすべてのカスタムテンプレート言語があります。ページを更新するために$scope.$apply()を直接呼び出す必要があるすべての状況などです。

ElmのJavaScriptへのコンパイルも確かに非常に複雑ですが、開発者はそのすべての要点を知る必要から保護されています。 Elmをいくつか書いて、コンパイラにその作業を任せてください。

そして、エルムを選んでみませんか?

エルムを称賛するのに十分です。 それでは、それほど素晴らしい側面をいくつか見てみましょう。

ドキュメンテーション

これは本当に大きな問題です。 Elm言語には詳細なマニュアルがありません。

公式チュートリアルでは、言語をざっと見て、多くの未回答の質問を残します。

公式のAPIリファレンスはさらに悪いです。 多くの関数には、説明や例がありません。 そして、次のような文があります。「これが紛らわしい場合は、Elmアーキテクチャチュートリアルを実行してください。 それは本当に役に立ちます!」 公式のAPIドキュメントで見たい最高の行ではありません。

うまくいけば、これはすぐに変わるでしょう。

Elmは、このような貧弱なドキュメント、特にそのような概念や機能がまったく直感的ではないJavaやJavaScriptから来た人々に広く採用できるとは思いません。 それらを把握するには、多くの例を含むはるかに優れたドキュメントが必要です。

フォーマットと空白

中括弧や括弧を取り除き、インデントに空白を使用すると見栄えがよくなります。 たとえば、Pythonコードは非常にきれいに見えます。 しかし、 elm-formatの作成者にとっては、それだけでは不十分でした。

すべての二重行スペースがあり、式と割り当てが複数行に分割されているため、Elmコードは水平よりも垂直に見えます。 古き良きCのワンライナーは、Elm言語では複数の画面に簡単に拡張できます。

あなたが書かれたコードの行によって支払われるならば、これは良いように聞こえるかもしれません。 ただし、150行前に開始された式に何かを揃えたい場合は、適切なインデントを見つけてください。

レコード処理

彼らと一緒に働くのは難しいです。 レコードのフィールドを変更するための構文は見苦しいです。 ネストされたフィールドを変更したり、名前で任意のフィールドを参照したりする簡単な方法はありません。 また、アクセサ関数を一般的な方法で使用している場合、適切な入力には多くの問題があります。

JavaScriptでは、レコードまたはオブジェクトは、さまざまな方法で構築、アクセス、および変更できる中心的な構造です。 JSONでさえ、レコードのシリアル化されたバージョンにすぎません。 開発者はWebプログラミングでレコードを操作することに慣れているため、レコードをプライマリデータ構造として使用すると、Elmでのレコードの処理の難しさが顕著になる可能性があります。

もっと入力する

Elmでは、JavaScriptよりも多くのコードを記述する必要があります。

文字列および数値操作の暗黙的な型変換はないため、多くのint-float変換、特にtoString呼び出しが必要です。これには、正しい数の引数に一致するかっこまたは関数適用記号が必要です。 また、 Html.text関数には、引数として文字列が必要です。 MaybeResults 、タイプなど、多くのケース式が必要です。

これの主な理由は厳密な型システムであり、それは支払うべき公正な価格である可能性があります。

JSONデコーダーとエンコーダー

より多くのタイピングが本当に際立っている領域の1つは、JSON処理です。 JavaScriptでのJSON.parse()呼び出しとは、Elm言語では数百行にまたがることがあります。

もちろん、JSON構造とElm構造の間の何らかのマッピングが必要です。 ただし、同じJSONに対してデコーダーとエンコーダーの両方を作成する必要があることは深刻な問題です。 REST APIが数百のフィールドを持つオブジェクトを転送する場合、これは多くの作業になります。

要約

Elmについて見てきましたが、おそらくプログラミング言語自体と同じくらい古い、よく知られた質問に答える時が来ました。それは競合他社よりも優れているのでしょうか。 プロジェクトで使用する必要がありますか?

すべてのツールがハンマーであるとは限らず、すべてが釘であるとは限らないため、最初の質問に対する答えは主観的なものかもしれません。 Elmは輝いていて、多くの場合、他のWebクライアントフレームワークよりも優れた選択肢ですが、他のフレームワークでは不十分です。 しかし、それは、Webフロントエンドの開発を他の方法よりもはるかに安全で簡単にすることができるいくつかの本当にユニークな価値を提供します。

2番目の質問については、昔からの「状況によって異なります」との回答を避けるために、簡単な答えは次のとおりです。はい。 言及されたすべての不利な点があっても、Elmがあなたのプログラムが正しいことについてあなたに与える自信だけがそれを使うのに十分な理由です。

Elmでのコーディングも楽しいです。 これは、「従来の」Webプログラミングパラダイムに慣れている人にとってはまったく新しい視点です。

実際の使用では、アプリ全体をすぐにElmに切り替えたり、アプリ内で完全に新しいアプリを開始したりする必要はありません。 JavaScriptとの相互運用性を活用して、インターフェースの一部またはElm言語で記述された機能から始めて試してみることができます。 それがあなたのニーズに合っているかどうかをすぐに見つけて、それからその使用法を広げるか、それを残すでしょう。 そして、誰が知っているか、あなたはまた、機能的なウェブプログラミングの世界に恋をするかもしれません。

関連:フロントエンド開発のためのClojureScriptの発掘