LoginSignup
45
27

More than 1 year has passed since last update.

Elixir版?①「プログラミングHaskell第2版」

Last updated at Posted at 2019-09-23

fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます :bow:

先日9/7(土)に開催した、「ElixirConf JP 2019」の準備・運営にかかりきりで、約3ヶ月弱ぶりの投稿です(ElixirConf JP 2019のレビューは追ってまとめます)

さて最近、Haskellerの方々と、ご一緒することが、ちょくちょくあります

ご存知の方は、ご存知ですが、元々、私の関数型初体験は、Haskellでした … それから幾つものプログラミング言語を経由して、現在、Elixirに落ち着いており、恐らく、Elixir以外の言語を選ぶことは、よほどのことが無い限り、無いと思います

とはいえ、言語ヲタとして、他言語に学ぶことは多くあり、今回は、先月出たばかりの「プログラミングHaskell 第2版」の第1章を、Elixirで解釈してみようと思います(コラムは、原著の目次と合わせているので、お持ちで無い方はお買い上げください、良い本です)
image.png

余談ですが、私は、「Haskellによる並列・並行プログラミング」という本も好きで、Elixirでのマルチコアプログラミングは、こういった部分からも刺激を受けています
image.png

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:


:shamrock::shamrock::shamrock::shamrock::shamrock: お知らせ:11/10(日)、ElixitとHaskellの登壇します :shamrock::shamrock::shamrock::shamrock::shamrock:

「関数型プログラミングカンファレンス2019 in Japan」で、本コラムシリーズ+αの内容で、35分の登壇をします

Haskellの神様、Edward Kmettさんや、GHC開発者のSimon Peyton Jones(SPJ)さん、Rustの有名人Pyry Kontioさんや原 将己さん、ElmのSeiya Izumiさんと、なんだか夢のような共演です

https://fpc2019japan-event.peatix.com/
image.png

なお、この前日11/9(土)に開催される「HaskellDay 2019」も参加です(コチラは登壇ではありません)

https://techplay.jp/event/727059
image.png

本コラムの検証環境、事前構築のコマンド

本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)

第1賞:導入

1.1 関数

本のP3、「Haskellにおける関数は、1つ以上の引数を取って1つの結果を返す、変換器です」と、しょっぱなから、「プログラミングElixir」と全く同じ趣旨のプログラミングに対する主張です
image.png

double関数の例は、簡単過ぎるので割愛

1.2 関数プログラミング

Haskellで、「[..]」で数値リストを生成し、「sum」でリストの総和を出すサンプルが、本のP4~6に記載されています

これを、GHCのREPLである「GHCi」を使って実行してみます

Haskell
Prelude> sum [1..5]
15

このように、Haskellのリスト操作系関数は、「プレリュード」と呼ばれる組込関数で定義されています

Elixirでも、.. で値の範囲を示すもの(リストでは無くレンジと言います)が生成でき、それを「Enum.to_list」でリストに変換し、「Enum.sum」でリストの総和を出せます

ElixirのREPLである「iex」を使って実行してみます

Elixir
iex> Enum.sum Enum.to_list 1..5
15

Haskellのリスト操作系関数に相当するものが、Elixirでは、Enumモジュールで実装されています

なおEnum.sumは、リストだけで無く、レンジも引数に取れるので、以下のように書けます

Elixir
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と変えています)

Haskell
Prelude> sum2 [] = 0; sum2 (n:ns) = n + sum2 ns

リストが無くなるまで下の関数が繰り返され、リストが空になると、上の関数が呼ばれ、再帰が終わります

なお、GHCiで複数行の関数定義をすると、エラーになります(ファイルに複数行で定義し、GHCi起動時に指定すれば、エラーにはなりません)

Haskell
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内で、複数行の関数定義したい場合は、:{ で始め、:} で終えてください

Haskell
Prelude> :{
Prelude| sum2 [] = 0
Prelude| sum2 (n:ns) = n + sum2 ns
Prelude| :}
Prelude> sum2 [1, 2, 3]
6

Elixirでも、同等の再帰関数が作れますが、無名関数定義内で、自身を呼び出すことができないため、モジュール内で定義する「名前付き関数」を使い、関数呼出時はモジュール名指定します(以降はコチラで記述します)

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と同等の呼出記述にできます(以降の説明では省略します)

Elixir
iex> import Sample
iex> sum2 [1, 2, 3]
6

このように、モジュールを挟んではいますが、Haskellと似たような書き方ができることがお分かりかと思います

その後、本では型定義について書かれていますが、これは、sumにおけるaは、Num型aのリストを、Num型aに変換する関数であることを示しています

Haskell
sum2 :: Num a => [a] -> a

Elixirでも、ErlangのDyalizerという静的型チェックツール(ElixirだとDyalixirで使用可能)に食わせられる型チェック構文があり、number型のリストをnumber型に変換する関数であることを示しています

Elixir
@spec sum2(list(number)) :: integer

なお、「::」の構文/意味が、Haskellのソレとはだいぶ異なるので、ご注意ください

ちなみに、この型定義を明確にしていなくても、Elixirは、numberに対してのみ有効な + 演算子が効かない型が来た場合、ランタイムエラーで検出します

Elixir
iex> Sample.sum2 [1, 2, 3]
6
iex> Sample.sum2 [1, "a", 3]
** (ArithmeticError) bad argument in arithmetic expression

余談:Elixirの無名関数で定義すると、どうなるか?

Elixirの無名関数定義内では、自身を呼び出すことができないため、おのずと高階関数で書くことになります

Elixir
iex> sum2 = fn
...>   [], _f -> 0
...>   [n|ns], f -> n + f.(ns, f)
...> end
iex> sum2.([3, 2, 1], sum2)

下記のように、無名関数内で関数を定義し、高階関数の呼出をラップする書き方もできますが、ちょっと複雑です

Elixir
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 数値を整列する

再帰によるクイックソートが、本に書かれています

Haskell
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句で記載することになります

Elixir
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]

比較的、似たような構文で記述できることがお分かりでしょう

無名関数で、スマートに書き直すこともできます

Elixir
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 によるモジュール内関数で書き直すこともできます

Elixir
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で書くこともできます

Elixir
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も同様です

Elixir
iex> qsort [3, 2, 1]
[1, 2, 3]
iex> qsort 'cba'
'abc'

なお、Elixirでは、' で括った文字列は、charlistとして順序を持ったリストとして扱われますが、" で括った文字列は、リストとして扱われないため、上記qsortが適用できません(が、実用上は、数値と文字列を同一視する処理はあまり書かず、むしろ混同しなくて良い仕様と個人的に捉えています)

Elixir
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 アクションを逐次に実行する

関数のリストを受け取って、順次実行し、その戻りをリストで返す関数が、本に記載されています

Haskell
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文字と数えてしまいますが…)

Haskell
Prelude> seqn [getChar, getChar, getChar]
a
b
c
"abc"

これをElixirで表現すると、以下のようになります

なお、getCharに相当する引数無入力が無いため、IO.getnを想定した書き方になっています(IO.getnも、WindowsだとEnterをやはり1文字と数えてしまうため、ここでは、2文字を受け取る呼び出し方で対処しています)

Elixir
iex> defmodule Sample do
...>   def seqn(acts), do: for act <- acts, do: act.("", 2)
...> end

関数に、入力された2文字を返す&IO.getn/2を3つ渡すと、3回、入力が促され、2文字x3つを連結したリストが返されます

Elixir
iex> seqn [&IO.getn/2, &IO.getn/2, &IO.getn/2]
a
b
c
["a\n", "b\n", "c\n"]

上記Haskellと同じような記載にしたいなら、getChar相当を下記のように定義します

Elixir
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とほぼ同じ記載/挙動が行われます

Elixir:Elixir
iex> seqn [&getchar/0, &getchar/0, &getchar/0]
a
b
c
'abc'

本では、この続きとして、この関数の型と汎用性について解説がありますが、Elixirは、最初から動的型付言語なので汎用的です

また、この関数は、入出力といったアクションに特化している訳では無く、もっと広い作用を扱え、本にも例があるような、「変数の値を変えること」「計算が失敗すること」「ログファイルを書き出すこと」等に利用できる点も、Haskellと同様です(IOやモナドの解説については、後述する章の中で扱うとしましょう)

このような感じで、Haskellの妙味は、Elixirでも味わうことができます

終わり

ここまでは、とても簡単な例ではありますが、思った以上に、HaskellとElixirの類似性が高いことがお分かりかと思います

これからElixirを学ぶ上で、Haskellを学ぶべきかどうかは、順序が逆の私からは、よく分かりませんが、同じようなマインドが根底にあることを知ったり、どういった部分で相違点があるのかを把握することで、Elixirの各機能が、なぜ提供されているかについて、より深い理解をすることはできると考えます

次回は、書籍の第2章を扱います

p.s.「いいね」よろしくお願いします

ページ左上のimage.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

45
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
27