fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
前回に引き続き、「プログラミングHaskell 第2版」の第2章を、Elixirで解釈しつつ、HaskellやElixirのTIPSを追記してみようと思います(本コラムの目次は、書籍の目次を流用しています)
今回は、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.9.1 ※最新版のインストール手順はコチラ
- GHC 8.8.1
第2章:はじめの一歩
2.1 GHC
GHCには、コンパイラと対話インタプリタ(REPL)の「GHCi」が含まれています
Elixirも同様に、コンパイラと対話REPLの「iex」が含まれています
2.2 インストールして利用開始
GHC同様、Elixirも総合情報サイトである、https://elixir-lang.org/ から無料で入手できます
各プラットフォーム毎の詳細は、こちらの入門コラムをご参考ください
インストール後、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のリスト操作は、「プレリュード」と呼ばれる組み込み関数で提供されます
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同様、引数を囲むカッコは、省略可能です
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は、全ての関数で、第一引数は、リストを取るのが最大の違いで、これはElixirのパイプにも影響します
なお、モジュール表記の無かったhd/tl/lengthは、ビルトインモジュールのKernelモジュールに定義されており、上記の通り、Kernelは省略可能ですが、実際は、以下のようなコードが実行されています
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つずつマッチして削除します)
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のコード対比を下記にまとめます(なお、上記紹介済の重複は省略します)
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)でカバーできます
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で再現可能です
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を使うことも可能です
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等を使います
iex> Stream.iterate(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を使います(タイマー処理を作るときに便利です)
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では、関数適用に空白文字を使い、乗算は演算子*を使うため、こう書きます
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でも、これはほぼ同様ですが、引数の間にカンマが必要なのが差になります
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の無名関数で書くと、関数名の後の「.」とカッコが省略できないので、以下のような複雑な記載になります
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では、カッコ有とカッコ無の関数は、別物として扱われるので、注意してください
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)
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
また、「カッコはあるけど、カンマは無い」という表記は、エラーになります
Prelude> f(a b) = a / b
parse error (possibly incorrect indentation or mismatched brackets)
2.5 Haskellプログラム
ファイルによる関数定義と、GHCiからの呼出、GHCiコマンド、命名規則、レイアウト規則、タブ文字、コメントについての記載ありますが、9章や10章でファイルを使ったサンプルがあるため、ここでは割愛します
オマケ: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]
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.「いいね」よろしくお願いします
ページ左上のや のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!