Haskell 的視点から見た Elm

  • 59
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

前書き

Egison のドキュメントをいろいろ眺めていたところ The 6 Programming Languages Interesting to Try を読んで Elm という言語を知り、これは面白いと思い Egison そっちのけで Elm をダウンロードしていろいろ遊んでみました。そのうちにいろいろと思うところがあったので Elm Advent Calendar に乗じて徒然なるままに書いてみることにしました。

もし Elm のことは全然知らないというならば、まずは Examples で遊んでみてください。Web 上で Elm を実行して遊べます。

純粋関数型の世界から JavaScript の世界へ

Elm は Haskell に非常に良く似た文法を持っています。Haskell と同様、副作用を暗黙に生じさせない「純粋関数型」のアプローチを取っていて、コンパイラは厳密な静的型付けのもとで強力な型推論と型検査を行っています。

しかし、Elm をコンパイルして得られるのは実行ファイルでなく JavaScript(を埋め込んだ html)です。JavaScript は緩い型付けですし、副作用を普通に使います。一体どういうことをしているのでしょうか。

Randomize のコードを使ってみます。元の Elm コードはこんな感じです。

import Random

main : Signal Element
main =
    lift asText (Random.range 0 100 (every second))

これをコンパイルした結果の JavaScript は次のようになります。

Elm.Main.make = function (_elm) {
   "use strict";
   _elm.Main = _elm.Main || {};
   if (_elm.Main.values)
   return _elm.Main.values;
   var _op = {},
   _N = Elm.Native,
   _U = _N.Utils.make(_elm),
   _L = _N.List.make(_elm),
   _A = _N.Array.make(_elm),
   _E = _N.Error.make(_elm),
   $moduleName = "Main",
   $Graphics$Element = Elm.Graphics.Element.make(_elm),
   $Random = Elm.Random.make(_elm),
   $Signal = Elm.Signal.make(_elm),
   $Text = Elm.Text.make(_elm),
   $Time = Elm.Time.make(_elm);
   var main = A2($Signal.lift,
   $Text.asText,
   A3($Random.range,
   0,
   100,
   $Time.every($Time.second)));
   _elm.Main.values = {_op: _op
                      ,main: main};
   return _elm.Main.values;
};

最初におまじないの部分がありますが、var main = 以降を見ればほぼ逐語的に翻訳していると分かると思います。

実は、Elm は遅延評価も最外簡約もグラフ簡約もしていません。見た目には「純粋関数型」であっても、実際には副作用を構文の上で禁止しているに過ぎず、特別な純粋関数型の恩恵を受けている訳ではないのです。僕としては純粋関数型を固持する必要は無く、副作用を許してもいいだろうと思っています。

とにかく、強力な型システムや洗練された構文はやはり便利ですし、現状でも Elm を使う意義は十分にあると思います。

JavaScript の世界から(ほぼ)純粋関数型の世界へ

Elm の core のライブラリを覗いてみると、実は内部的に Native というフォルダにおいて JavaScript を利用していることが分かります。

Elm には Haskell のような型クラスは用意されていません。その代わり、asText< のような型によって処理が変わるような関数はネイティヴの JavaScript で記述しているのです。

この機能を使えば、Elm の世界にも副作用を持ち込むことさえ出来てしまいます。

Elm の core ライブラリは、Signal を使って副作用を上手く隠す形で、外部の環境との種々の操作を提供していますが、ちょっと頑張ると副作用の影を見せることができます

import Time

go1 : Signal Float
go1 = fst <~ timestamp (every millisecond)

go2 : Signal Float
go2 = fst <~ timestamp (every millisecond)

main = asText <~ ((-) <~ go1 ~ go2)

fst <~ timestamp (every millisecond) は1ミリ秒ごとに現在の時間(タイムスタンプ)を更新するシグナルです。
go1go2 は同じ値を取ることが期待されますが、実行してみると二つの値には微妙な差があることが分かるはずです。
生成される JavaScript を見るとさらによく分かるはずです。

ですから、厳密な意味では Elm は純粋関数型言語ではないのです

Signal とは何か、IO とどう違うか

Elm では main の型は Signal Element (あるいは Element) です。

Signal は内部的には巧妙な JavaScript で扱っていますが、ある程度の理想化をすれば次のように考えられます。

type Time = BigInteger
type Updated = Bool
type Signal a = Time -> (Updated, a)

実際には Elm は遅延評価でないのでこのようにして Elm で扱うことは出来ません。
Haskell 上で同様のことを実装すればどうなるかという観点で考えています。

data Stream a = Cons a (Stream a)
type Updated = Bool
type Signal a = Stream (Updated, a)

と考えることもできます。

このモデルを元にすれば Signal にある関数はすべて簡単に実装することが出来ます。

なお、実際の Elm では JavaScript を使い、更新が起こった時だけ処理を行うことにより、より効率的な FRP を実現しています。

Elm が副作用を表面上無くすことに成功しているのは、時間によって変化する入力の情報、そして出力情報を、時間から情報への関数と見なすことで一定のもののように扱える、という仕組みを使っているからです。

一方、Haskell では副作用に IO を使っています。main の型は IO TT は任意の型であり、() でも構わない)です。IO は少し理想化すれば次のように考えられます。

data RealWorld = ......
newtype IO a = IO (RealWorld -> (RealWorld, a))

外界の状態 (RealWorld) を元にして、新たな外界の状態 (RealWorld) と値 (a) をもたらす関数が IO a なのです。IO は「状態が RealWorld である状態モナド」とも見なせます。

Haskell と Elm における副作用の概念の違いをまとめれば、

  • Haskell では入出力をまとめて一つの外界として扱い、外界の状態を読み取り、かつ変化させることができる
  • Elm では入力と出力は分かれていて、ライブラリによって与えられる入力を元に main という出力を返す

と考えることができます。

もっとまとめれば、Elm に IO は無いのです

最後に

Elm は便利で面白い言語です。

今の時点で Elm を実用する意義は大いにあります。

しかし、Elm は Web の世界に Haskell 的なアプローチを導入した第一歩に過ぎません。

Elm の今後の発展に期待しながらも、Elm に刺激されて新たな素晴らしい言語が生まれることを、あるいは自分で作り出すことを、夢見ています。

この投稿は Elm Advent Calendar 20145日目の記事です。