Edited at
ElmDay 14

Elmの関数型的側面の多分ていねいな解説

ちゃおちゃお:wave_tone1:アッキーだよっ:sparkles:

最近弊社内でElmを使うことが多くなってきていて同僚の方と「Elmは学習コストゼロ:ok_hand_tone1:」などという妄言を言い合っていたんだけど、意外とそうでもない:interrobang:んじゃないかということになり、社内勉強会を開くことになりました:closed_book:ただ普通にチュートリアルやるんじゃ時間がかかるので、Elmアーキテクチャと関数型言語特有のハマりポイントに焦点を当てて説明していきたいと思います:wink:

なおこの記事はElm 0.19に基づいた内容となります。

参考:はじめに · An Introduction to Elm


Elmアーキテクチャ

フロントエンド開発では言語はJavaScriptやTypeScript、フレームワークはVue.jsやReactなどの選択肢があるかと思いますが、Elmは言語であり、フレームワークそのものでもあります。フレームワークとしての側面で見るときは特に「Elmアーキテクチャ」として以下の図のように語られることが多いです。

image.png




Model



アプリケーションの状態を保持する



Update



状態を更新する



View



ModelからHTMLを作る



基本的にはこの閉じた構造の中でぐるぐると値を回すイメージになります。ModelViewに突っ込まれ、ユーザが何かを行うとUpdateが呼ばれ更新されたModelViewに……といった具合です。図中のカッコの部分はこの記事では取り扱いません(HTTPリクエストやブラウザからのイベントなど、Elmの外側とやり取りする仕組みです)。ちょっとしたカウンターの例を見てみましょう。

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

type alias Model =
{ count : Int }

initialModel : Model
initialModel =
{ count = 0 }

type Msg
= Increment
| Decrement

update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }

Decrement ->
{ model | count = model.count - 1 }

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

main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}

詳しい構文はElmガイドに譲るとして、Elmの静的型付関数型の特徴とハマりそうなポイントを解説し終えたら、またElmアーキテクチャに戻ってきましょう。


Elmの関数型っぽさに慣れるために

Elmには次の3つの要素しか存在しません。


  • 型(と型変数)


  • 関数(とその引数)

オブジェクトやインターフェース、そんなものは存在しません。Elmを書く上でこの3つしか無いことを常に頭の中においておけば、理解がスムーズに行くことでしょう。また、小文字で始まるものは「関数、引数または型変数」、大文字で始まるものは「型、値または関数」と覚えておくといいかもしれません。また、値や関数について悩んだらそれらの型に注目する癖をつけましょう。合言葉は迷ったら型です。


型とは値と関数の種類を決めるものです。値と関数には必ず型があります。まずはElmで最初から使える、値がひとつの型を紹介しましょう。



  • Int 整数(-1,42など)


  • Float 浮動小数点(1.233.001.602e−19など)


  • Bool ブール(TrueFalseのみ)


  • String 文字列("foo""bar"""など)


  • Char 文字('a''\n'

ソースコードでの表現も合わせて記してあります。ここでPythonやRuby、JavaScriptなどに慣れている方に注意してほしいことがふたつあります。


  1. 文字列と文字は別の型であること

  2. 文字列は文字の配列(リスト)ではないこと

Elmでは型に厳しいため関数に値を渡すときは型が一致している必要があります。そして型があっているかどうかはコンパイラが自動でチェックして未然に知らせてくれます。このため関数では決まった型が渡されることが保証されているので安心してコードを書くことができます。


タプル

タプルとは型を組み合わせた型です。例えば(1, 2)という値は(Int, Int)という型になります(正確にはちょっと違ったりするのですが後述します)。タプルは違う型も組み合わせることができるので(1, 1.2, "nyaan")という(Int, Float, String)型の値を作ることができます。(1, 2) == (2, 4)という比較はできますが("hoge", 1) == (2, "fuga")といった比較は型が違うのでできません。0個のタプルも作れます。()と書けば()型という不思議な型になります1


リスト

リスト一種類の型の値を0個以上集めた型です。他の言語でいう配列やリストのようですがsomeList[1]のような数字でリストの中身を参照することはできません(そのようなことができる型にArrayがあります)。[1, 2, 3]と書くとList Int型になり、["bar"]ならばList Stringという型になります。JavaScriptのように[1, "baz"]のような値は作れません2


関数の型

関数とは値を渡して新たな値を返してくれるものです。関数を作るときはfunc x = x + 1のように書きます。かっこは必要ありません。funcという関数に値42を渡すにはfunc 42と書きます。そうすると42 + 1になるので43が返ってきます3xInt型でfunc xInt型になります。実はfuncそのものにもInt -> Intという型があります。これが関数の型です。関数の型はコンパイラが勝手に推論してくれます。なのでfunc "hoge"としようとするとコンパイラがエラーを吐くでしょう4。型についてはコンパイラがよしなにやってくれますが、具体的に型を書いてあげると人間が読みやすい形になります。これを型注釈(型アノテーション)と呼び、以下のように関数を作るのと一緒に書いておきます。

func : Int -> Int -- これが型注釈

func x = x + 1

2個の値を渡したい場合は次のようになります5

funcA : Int -> Int -> Int

funcA x y = x + y

funcA 1 2とすれば3が返ってきます。さてこの関数ですが、funcA 1のように引数を1つだけ渡すとどうなるのでしょうか。エラーになる?実はちゃんとコンパイルできます!実はfuncAの型はInt -> ( Int -> Int )という順番で解釈されます。Int型をもらってInt -> Int型が返ってくる、つまり関数が返ってくるのです。これを部分適用と呼びます。以下のように書いたときfuncB z = 3 + zと書いてあるのと同様の働きをします。

funcB : Int -> Int

funcB z = funcA 3 z

3個の引数をもらう関数はInt -> ( Int -> ( Int -> Int) ) )のような型になります。よく見るとa -> bのような形が入れ子になっているのがわかるでしょうか。つまり関数はすべて引数ひとつの関数とみなせるわけです。


関数に関数を渡す?

Int -> Int -> Int型はInt -> ( Int -> Int )と解釈されると言いましたが、ちょっといじって( Int -> Int ) -> Intという型を作ったらどうなるでしょう。Int -> Int型をもらってInt型を返す……なんと関数を渡すことができました!

funcC : ( Int -> Int ) -> Int

funcC f = f 2

funcD : Int -> Int
funcD x = x + 3

このように関数を作りfuncC funcDとすると2 + 3が実行され5が返ってきます。関数を渡す、というのはなんとも不思議ですがJavaScriptのArrayに対するmapfilter思い浮かべてもらうとイメージがつかみやすいと思います。


カスタム型

Elmでは自分で型を作ることができます。これをカスタム型6と呼びます。例えばBool型はTrueFalse2個の値を持つ型です。これを自分でつくろうとすれば以下のようになります。

type Bool = True | False

値の名前がモジュールの中(とElmで最初から用意してあるもの)がぶつからなければ自由に型と値を決めることができます。

type Hoge = Hoge | Foo | Bar | Baz -- 型と値は名前がかぶっても大丈夫

値を作るとき、別の型と組み合わせて決めることもできます。

type User

= Guest String
| Member Int String
| Admin

User型の値を作るにはGuest "John Cleese"Member 3 "Eric Idle"Adminのように書きます。Guest"John Cleese"というString型をわたしてあげるとUser型が返ってくる……なにか似たようなものがありましたね?そうです、関数です!実は上記のUser型を作ると以下のような型を持つ関数を作ってくれるのです(正しい文法ではないのでご注意!)。

Guest : String -> User

Member : Int -> String -> User
Admin : User

そして関数は関数に渡すことができるので……

makeTerryAs : ( String -> User ) -> User

makeTerryAs f = f "Terry Jones"

と言う関数が作れます!makeTerryAs GuestとすればGuest "Terry Jones"と言う値ができますし。makeTerryAs (Member 5)とすればMember 5 "Terry Jones"と言う値ができます。この使い方は(この記事では使いませんが)ElmアーキテクチャのViewからUpdateへのつながりで使うので覚えておきましょう。


型エイリアスとレコード

型には別名(エイリアス)をつけることができます。

type alias UserId = Int

type alias UserName = String

getUserName : UserId -> UserName -- Int -> String と同じ
getUserName i = ...

こう書いておけばドキュメント代わりにもなりますが、あまりやりすぎるともともとの型がわからなくなってしまいがちですので間違えやすそうなもののみエイリアスをつけるなどにしましょう(上記の例であればgetUserName : UserId -> Stringとか)。

さて、ユーザのデータを管理したいと思い次のようなデータを作ったとしましょう。

type User = User Int String String

型はわかりますが、一体何を意味しているのかわかりませんね……。ここで型エイリアスを使い

type User = User UserId UserName UserFavoriteCheese

とするのも手ですが、今度はエイリアスが増えまくって余計に分けが分からなくなります。ではどうするのかというとレコードという、キーと値を組み合わせたものを使います。レコード型の値を作るには

{ id = 1, name = "Michael Palin", favoriteCheese = "Stillton" }

と書き、この値の型は

{ id : Int, name : String, favoriteCheese : String }

となります。キーは小文字から始めなければいけないので注意してください。これを使って関数を書いてみましょう。

doesLikeBoursin : { id : Int, name : String, favoriteCheese : String } -> Bool

doesLikeBoursin user = user.favoriteCheese == "Boursin"

user.favoriteCheeseとピリオドで区切ることにより型に書いてあるキーの値が取り出せます。うっかりキーの名前を間違えてもコンパイルが通らないので安心です。しかしこのままではレコードの型が覚えにくいので型エイリアスをつけておくことにしましょう。

type alias User =

{ id : Int
, name : String
, favoriteCheese : String
}

doesLikeBoursin : User -> Bool
doesLikeBoursin user = user.favoriteCheese == "Boursin"

スッキリしましたね!しかもエイリアスをつけると次のようなおまけがついてきます(正しい文法ではないのでご注意!)。

User : Int -> String -> String -> User

User i name cheese = { id = i, name = n, favoriteCheese = cheese }

つまりUser型はレコード型ではありますがUser 10 "Graham Chapman" "Cheddar"と言う書き方で値を作ることができます。これは便利ですね!これをレコードコンストラクタと呼びます。

もしもらったレコードの一部だけが違うレコードが欲しい場合次のようにします。

eatGouda : User -> User

eatGouda user = { user | favoriteCheese = "Gouda" }

この関数にUser型の値を渡すとfavoriteCheeseだけが"Gouda"に変わったものが返ってきます。


型変数

ちょっと話をリストに戻しましょう。[1][1, 2, 3]というリストの値はList Int型を持ちます。["hoge", "fuga"]であればList Stringになります。それならばさっき作ったUser型を並べてList Userも作れないとおかしいですね。どうやらList aaにはどんな型を入れても良さそうです。このときa型変数と呼びます。bでもmsgでも先頭が小文字なら何でも良いのですが、大体1文字の名前をつけるのが習わしです。

リストの紹介をするとき実は空のリスト[]の型については触れていませんでした。だって中身が無いので型の付けようがありません。ですが型変数さえ手に入れれば[]の型はList aと書けることになります。カスタム型を使って無理やりリストを表現すれば以下のようになるでしょう(例によって正しい文法ではないのでご注意を!)

type List a = [] | [a] | [a, a] | [a, a, a] | ...

このときaIntでもFloatでも、自分で作ったMyTypeでもどんな型が入っても良いというわけです。

ここで注意!以下の名前は型変数に使うとちょっと特殊な働きをします。


  • number

  • appendable

  • comparable

  • compappend

例えばnumberを型変数に使うとどんな型を突っ込むことはできなくなり、IntFloatしか入れられません。1Int型といっていましたが実はウソでした。正確にはIntFloatかどっちかわからん!のでnumber型となります。1.0と書けばFloatになります。


演算子、そんなものはない

そういえば型と値と関数しか無いと言っておきながらしれっとx + yとか書いてました。+っていわゆる演算子ってやつじゃないの?と思われるかもしれませんが、この+も実は関数です。ただし値と値の真ん中に置くことができるという特殊な扱いを受けています。特殊扱いをやめるには(+)とカッコでくくってあげましょう。(+) 1 2と書くことで1 + 2と書いたのと同様になります。関数ならばもちろん型があります。(+)の型は

(+) : number -> number -> number

となります。多少特殊な扱いは受けていますが、演算子すら関数です。


ifとパターンマッチ

もちろんElmにもifはあります。ただ条件分岐ではないので一旦他の言語のifは忘れましょう。ifを使った関数は次のように書けます。

bigNumber : Int -> String

bigNumber n = if n > 1000 then "Big!!!" else "Small..."

if foo then bar else bazと書き、foo必ずBool型、barbazは同じ型である必要があります。else省略できませんifBool型の値を取り、何らかの値を必ず返します(なのでn > 1000の型はBoolです)。Bool型はTrueFalseの2種類の値がありますから、それぞれに対応する値を決めなければいけません。ちょっと煩わしいかもしれませんが、返ってくる値の型が同じであることが保証されているのでやはり安心できます。

Bool型以外にもifを使いたい!と言う場合はパターンマッチが使えます。fromIntStringに変換する関数で、(++)String同士を連結する関数です。

specialNumber :: Int -> String

specialNumber x =
case x of
4 ->
"My lucky number!"
42 ->
"Answer to the Ultimate Question of Life, the Universe, and Everything"
y ->
(fromInt y) ++ " is not special"

なんとなくx442の時それに対応したStringが返ってくるんだろうな、というのは感じ取れると思います。小文字から始まる名前にした場合、その名前が値を表します。この場合例えばx10なら4でも42でもありませんからy10に置き換えられ、"10 is not special"と返ってきます。なおパターンマッチは上から順番にあっているか確かめられるのでyの順番を変えると期待通りに動かなくなります。また、パターンマッチも必ず値を返さなければいけないので対応するパターンがないとコンパイルが通りません。この場合だとyの部分を消してしまうとエラーになります。もしyの値を使わない場合、以下のように_(アンダーバー)を使って明示的に書くことができます。

    _ ->

"Not special"

パターンマッチはカスタム型と一緒に用いることでさらなる威力を発揮します!次のコードを見てください。

type User 

= Guest String
| Member Int String
| Admin

greeting : User -> String
greeting user =
case user of
Guest name ->
"Welcome, " ++ name ++ "!!"
Member 42 _ ->
"Answer to the Ult*snip*"
Member _ name ->
"Hello! " ++ name ++ "."
Admin ->
"How is your highness?"

値を複数持つ型の場合一部にのみパターンマッチを行うことが可能ということを示しています。


Elmアーキテクチャ再び

ずいぶんと長くなってしまいましたが、ようやくElmアーキテクチャの再登場です!冒頭で紹介したソースコードからView、Model、Updateに当たる部分を抜き出し、見ていくことにしましょう。


View

view : Model -> Html Msg

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

type Msg
= Increment
| Decrement

viewModelをもらってHtml Msgという型の値を返します。なんとなくHTMLのような構造が伺えるでしょうか。例えば<div>のブロックを作りたい場合div : List (Attribute msg) -> List (Html msg) -> Html msgと言う関数を使います。属性と子ノードをわたしてノードを返すと読み取れます。msgは型変数なので何でも良いのですが、この例ではMsgという型の値が渡されることがviewの方から読み取れます。onClick関数にMsg型の値を渡し、それをリストにしてbuttonにわたしてやるとボタンをクリックしたときにElmのUpdateにMsg型の値が渡されることになります。逆に言うとViewの中でなにかイベントが起こらない限りずっとViewで止まったままになります。


Model

type alias Model =

{ count : Int }

ModelもModelという単なる型です。気に入らなければModel以外の名前にしてももちろんOKです!ここではInt型のcountだけを持つレコードとしていますが、より複雑にしてもいいですし、より単純にしても構いません。このModelの値を先程のview関数に渡すと画面を表示し、ユーザからのイベントを待つ状態になります。


Update

update : Msg -> Model -> Model

update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }

Decrement ->
{ model | count = model.count - 1 }

Updateも単なる関数です!なにかMsgModelが渡された時、その2つの値から新しいModel型の値を作り出しているだけなんです。


締め

さて、ずいぶんと長くなりElmアーキテクチャの部分は駆け足気味になった気がしますがいかがだったでしょうか。最後に大事な部分だけおさらいしておきます。


  • image.png


  • 関数しか無い

  • 迷ったら型

以上、アッキーでした:sparkles:明日のAdvent Calendarはmikesoraeさんです!





  1. 1個のタプルはElmだと作れません……と断言できる資料見つかりませんでしたが、多分作れないです 



  2. これのせいでJSON扱うとき大体キレてる 



  3. func x = x + 13 * 4を渡そうとしてfunc 3 * 4と書くと((func 3) * 4)と解釈され16が返ってくるので気をつけましょう 



  4. どこかのJavaScriptのように"hoge1"とかにはなりません 



  5. タプルで渡すこともできますが、Elmではあまりやらないのでここでは触れません 



  6. 以前のバージョンではユニオン型と呼ばれていました