Edited at

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

fukuoka.ex/kokura.exのpiacereです

ご覧いただいて、ありがとうございます :bow:

前回に引き続き、「プログラミングHaskell 第2版」の第2章を、Elixirで解釈しつつ、HaskellやElixirのTIPSを追記してみようと思います(本コラムの目次は、書籍の目次を流用しています)

今回は、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でも動作する想定です)


第2章:はじめの一歩


2.1 GHC

GHCには、コンパイラと対話インタプリタ(REPL)の「GHCi」が含まれています

Elixirも同様に、コンパイラと対話REPLの「iex」が含まれています


2.2 インストールして利用開始

GHC同様、Elixirも総合情報サイトである、https://elixir-lang.org/ から無料で入手できます

image.png

各プラットフォーム毎の詳細は、こちらの入門コラムをご参考ください

インストール後、GHCは、ghciコマンドでREPLが起動できます

> ghci

GHCi, version 8.8.1: https://www.haskell.org/ghc/ :? for help
Prelude> 2 + 3 * 4
14
Prelude> ( 2 + 3 ) * 4
20
Prelude> sqrt ( 3 ^ 2 + 4 ^ 2 )
5.0

Elixirは、iexコマンドでREPLが起動できます

ちなみにElixirには、演算子で、べき乗が無いので、:math.powで代用しています

> iex

iex(1)> 2 + 3 * 4
14
iex(2)> ( 2 + 3 ) * 4
20
iex(3)> :math.sqrt( :math.pow( 3, 2 ) + :math.pow( 4, 2 ) )
5.0

なお、演算子の結合順位は、HaskellとElixirで違いはありません


2.3 プレリュード

Haskellのリスト操作は、「プレリュード」と呼ばれる組み込み関数で提供されます


Haskell

Prelude> head [ 1, 2, 3, 4, 5 ]

1
Prelude> tail [ 1, 2, 3, 4, 5 ]
[2,3,4,5]
Prelude> [ 1, 2, 3, 4, 5 ] !! 2
3
Prelude> take 3 [ 1, 2, 3, 4, 5 ]
[1,2,3]
Prelude> drop 3 [ 1, 2, 3, 4, 5 ]
[4,5]
Prelude> length [ 1, 2, 3, 4, 5 ]
5
Prelude> sum [ 1, 2, 3, 4, 5 ]
15
Prelude> product [ 1, 2, 3, 4, 5 ]
120
Prelude> [ 1, 2, 3 ] ++ [ 4, 5 ]
[1,2,3,4,5]
Prelude> reverse [ 1, 2, 3, 4, 5 ]
[5,4,3,2,1]

Elixirでも、組み込み関数か、ビルトインモジュールのEnumモジュールで実現できます(productのみ、Enumで用意していないため、reduceで積み上げ乗算しています)

Elixirも、Haskell同様、引数を囲むカッコは、省略可能です


Elixir

iex> hd [ 1, 2, 3, 4, 5 ]

1
iex> tl [ 1, 2, 3, 4, 5 ]
[2, 3, 4, 5]
iex> Enum.at [ 1, 2, 3, 4, 5 ], 2
3
iex> Enum.take [ 1, 2, 3, 4, 5 ], 3
[1, 2, 3]
iex> Enum.drop [ 1, 2, 3, 4, 5 ], 3
[4, 5]
iex> length [ 1, 2, 3, 4, 5 ]
5
iex> Enum.sum [ 1, 2, 3, 4, 5 ]
15
iex> Enum.reduce [ 1, 2, 3, 4, 5 ], &( &1 * &2 )
120
iex> [ 1, 2, 3 ] ++ [ 4, 5 ]
[1, 2, 3, 4, 5]
iex> Enum.reverse [ 1, 2, 3, 4, 5 ]
[5, 4, 3, 2, 1]

Haskellが、第一引数がリストとは限らない一方、Elixirは、全ての関数で、第一引数は、リストを取ります

なお、モジュール表記の無かったhd/tl/lengthは、ビルトインモジュールのKernelモジュールに定義されており、上記の通り、Kernelは省略可能ですが、実際は、以下のようなコードが実行されています


Elixir

iex> Kernel.hd [ 1, 2, 3, 4, 5 ]

1
iex> Kernel.tl [ 1, 2, 3, 4, 5 ]
[ 2, 3, 4, 5 ]
iex> Kernel.length [ 1, 2, 3, 4, 5 ]
5

「++」によるリスト連結だけで無く、「--」によるリスト削除も可能です(各要素毎にマッチさせるため、順序が異なっていても削除できますし、重複する値は、1つずつマッチして削除します)


Elixir

iex> [ 1, 2, 3 ] -- [ 2 ]

[1, 3]
iex> [ 1, 2, 3 ] -- [ 2, 3 ]
[1]
iex> [ 1, 2, 3 ] -- [ 3, 2 ]
[1]
iex> [ 1, 2, 3 ] -- [ 3, 2, 1 ]
[]
iex> [ 1, 2, 2, 3 ] -- [ 2 ]
[1, 2, 3]
iex> [ 1, 2, 2, 3 ] -- [ 2, 2 ]
[1, 3]


付録B「B.8 リスト」

書籍では、使用頻度の高い定義を付録Bに収録(リスト操作以外も含む)していますが、全部の対比を書くと大変なので、「B.8 リスト」に限定して、HaskellとElixirのコード対比を下記にまとめます(なお、上記紹介済の重複は省略します)


Haskell

Prelude> last [ 1, 2, 3, 4, 5 ]

5
Prelude> filter even [ 1, 2, 3, 4, 5 ]
[2,4]
Prelude> takeWhile ( <= 3 ) [ 1, 2, 3, 4, 5 ]
[1,2,3]
Prelude> init [ 1, 2, 3, 4, 5 ]
[1,2,3,4]
Prelude> dropWhile ( <= 3 ) [ 1, 2, 3, 4, 5 ]
[4,5]
Prelude> splitAt 3 [ 1, 2, 3, 4, 5 ]
([1,2,3],[4,5])
Prelude> repeat [ 1, 2 ]
[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],Interrupted.
Prelude> replicate 3 [ 1, 2 ]
[[1,2],[1,2],[1,2]]
Prelude> iterate ( + 1 ) 1
[1,2,3,4,5,6,7,8,9,10,11,Interrupted.
Prelude> zip [ 1, 2, 3 ] [ 4, 5, 6 ]
[(1,4),(2,5),(3,6)]
Prelude> map ( + 1 ) [ 1, 2, 3, 4, 5 ]
[2,3,4,5,6]

上記Haskellと同等のElixirコードは、以下です

Elixirで無限リストを扱うには、Streamモジュールを使います

repeat/replicate/iterateは、全て、Stream.iterate(とEnum.take)でカバーできます


Elixir

iex> List.last [ 1, 2, 3, 4, 5 ]

5
iex> import Integer
iex> Enum.filter [ 1, 2, 3, 4, 5 ], &is_even( &1 )
[2, 4]
iex> Enum.take_while [ 1, 2, 3, 4, 5 ], & &1 <= 3
[1, 2, 3]
iex> Enum.drop [ 1, 2, 3, 4, 5 ], -1
[1, 2, 3, 4]
iex> Enum.drop_while [ 1, 2, 3, 4, 5 ], & &1 <= 3
[4, 5]
iex> Enum.split [ 1, 2, 3, 4, 5 ], 3
{[1, 2, 3], [4, 5]}
iex> Stream.iterate( [ 1, 2 ], & &1 ) |> Enum.take( 11 )
[[1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2]]
iex> Stream.iterate( [ 1, 2 ], & &1 ) |> Enum.take( 3 )
[[1, 2], [1, 2], [1, 2]]
iex> Stream.iterate( 1, & &1 + 1 ) |> Enum.take( 11 )
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
iex> Enum.zip [ 1, 2, 3 ], [ 4, 5, 6 ]
[{1, 4}, {2, 5}, {3, 6}]
iex> Enum.map [ 1, 2, 3, 4, 5 ], &( &1 + 1 )
[2, 3, 4, 5, 6]

Enum.take_whileでできることは、Enum.filterで再現可能です


Elixir

iex> Enum.take_while [ 1, 2, 3, 4, 5 ], & &1 <= 3

[1, 2, 3]
iex> Enum.filter [ 1, 2, 3, 4, 5 ], & &1 <= 3
[1, 2, 3]

Enum.dropで末尾を削除(-1で指定)の代わりに、List.delete_atを使うことも可能です


Elixir

iex> Enum.drop [ 1, 2, 3, 4, 5 ], -1

[1, 2, 3, 4]
iex> List.delete_at [ 1, 2, 3, 4, 5 ], -1
[1, 2, 3, 4]

Elixirの無限リストは、Haskellのrepeatやiterateのように、その場で実体化はせず、関数のままでいます

実体化するには、Enum.takeやEnum.to_list等を使います


Elixir

iex> Stream.iterate( 1, & &1 + 1 )

#Function<65.35756501/2 in Stream.unfold/2>
iex> Stream.iterate( 1, & &1 + 1 ) |> Enum.take( 10 )
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iex> Stream.iterate( 1, & &1 + 1 ) |> Enum.to_list
(この場合、無限に続くため、表示がされない)

ミリ秒単位で0オリジンの生成をしたいシチュエーションでは、Stream.intervalを使います(タイマー処理を作るときに便利です)


Elixir

iex> Stream.interval( 1000 ) |> Enum.take( 10 )

(1秒 x 10個分、待つ)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

この他にも、Stream.transformを使うと、条件判定付きの高度な無限リスト処理ができますが、これは「Elixir無限ストリームでズンドコキヨシ」というコラムで紹介しています


2.4 関数適用

数学の下記表記は、前半が関数適用、後半が乗算を表します

f(a, b) + c d

Haskellでは、関数適用に空白文字を使い、乗算は演算子*を使うため、こう書きます


Haskell

Prelude> a = 1

Prelude> b = 2
Prelude> c = 3
Prelude> d = 4
Prelude> f a b = a + b
Prelude> f a b + c*d
15

Elixirでも、これはほぼ同様ですが、引数の間にカンマが必要なのが差になります


Elixir

iex> a = 1

iex> b = 2
iex> c = 3
iex> d = 4
iex> defmodule Sample, do: def f a, b, do: a + b
iex> import Sample
iex> f a, b + c*d
15

なお、Elixirの無名関数で書くと、関数名の後の「.」とカッコが省略できないので、以下のような複雑な記載になります


Elixir

iex> f = fn a, b -> a + b end

#Function<12.99386804/2 in :erl_eval.expr/5>
iex> f.(a, b) + c*d
15

数学とHaskell、Elixirの記載の違いは、以下のようになります

Haskellは、引数の間と、関数適用を、どちらも空白文字で区別しないため、fの前にgを関数適用する場合、「f (g x)」と記載しなければならない一方、Elixirは、引数の間は「,」が必須であり、空白を混同することは無い仕様のため、「f g x」と記載可能です

書籍では、上記説明となっていますが、実際は、カッコとカンマの組み合わせで、数学式に近い定義が、HaskellもElixirも可能です

数学
Haskell書籍
Elixir書籍相当
Haskell数学類似
Elixir数学類似

f(x)
f x
f x
f(x)
f(x)

f(x, y)
f x y
f x, y
f(x, y)
f(x, y)

f(g(x))
f (g x)
f g x
f(g(x))
f(g(x))

f(x, g(y))
f x (g y)
f x, g y
f(x, (g(y))
f(x, (g(y))

f(x)g(y)
f x * g y
f x * g y
f(x) * g(y)
f(x) * g(y)

ちなみにHaskellでは、カッコ有とカッコ無の関数は、別物として扱われるので、注意してください


Haskell

Prelude> f a b = a + b

Prelude> f 2 3
5
Prelude> f( 2, 3 )
<interactive>:3:1: error:
? Non type-variable argument in the constraint: Num (a, b)
(Use FlexibleContexts to permit this)
? When checking the inferred type
it :: forall a b. (Num a, Num b, Num (a, b)) => (a, b) -> (a, b)


Haskell

Prelude> f( 2, 3 ) = a + b

Prelude> f 2 3
<interactive>:3:1: error:
? Non type-variable argument in the constraint: Num (t1 -> t2)
(Use FlexibleContexts to permit this)
? When checking the inferred type
it :: forall t1 t2.
(Num t1, Num (t1 -> t2), Num (t1 -> t2, t1 -> t2)) =>
t2
Prelude> f( 2, 3 )
5

また、「カッコはあるけど、カンマは無い」という表記は、エラーになります


Haskell

Prelude> f( a b ) = a / b

parse error (possibly incorrect indentation or mismatched brackets)


2.5 Haskellプログラム

ファイルによる関数定義と、GHCiからの呼出、GHCiコマンド、命名規則、レイアウト規則、タブ文字、コメントについての記載ありますが、9章や10章でファイルを使ったサンプルがあるため、ここでは割愛します


オマケ:Elixirのパイプ演算子

Elixirは、パイプ演算子「|>」を使うことで、第一引数を前の結果から渡すことができるため、今回のサンプルは、以下のように書き換えできます

「データから始め、データにどんな操作を施すか」が、分かりやすく記載できます


Elixir

iex> [ 1, 2, 3, 4, 5 ] |> hd

1
iex> [ 1, 2, 3, 4, 5 ] |> tl
[2, 3, 4, 5]
iex> [ 1, 2, 3, 4, 5 ] |> Enum.at 2
3
iex> [ 1, 2, 3, 4, 5 ] |> Enum.take 3
[1, 2, 3]
iex> [ 1, 2, 3, 4, 5 ] |> Enum.drop 3
[4, 5]
iex> [ 1, 2, 3, 4, 5 ] |> length
5
iex> [ 1, 2, 3, 4, 5 ] |> Enum.sum
15
iex> [ 1, 2, 3, 4, 5 ] |> Enum.reduce &( &1 * &2 )
120
iex> [ 1, 2, 3, 4, 5 ] |> Enum.reverse
[5, 4, 3, 2, 1]


Elixir

iex> [ 1, 2, 3, 4, 5 ] |> List.last

5
iex> import Integer
iex> [ 1, 2, 3, 4, 5 ] |> Enum.filter &is_even( &1 )
[2, 4]
iex> [ 1, 2, 3, 4, 5 ] |> Enum.take_while & &1 <= 3
[1, 2, 3]
iex> [ 1, 2, 3, 4, 5 ] |> Enum.drop -1
[1, 2, 3, 4]
iex> [ 1, 2, 3, 4, 5 ] |> Enum.drop_while & &1 <= 3
[4, 5]
iex> [ 1, 2, 3, 4, 5 ] |> Enum.split 3
{[1, 2, 3], [4, 5]}
iex> [ 1, 2, 3, 4, 5 ] |> Enum.map &( &1 + 1 )
[2, 3, 4, 5, 6]


終わり

HaskellとElixirのリスト操作は、無限リストも含め、ほぼ同じことが実現できました(第一引数が必ずリストを取るElixirと、そうでないHaskellの違いはありますが)

また、関数適用については、引数の間に「,」が必須なElixirと、そうで無く空白文字で代用できるHaskellでは、その後のカッコの扱いが変わってくることも分かりました

次回は、書籍の第3章「型と型クラス」を扱います


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

ページ左上のimage.pngimage.png のクリックを、どうぞよろしくお願いします:bow:

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