Elmで体験する関数型言語の面白さ


ElmはフロントエンドのWebApplicationのフレームワークを内蔵したDSL言語です。シンプルながら高機能なElm言語は言語の分類として純粋関数型言語という枠組みに含まれます。そのシンプル故に快適というElmでの開発の特徴は純粋関数型言語の特性でもあるように思います。なので、他の言語をやっている人に純粋関数型言語の魅力を伝えるべくこの記事を書きました。では早速Elmの世界をのぞいてみましょう。

定義と型と値と関数

Elmでは、以下のようにして関数や値の定義をします。

-- valを123として定義
val = 123

-- strをString型の"hoge"という文字列として定義
str : String
str = "hoge"

-- numberを引数にとってnumberにする関数の定義
twice : number -> number
twice n = n * 2

-- 足し算する関数の定義
add : number -> number -> number
add x y = x + y

-- 掛け算する関数の定義
mul : number -> number -> number
mul x y = x * y

Elmでの値は名前 : 型 名前 = 値のようにセットで宣言します。型は省略可能で、上記のvalはコンパイラが勝手に右辺から推論してnumber型であると推論してくれます。でもなるべく書いて置く癖を付けておきましょう。意図しない型になった時にコンパイラがやさしく叱ってくれます。

関数は名前 引数1 引数2 ... = 戻り値 という形式で書きます。なんと、初っ端から戻り値を書きます。returnないの?と思ったみなさん、理由はもうすぐわかります。そして関数呼び出しはtwice 3add 1 2のように、カッコを付けずに行います。また優先的に計算したいところはカッコで括ります。

(1+2)*(3+4)をする場合はmul (add 1 2) (add 3 4)というように書きます。また、add 1 2 * 3というような場合、関数適用の方が優先されるため、結果は9になります。逆にadd 1 * 2 3としようとすると、(add 1 *) 2 3のように解釈されてしまい、「* がnumberじゃないよ」と言われてしまうので計算できません。

気づいた方もいらっしゃるかと思いますが、addの型をみるとnumber -> number -> numberとちょっと不思議な形になってます。他の言語とかだと、ここの型は(number, number) -> numberのように、二つの引数をとって、1つの戻り値を返すのがスタンダードです。これは一体なんなのでしょうか?

カリー化と部分適用

実はこのadd関数、引数が足りなくても呼べてしまいます。なんとadd 5のような呼び出しができてしまうのです。Elmにはreplがあるのでこれを実際に試してみましょう。

replの結果
> add x y = x + y
<function> : number -> number -> number    add の値と型

> add 5
<function> : number -> number              add 5 の値と型

呼び出しが成功し、関数が返ってきました。この戻り値でfを定義して、もう一回呼び出してみましょう。

replの結果
> f = add 5
<function> : number -> number              f の値と型

> f 2
7 : number                                 f 2 の値の型

つまり、add関数はnumber -> (number -> number)、すなわち numberを引数に取り、number -> numberを返す関数なのです。

add 1 2というのは

add 1 2          -- add 1を計算して <function> 2に
<function> 2     -- <function> 2を計算して 3に
3

という順番で計算されるのです。 Elmでの関数は、必ず1つづつ引数を取るようにできているのです。そうした関数をカリー化された関数と呼びます。

なんでも関数。あれもこれも。

Elmは関数型言語です。ほとんどのものが関数として実装されています。比較演算子の==>などの中置演算子は全て関数として実装されてます。中置演算子は(==)のようにカッコでくくると普通の関数のように扱えます。

replの結果
> (==)
<function> : a -> a -> Bool

> (==) 3
<function> : number -> Bool

> (==) 3 4
False : Bool

> (==) 3 3
True : Bool

なんでも値。あれもこれも。

関数もれっきとした値です。値なので引数に取ることもできるし戻り値として使うこともできます。例えばListモジュールのfilterは関数を引数に取り関数を返します。

replの結果
> List.filter
<function> : (a -> Bool) -> List a -> List a

> even n = modBy 2 n == 0
<function> : Int -> Bool

> List.filter even
<function> : List Int -> List Int

> List.filter even [1,2,3,4,5]
[2,4] : List Int

filterに出てくるaという型の中に現れる小文字で表されているものは型引数と呼び、任意の型を表します。他言語で言う所のジェネリックやテンプレートに当たります。それらと同じように、型に出てくる全てのaには同一の型が入ります。List.filterevenを渡した時に、さっきまでaだった部分がIntになるのはaIntという具体的な型が定まり、他の箇所のaが全てIntになったのです。ちなみに型引数は1文字である必要はないです。(のちに出てくるmsgなどが型引数であることに気づくのにだいぶ時間がかかりました...)

「コンパイラのためにこんなめんどくさい型を書かなきゃいけないのかよと」思いましたか?この型情報はコンパイラのために書くという理解でも良いのですが、実は人間にとっても読みやすい、むしろコンパイラも人間も読める、共通の仕様としての役割を果たしてくれるのです。

List.filter関数は人間に寄せて読むと、フィルターの条件 : a -> Boolを渡すとリストをフィルタリングする処理 : List a -> List aを返すと言ってるのです。その処理になんかのリスト : List aを渡せば、フィルター結果: List aを返しますよと表現していることになります。なんだかロマンを感じませんか?僕だけですかね。次に行きましょう。

パイプ演算子、合成演算子

僕が便利度ナンバーワンの演算子だと思っているこの演算子のおかげで思考に則した綺麗な実装をすることができます。パイプ演算子|>, <|はunixのパイプと似た挙動をします。以下のように、最初に値を書いて、その次に関数を書き、その計算結果が|>で次に流れていくイメージです。

List.range 0 10
    |> List.map ((+) 5)
    |> List.map ((*) 2)
    |> List.filter (\n -> modBy 3 n == 0)
    |> List.sum

この見た目、rubyなどのメソッドチェインに見えませんか?もちろんメソッドチェインも強力なのですが、Elmは|>自体がa -> (a -> b) -> bといった型を持つ関数なので、より強力です。関数であれば何でも繋げて書くことができ、Elmのほとんどの機能が関数として実装されているため、だいたいは全部このパイプラインでつなぐことができます。

そしてさらに強力な関数合成演算子>>, <<は先ほどの例で以下のように使うことができます。

List.range 0 10
    |> List.map ((+) 5 >> (*) 2)
    |> List.filter (\n -> modBy 3 n == 0)
    |> List.sum

>>の型は(a -> b) -> (b -> c) -> a -> cとなってます。つまり
a -> bb -> cbの部分を繋げてa -> cの関数を作る演算です。この>>もどんどん繋げていくことで、いろんな矢印を一つの矢印として繋げるって考えるとパズル的な側面もあり、書いてて面白いです。でも濫用には気をつけてくださいね。

代数的データ型

また難しそうなワードが出てきました。ですがこれがかなーり強力な機能であり、Elmにとってめちゃくちゃ大事な要素になります。これは自分で既存のものではない新たな型(値)を作る時に使います。

例えば、3択の選択肢があり、それが正解かどうか判定する関数があった時

right : Int -> Bool

のようにしてしまうと、負の整数や4以上の数字を許容してしまいます。もっというと1~3なのか0~2なのかさえわかりません。え?ドキュメントを書けばいいじゃないかですって?もちろんそうしたいのであればそうするのも結構です。

ですがElmではそのような事で悩む必要はありません。なぜなら代数的データ型があるからです。

type Choice
    = First
    | Second
    | Third

たったこれだけでChoice型のデータFirst, Second, Thirdの三つの値ができました。あとは選択肢を引数にとって何か計算する処理を書きたいならInt -> aではなくChoice -> aとすればいいのです。この記法の良いところは、Choiceという型があり、3択であることを強制し、コンパイラも理解できるというところです。コンパイラは人間より真面目で賢いので、ケアレスミスはコンパイラに指摘してもらうのは良いことです。

直和型と直積型

-- 直和型の例。実際にElmのcoreに含まれている。
type Result error value
    = Ok value
    | Err error


-- 直積型の例。Tupleを模した実装
type MyTuple a b
    = MyTuple a b

この直和型や直積型に埋まった値を使うためにはcase文や引数に取るタイミングでパターンマッチをする必要があります。

showTaskType : Result error value -> String
showTaskType res =
    case res of
        Err _ ->
            "Errorだった"
        Ok val ->
            "Okだった(" ++ toString val ++ ")"


-- 直和じゃないものは引数を受け取る地点でパターンマッチできる

fst : MyTulpe a b -> a
fst (MyTuple a _) = a

snd : MyTuple a b -> b
snd (MyTuple _ b) = b

紹介だけにとどめますが、直和、直積、さらに再帰まで表してくれている木構造は学ぶのにとても良い対象です。

type BinaryTree a
    = Leaf a
    = Node (BinaryTree a) val (BinaryTree a)

-- 多分木なども
type Tree a
    = Leaf a
    | Node a (List (Tree a))

The Elm Architecture

ここまでずっと言語の特徴、純粋関数型の特徴をつらつら書き連ねていきました。が、忘れてはいけないのはElmはWebApplicationのための言語というところです。ここまでの純粋関数型のスタイルをわかると、アプリケーションを実際書くところは全然苦にならないです。先ほど伝えたように、Elmの世界ではほとんどがシンプルな値と関数のみでできてます。もちろんアプリケーションを書いていく過程でも変わりません。

ElmにはThe Elm Architectureという概念があります。最低限アプリケーションを構成するものとして、

  • モデルがあって
  • モデルを元に、メッセージを発行するビューが計算できて
  • メッセージが発火したらモデルを更新する

のようなフローでアプリケーションを書きます。Reduxの元になった考え方だそうです。

ここでElmの標準のBrowser.sandbox関数の型をみてみましょう。

sandbox関数の型
sandbox :
    { init : model
    , view : model -> Html msg
    , update : msg -> model -> model
    }
    -> Program () model msg

もう先ほどの概念のまんまですね。実際にアプリケーションを作る上で、はinitviewupdateの三つの値を定義し、これをsandbox関数に{ init = init , view = view , update = update }という値を渡せばアプリケーションの完成です。Elmのランタイムはsandboxの戻り値を評価することでアプリケーションを走らせます。

まとめ

まだまだ伝えきれてない色々なノウハウがありますがとりあえず公開のためにひとまずここまでにします。

Elm楽しいのでぜひやってみてください。この言語で学べることの多くはElmとかElmのapiを学ぶと言うよりは、考え方を学ぶという方がしっくりきます。Elmを書く上で培った経験はそのまま他言語で活躍するでしょう。それでは良きプログラミングライフを!

「もっと知りたい」方々へ