21
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-10-22

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は、全ての関数で、第一引数は、リストを取るのが最大の違いで、これは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 +  )
#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:

21
13
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
21
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?