fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
先日9/7(土)に開催した、「ElixirConf JP 2019」の準備・運営にかかりきりで、約3ヶ月弱ぶりの投稿です(ElixirConf JP 2019のレビューは追ってまとめます)
さて最近、Haskellerの方々と、ご一緒することが、ちょくちょくあります
ご存知の方は、ご存知ですが、元々、私の関数型初体験は、Haskellでした … それから幾つものプログラミング言語を経由して、現在、Elixirに落ち着いており、恐らく、Elixir以外の言語を選ぶことは、よほどのことが無い限り、無いと思います
とはいえ、言語ヲタとして、他言語に学ぶことは多くあり、今回は、先月出たばかりの「プログラミングHaskell 第2版」の第1章を、Elixirで解釈してみようと思います(コラムは、原著の目次と合わせているので、お持ちで無い方はお買い上げください、良い本です)
余談ですが、私は、「Haskellによる並列・並行プログラミング」という本も好きで、Elixirでのマルチコアプログラミングは、こういった部分からも刺激を受けています
内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします
お知らせ:11/10(日)、ElixitとHaskellの登壇します
「関数型プログラミングカンファレンス2019 in Japan」で、本コラムシリーズ+αの内容で、35分の登壇をします
Haskellの神様、Edward Kmettさんや、GHC開発者のSimon Peyton Jones(SPJ)さん、Rustの有名人Pyry Kontioさんや原 将己さん、ElmのSeiya Izumiさんと、なんだか夢のような共演です
https://fpc2019japan-event.peatix.com/
なお、この前日11/9(土)に開催される「HaskellDay 2019」も参加です(コチラは登壇ではありません)
https://techplay.jp/event/727059
本コラムの検証環境、事前構築のコマンド
本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.8.1 ※最新版のインストール手順はコチラ
- GHC 8.8.1
第1賞:導入
1.1 関数
本のP3、「Haskellにおける関数は、1つ以上の引数を取って1つの結果を返す、変換器です」と、しょっぱなから、「プログラミングElixir」と全く同じ趣旨のプログラミングに対する主張です
double関数の例は、簡単過ぎるので割愛
1.2 関数プログラミング
Haskellで、「[..]」で数値リストを生成し、「sum」でリストの総和を出すサンプルが、本のP4~6に記載されています
これを、GHCのREPLである「GHCi」を使って実行してみます
Prelude> sum [1..5]
15
このように、Haskellのリスト操作系関数は、「プレリュード」と呼ばれる組込関数で定義されています
Elixirでも、..
で値の範囲を示すもの(リストでは無くレンジと言います)が生成でき、それを「Enum.to_list」でリストに変換し、「Enum.sum」でリストの総和を出せます
ElixirのREPLである「iex」を使って実行してみます
iex> Enum.sum Enum.to_list 1..5
15
Haskellのリスト操作系関数に相当するものが、Elixirでは、Enumモジュールで実装されています
なおEnum.sumは、リストだけで無く、レンジも引数に取れるので、以下のように書けます
iex> Enum.sum 1..5
15
これは、Elixirにおける「多相性」で、defprotocol/defimplによって実現しています
1.3 Haskellの特徴
本に書かれている特徴を、Elixirで照らし合わせると、以下の通りです
- 簡潔なプログラム(第2章と第4章) … Elixirでも同様の効果があり、他言語よりも、2~10倍短く書けます
- 強力な型システム(第3章と第8章) … Elixirは動的型付のため、Haskellほど型システムが厳密では無いですが、コンパイル時型チェックが走り、他の動的型付言語とは一線を画し、更にDyalyzerによる静的チェックもできれば、多相性や多重定義もdefprotocol/defimplでナチュラルに実現できます
- リスト内包表記(第5章) … 同様のメカニズムがElixirにもあります
- 再帰関数(第6章) … 再帰およびパターンマッチとガードは、Elixirでも利用可能です(が、私の周りのElixir勢およびHaskell勢は再帰を推奨していません、詳細は別途)
- 高階関数(第7章) … Elixirでも、高階関数と、それを利用したDSL構築はもちろんできます
- 作用を持つ関数(第10章と第12章) … Haskellは、関数の純粋性を犠牲にしないメカニズムが多くあり、Elixirでも類似した実装は可能ですが、よりシンプルなメカニズムも提供されています
- 汎用的な関数(第12章と第14章) … 関手、アプリカティブ、モナド、Foldable、Traversableは、Elixirでも実現できます(が、それが有用かは議論の余地ありで、上述の「よりシンプルなメカニズム」で代行するケースが大半です)
- 遅延評価(第15章) … Elixirでは、データストリームに限定された、Streamモジュールによる遅延評価があります
- 等式推論(第16章と第17章) … ここはElixirがカバーできていない領域です(が私達の研究ではカバー範囲になります)
1.4 歴史的背景
本のP8~9には、Haskellの前身となった1930年代のラムダ計算や1950年代のLisp誕生、1987年のHaskell誕生といった歴史がリストされていますが、Elixirは、創始者であるJosé Valimが、Haskellをはじめ、Clojure、OCaml等の様々な関数型言語を研究し、Erlang/OTPに出会った結果、作られた言語なので、ある意味、Haskellの歴史の先に出た派生であるといっても過言ではありません
ですので、Haskellの歴史の出来事の中から、Elixirで採用されなかった対象を見ていくと、もう一段、Elixirのことを楽しむことができます
1.5 Haskellの妙味
1.5.1 数値を足し合わせる
再帰によるリスト総和が、本に書かれています(なお、sumだとプレリュードのsumと齟齬りそうなので、sum2と変えています)
Prelude> sum2 [] = 0; sum2 (n:ns) = n + sum2 ns
リストが無くなるまで下の関数が繰り返され、リストが空になると、上の関数が呼ばれ、再帰が終わります
なお、GHCiで複数行の関数定義をすると、エラーになります(ファイルに複数行で定義し、GHCi起動時に指定すれば、エラーにはなりません)
Prelude> sum2 [] = 0
Prelude> sum2 (n:ns) = n + sum2 ns
Prelude> sum2 [1, 2, 3]
*** Exception: <interactive>:41:1-25: Non-exhaustive patterns in function sum4
GHCi内で、複数行の関数定義したい場合は、:{
で始め、:}
で終えてください
Prelude> :{
Prelude| sum2 [] = 0
Prelude| sum2 (n:ns) = n + sum2 ns
Prelude| :}
Prelude> sum2 [1, 2, 3]
6
Elixirでも、同等の再帰関数が作れますが、無名関数定義内で、自身を呼び出すことができないため、モジュール内で定義する「名前付き関数」を使い、関数呼出時はモジュール名指定します(以降はコチラで記述します)
iex> defmodule Sample do
...> def sum2([]), do: 0
...> def sum2([n | ns]), do: n + sum2 ns
...> end
iex> Sample.sum2 [1, 2, 3]
6
importを使うと、モジュール名指定が省略でき、Haskellと同等の呼出記述にできます(以降の説明では省略します)
iex> import Sample
iex> sum2 [1, 2, 3]
6
このように、モジュールを挟んではいますが、Haskellと似たような書き方ができることがお分かりかと思います
その後、本では型定義について書かれていますが、これは、sumにおけるaは、Num型aのリストを、Num型aに変換する関数であることを示しています
sum2 :: Num a => [a] -> a
Elixirでも、ErlangのDyalizerという静的型チェックツール(ElixirだとDyalixirで使用可能)に食わせられる型チェック構文があり、number型のリストをnumber型に変換する関数であることを示しています
@spec sum2(list(number)) :: integer
なお、「::」の構文/意味が、Haskellのソレとはだいぶ異なるので、ご注意ください
ちなみに、この型定義を明確にしていなくても、Elixirは、numberに対してのみ有効な +
演算子が効かない型が来た場合、ランタイムエラーで検出します
iex> Sample.sum2 [1, 2, 3]
6
iex> Sample.sum2 [1, "a", 3]
** (ArithmeticError) bad argument in arithmetic expression
余談:Elixirの無名関数で定義すると、どうなるか?
Elixirの無名関数定義内では、自身を呼び出すことができないため、おのずと高階関数で書くことになります
iex> sum2 = fn
...> [], _f -> 0
...> [n|ns], f -> n + f.(ns, f)
...> end
iex> sum2.([3, 2, 1], sum2)
下記のように、無名関数内で関数を定義し、高階関数の呼出をラップする書き方もできますが、ちょっと複雑です
iex> sum2 = fn
...> n -> func = fn
...> [], _f -> 0
...> [n|ns], f -> n + f.(ns, f)
...> end
...> func.(n, func)
...> end
iex> sum2.([3, 2, 1])
6
1.5.2 数値を整列する
再帰によるクイックソートが、本に書かれています
Prelude> :{
Prelude| qsort [] = []
Prelude| qsort ( x:xs ) = qsort smaller ++ [x] ++ qsort larger
Prelude| where
Prelude| smaller = [a | a <- xs, a <= x]
Prelude| larger = [b | b <- xs, b > x]
Prelude| :}
Prelude> qsort [5, 1, 3]
[1,3,5]
リストが無くなるまで下の関数の大小比較と連結が繰り返され、リストが空になると、上の関数が呼ばれ、再帰が終わります
Elixirでも、同等の再帰関数が作れますが、Haskellのようなwhere句は無いため、smallerやlargerを、for句で記載することになります
iex> defmodule Sample do
...> def qsort([]), do: []
...> def qsort([x|xs]), do: qsort(for a <- xs, a <= x, do: a) ++ [x] ++ qsort(for b <- xs, b > x, do: b)
...> end
iex> qsort [5, 1, 3]
[1, 3, 5]
比較的、似たような構文で記述できることがお分かりでしょう
無名関数で、スマートに書き直すこともできます
iex> defmodule Sample do
...> def qsort([]), do: []
...> def qsort([x|xs]) do
...> smaller = fn x, xs -> for a <- xs, a <= x, do: a end
...> larger = fn x, xs -> for b <- xs, b > x, do: b end
...> qsort(smaller.(x, xs)) ++ [x] ++ qsort( larger.(x, xs))
...> end
...> end
iex> qsort [5, 1, 3]
[1, 3, 5]
もしくは、下記のように、defp
によるモジュール内関数で書き直すこともできます
iex> defmodule Sample do
...> def qsort([]), do: []
...> def qsort([x|xs]), do: qsort(smaller(x, xs)) ++ [x] ++ qsort(larger(x, xs))
...> defp smaller(x, xs), do: for a <- xs, a <= x, do: a
...> defp larger( x, xs), do: for b <- xs, b > x, do: b
...> end
iex> qsort [5, 1, 3]
[1, 3, 5]
for句では無く、Enumで書くこともできます
iex> defmodule Sample do
...> def qsort([]), do: []
...> def qsort([x|xs]), do: qsort(smaller(x, xs)) ++ [x] ++ qsort(larger(x, xs))
...> defp smaller(x, xs), do: Enum.filter xs, & &1 <= x
...> defp larger( x, xs), do: Enum.filter xs, & &1 > x
...> end
本ではこの後、Haskellが、「数字リストだけで無く、文字列のような、順序を持つ型であれば、何であれ適用可能」という記述がありますが、これはElixirも同様です
iex> qsort [3, 2, 1]
[1, 2, 3]
iex> qsort 'cba'
'abc'
なお、Elixirでは、'
で括った文字列は、charlistとして順序を持ったリストとして扱われますが、"
で括った文字列は、リストとして扱われないため、上記qsortが適用できません(が、実用上は、数値と文字列を同一視する処理はあまり書かず、むしろ混同しなくて良い仕様と個人的に捉えています)
iex> is_list('abc')
true
iex> is_list("abc")
false
iex> qsort "cba"
** (FunctionClauseError) no function clause matching in Sample.qsort/1
1.5.3 アクションを逐次に実行する
関数のリストを受け取って、順次実行し、その戻りをリストで返す関数が、本に記載されています
Prelude> :{
Prelude| seqn [] = return []
Prelude| seqn (act:acts) = do x <- act
Prelude| xs <- seqn acts
Prelude| return (x:xs)
Prelude> :}
関数に、入力された1文字を返すgetCharを3つ渡すと、3回、入力が促され、3文字を連結したものが返されます(Windowsだと挙動がこうならず、文字入力後に打鍵するEnterも1文字と数えてしまいますが…)
Prelude> seqn [getChar, getChar, getChar]
a
b
c
"abc"
これをElixirで表現すると、以下のようになります
なお、getCharに相当する引数無入力が無いため、IO.getnを想定した書き方になっています(IO.getnも、WindowsだとEnterをやはり1文字と数えてしまうため、ここでは、2文字を受け取る呼び出し方で対処しています)
iex> defmodule Sample do
...> def seqn(acts), do: for act <- acts, do: act.("", 2)
...> end
関数に、入力された2文字を返す&IO.getn/2を3つ渡すと、3回、入力が促され、2文字x3つを連結したリストが返されます
iex> seqn [&IO.getn/2, &IO.getn/2, &IO.getn/2]
a
b
c
["a\n", "b\n", "c\n"]
上記Haskellと同じような記載にしたいなら、getChar相当を下記のように定義します
iex> defmodule Sample do
...> def seqn(acts), do: for act <- acts, do: act.()
...>
...> def getchar(), do: IO.getn("", 2) |> String.first |> String.to_charlist |> List.first
...> end
そうすると、Haskellとほぼ同じ記載/挙動が行われます
iex> seqn [&getchar/0, &getchar/0, &getchar/0]
a
b
c
'abc'
本では、この続きとして、この関数の型と汎用性について解説がありますが、Elixirは、最初から動的型付言語なので汎用的です
また、この関数は、入出力といったアクションに特化している訳では無く、もっと広い作用を扱え、本にも例があるような、「変数の値を変えること」「計算が失敗すること」「ログファイルを書き出すこと」等に利用できる点も、Haskellと同様です(IOやモナドの解説については、後述する章の中で扱うとしましょう)
このような感じで、Haskellの妙味は、Elixirでも味わうことができます
終わり
ここまでは、とても簡単な例ではありますが、思った以上に、HaskellとElixirの類似性が高いことがお分かりかと思います
これからElixirを学ぶ上で、Haskellを学ぶべきかどうかは、順序が逆の私からは、よく分かりませんが、同じようなマインドが根底にあることを知ったり、どういった部分で相違点があるのかを把握することで、Elixirの各機能が、なぜ提供されているかについて、より深い理解をすることはできると考えます
p.s.「いいね」よろしくお願いします
ページ左上のや のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!