【関数型言語】Elixir の基本文法 (2)

| 0件のコメント

Programming Elixir: Functional, Concurrent, Pragmatic, Fun』を読んでいます。前回の続きでI-6章からI-13章までです。

環境は OSX, Elixir 1.0.2です。
本投稿における間違いは私の理解不足に依るものなので, 本書の内容の正確性とは一切関係ありません。また, 英語が苦手なので誤訳もあるかと思います。

6. Modules and Named Functions

Timesモジュールに, 引数の値を2倍して返す double関数を持たせる。

defmodule Times do
	def double(n) do
		n * 2
	end
end

モジュールのコンパイル方法は2種類紹介されている。まずは, iex にファイルを指定して loadするやり方。

$ iex times.exs
iex(1)> Times.double(2)
4

REPLの中では c でimportができる。
doubleに文字列を渡すと例外 ArithmeticErrorが挙がる。

$ iex
iex(1)> c "times.exs"
[Times]
iex(2)> Times.double(3)
6
iex(3)> Times.double("cat")
** (ArithmeticError) bad argument in arithmetic expression
    times.exs:3: Times.double/1

続いて, do記法について。
do:の syntactic sugar (糖衣構文)としてdo…endブロックがある。下記のように do: で1行で書く場合や, 複数行でもdo:ブロックを()で囲う時は endはなくても良い。

def double(n),  do: n * 2

do…endブロックを使った場合。

defmodule Factorial do
  def of(0), do: 1
  def of(n), do: n * of(n-1)
end

デフォルトパラメータは param \\ value の構文で書く。
例として, 引数2つの足す funcを持つ Exampleモジュールを定義する。

defmodule Example do
	def func(a \\ 10, b \\ 20) do
		a + b
	end
end
$ iex param_example.exs
iex(1)> Example.func(1)
21
iex(2)> Example.func(1,2)
3

Exampleモジュールを拡張して, デフォルトパラメータや型によって関数の中身を変えてみる。

defmodule Example do
	def func(a, b \\ 20)

	def func(a, b) when is_list(a) do
		"You said #{b} with a list"
	end

	def func(a, b) do
		"You passed in #{a} and #{b}"
	end

end

渡す型をIntegerとStringと変えて振る舞いが変化している。

iex(3)> IO.puts Example.func(5)
You passed in 5 and 20
:ok
iex(4)> IO.puts Example.func(5, "cat")
You passed in 5 and cat
:ok
iex(5)> IO.puts Example.func([5], "cat")
You said cat with a list
:ok
iex(6)> IO.puts Example.func([5])
You said 20 with a list
:ok

続いて, pipe演算子について。本文中の pipe演算子の説明を引用する。

“Programming is transforming data, and the |> operator makes that transformation explicit.”

|> は明示的に変換を行う。構文的には, val |> f(a, b) と書き f(val, a, b)と同じである。

例えば, result = buz(bar(foo(DB.find_uer)))と () がネストしていくような書き方は読み辛くなってしまう。 |> は読みやすい。

result = DB.find_uer
  |> foo
  |> bar
  |> buz

Enum.map と組み合わせた例。

iex(2)> (1..10) |> Enum.map(&(&1*&1))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

次は, モジュールの名前空間について。

defmodule Outer do
	defmodule Inner do
		def func1 do
		end
	end

	def func2 do
		Inner.func1
	end
end

Outer.func2 や Outer.Inner.func1 のようにネストされたモジュールにdot演算子でアクセスできる。
また, ネストされたモジュールを直接定義することもできる。下記では, Foo.Bar.buzでアクセスできる

defmodule Foo.Bar do 
	def buz do
	end
end

モジュールを動かすための Directiveとして alias, import, requireの3つある。aliasで モジュールを使う例。

iex(4)> alias Foo.Bar, as: Example
iex(5)> Example.buz

7. Lists and Recursion

再帰構造の例として, 独自のListモジュールの関数を書いてみる。
[head | tail]を使って, Listの最初の要素を計算して, 残ったtailを再帰的に自身に渡していく。

defmodule RecursiveList do
  def len([]),             do: 0
  def len([_head | tail]), do: 1 + len(tail)

  def sum([], total),              do: total
  def sum([ head | tail ], total), do: sum(tail, head+total)

  def square([]),              do: []
  def square([ head | tail ]), do: [ head*head | square(tail) ]

  def map([], _func),             do: []
  def map([ head | tail ], func), do: [ func.(head) | map(tail, func) ]

  def reduce([], value, _),               do: value
  def reduce([head | tail], value, func), do: reduce(tail, func.(head, value), func)

end

関数型言語なので, この辺がすっきり書けるのは良い感じ。

iex(1)> RecursiveList.len(["dog", "cat", "bird"])
3
iex(2)> RecursiveList.sum([1,2,3], 0)  
6
iex(3)> RecursiveList.square([1,2,3,4,5])             
[1, 4, 9, 16, 25]
iex(4)> RecursiveList.map([1,2,3,4,5], fn (n) -> n > 3 end)
[false, false, false, true, true]
iex(5)> RecursiveList.reduce([1,2,3,4,5], 1, &(&1*&2))
120

もう少し複雑なListのパターン。Listの先頭の2つの要素を入れ替える例。

# https://media.pragprog.com/titles/elixir/code/lists/swap.exs
defmodule Swapper do
  def swap([]), do: []
  def swap([ a, b | tail ]), do: [ b, a | swap(tail) ]
  def swap([_]), do: raise "Can't swap a list with an odd number of elements"
end

Listの要素数が奇数の場合, swapできずに例外を挙げるようになっている事を確認してみる。

iex(1)> c "swapper.exs"
[Swapper]
iex(3)> Swapper.swap([1,2,3,4,5])
** (RuntimeError) Can't swap a list with an odd number of elements
    swapper.exs:4: Swapper.swap/1
    swapper.exs:3: Swapper.swap/1
iex(3)> Swapper.swap([1,2,3,4])  
[2, 1, 4, 3]

8. Dictionaries: Maps, HashDicts, Keywords, Sets, and Structs

Dictionaryから hashDicts, Mapへの型変換。

iex(1)> dict = [name: "fisproject", likes: "Programming", where: "tokyo", likes: "cat"] 
[name: "fisproject", likes: "Programming", where: "tokyo", likes: "cat"]
iex(2)> hash = Enum.into dict, HashDict.new
#HashDict<[name: "fisproject", where: "tokyo", likes: "cat"]>
iex(3)> map = Enum.into dict, Map.new
%{likes: "cat", name: "fisproject", where: "tokyo"}

dictの要素へのアクセスはget, get_valuesなどがある。get_valuesは複数の値が取れる。

iex(4)> dict[:likes]
"Programming"
iex(5)> Dict.get(dict, :likes)
"Programming"
iex(6)> Keyword.get_values(dict, :likes)
["Programming", "cat"]

mapとパターンマッチを使った例として BMIを計算する関数を書いた。
本文中では確か, ホテルのベッドサイズを身長から求める関数だったと思う。

people = [
	%{name: "bob", height: 1.75, weight: 90},
	%{name: "jack", height: 1.7, weight: 66},
	%{name: "james", height: 1.8, weight: 50}
]

defmodule BMI do

	def calc(%{name: name, height: height, weight: weight})
	when 25 < (weight / (height * height)) do
		IO.puts "#{name}'s BMI is high"
	end

	def calc(%{name: name, height: height, weight: weight})
	when 18.5 > (weight / (height * height)) do
		IO.puts "#{name}'s BMI is low"
	end

	def calc(%{name: name}) do
		IO.puts "#{name}'s BMI is normal"
	end
	
end

呼び出す方も, pipe演算子とEnum.eachですっきり書ける。

people |> Enum.each(&BMI.calc/1)

bob's BMI is high
jack's BMI is normal
james's BMI is low

Mapに限らず Elixirの全ての値は不変であるので Mapの更新も new mapを作っている。

iex(10)> map = %{a: 1, b: 2, c: 3}
%{a: 1, b: 2, c: 3}
iex(11)> map1 = %{ map | b: "two", c: "three"}
%{a: 1, b: "two", c: "three"}

モジュールの中で Structureを定義するには, defstructを使う。
Dictionary Structures はネストすることができ, company.prefecture.city のようにdot記法でアクセスできる。

続いて, Elixirの Kernelによるマクロや関数について。
ネストされた構造体に対して値を取得する get_in, 値を置く put_inの使い方。

nested = %{
    today: %{
      name: %{
        first: "tarou",
        last:  "tanaka"
      },
      age: 21
    },
    yesterday: %{
      name: %{
        first: "toru",
        last:  "satou"
      },
      age: 19
    }
}

IO.inspect get_in(nested, [:today])
# %{age: 21, name: %{first: "tarou", last: "tanaka"}}

IO.inspect get_in(nested, [:today, :name])
# %{first: "tarou", last: "tanaka"}

IO.inspect get_in(nested, [:today, :name, :first])
# "tarou"

9. An Aside—What Are Types?

Elixirのプリミティブな型とモジュールに関する補足という内容。
プリミティブなlist型は, […]リテラルでlistを作ったり, | 演算子はlistを分解して再度作る機能がある。また別のレイヤーとして, Listモジュールにはlistsを操作する関数が用意されている。

10. Processing Collections—Enum and Stream

EnumモジュールとStreamモジュールについて。
まずはEnumモジュール。CollectionのListへの変換, 定番系のconcat, map, filter, sort関数がある。

iex(1)> list = Enum.to_list 1..5
[1, 2, 3, 4, 5]
iex(2)> Enum.concat([1,2,3], [4,5,6])
[1, 2, 3, 4, 5, 6]
iex(3)> Enum.map(list, &(&1*&1))
[1, 4, 9, 16, 25]
iex(4)> Enum.map(list, &String.duplicate("*", &1))
["*", "**", "***", "****", "*****"]
iex(5)> Enum.filter(list, &(&1 > 3))
[4, 5]
iex(6)> Enum.sort ["a", "b", "d", "e", "c"]
["a", "b", "c", "d", "e"]

古そうで新鮮に感じたのは, ポジションを指定して要素を取り出す at関数。

iex(6)> Enum.at(10..20, 3)
13

max関数は文字列に対しては, Zに最も近い文字を返す。minの場合だと逆になりAを返す。

iex(7)> Enum.max ["a", "b", "d", "e", "c"]
"e"
iex(8)> Enum.min ["a", "b", "d", "e", "c"]
"a"

take, split, join, zip。

iex(9)> Enum.take(list, 2)
[1, 2]
iex(10)> Enum.split(list, 3)
{[1, 2, 3], [4, 5]}
iex(11)> Enum.join list     
"12345"
iex(12)> Enum.zip(list, [:a, :b, :c])  
[{1, :a}, {2, :b}, {3, :c}]

続いて, Elixirの特徴のひとつであるStream機能はStreamモジュールで提供される。本文中のStreamの説明を引用,

“A Stream Is a Composable Enumerator”

そのまま訳すと, Streamは合成可能なEnumerator (列挙子)である, となる。

iex(1)> stream = Stream.map [1,3,5,7], &(&1+1)
#Stream<[enum: [1, 3, 5, 7], funs: [#Function<45.29647706/1 in Stream.map/2>]]>
iex(2)> Enum.to_list stream
[2, 4, 6, 8]
iex(3)> odds = Stream.filter [1,2,3,4,5], fn x -> rem(x,2) == 1 end
#Stream<[enum: [1, 2, 3, 4, 5],
 funs: [#Function<39.29647706/1 in Stream.filter/2>]]>
iex(4)> Enum.to_list odds
[1, 3, 5]

Streamで扱えるのは Listだけではない。IO.streamは IO Deviceも streamに変換できる。

遅延評価なので, 1..10_000_100 も必要なだけ計算される。
試しに Streamを Enumに変えると非常に大きい listをメモリ上に実際に作ってしまう。

iex(1)> Stream.map(1..10_000_100, &(&1+1)) |> Enum.take(10)
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

Stream.iterateは無限の streamを生成する。

iex(2)> Stream.iterate(0, &(&1+1)) |> Enum.take(50)        
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
 42, 43, 44, 45, 46, 47, 48, 49]
iex(3)> Stream.iterate([], &[&1]) |> Enum.take(5)  
[[], [[]], [[[]]], [[[[]]]], [[[[[]]]]]]

他にも Stream.cycle, Stream.unfold, Stream.resourceなどの紹介もある。

続いて, Comprehensions (内包表記)について。基本構文は下記。

result = for generator or filter… [, into: value ], do: expression

値の全てのコンビネーションを抜き出して評価し, 新しい Collectionを生成する。2行目の例は25回繰り返される。

iex(1)> for x <- [1,2,3,4,5], do: x * x   
[1, 4, 9, 16, 25]
iex(2)> for x <- [1,2,3,4,5], y <- [1,2,3,4,5], x >= y, rem(x*y, 10) == 0, do: {x,y}
[{5, 2}, {5, 4}]

11. Strings and Binaries

stringは single-quoted formと double-quoted formの2種類があり内部表現は異なるが, 共通点は多い。

single-quoted formはキャラクタlist, double-quoted formは stringsとなる。single-quoted formはキャラクタのlistなので, Listの関数が使える。
例えば, to_tupleで個々のキャラクタの文字表現から 10進数表現が得られる。

iex(1)> single = 'elixir'
'elixir'
iex(2)> is_list single
true
iex(3)> length single
6
iex(4)> List.to_tuple single
{101, 108, 105, 120, 105, 114}

binaryリテラルは <<…>> と書く。
用途としては様々だけど, マルチメディアやネットワークプログラミングのパケットを扱う時などには bit単位でデータを処理する必要があったりする。n-bitで表現したい時, size::(n) と書ける。

iex(5)> bit = << 1::size(2), 1::size(3)>>            
<<9::size(5)>>
iex(6)> byte_size bit
1
iex(7)> bit_size bit 
5

Elixirに限らないかもしれないが, Elixirにはいくつかの構文に代替の構文がある。
例えば, 正規表現は ~r{…}を使ってかける。Elixirでは ~スタイルのリテラルは sigils (a symbol with magical powers)と呼ばれる。

~wは, whitespaceで区切ったエスケープされている単語listの代替構文。

iex(3)> ~w[the cat sat]
["the", "cat", "sat"]
iex(4)> ~w"""
...(4)> the
...(4)> cat
...(4)> sat
...(4)> """
["the", "cat", "sat"]
iex(5)> ~w[the c#{'a'}t sat]a
[:the, :cat, :sat]

本章では, 他にもStringモジュールやビッグ/リトルエンディアンの話題も紹介されている。

12. Control Flow

制御フローについて, if, unlessの使い方。

iex(1)> if 1 == 2, do: "true", else: "false"
"false"
iex(2)> unless  1 == 2, do: "true", else: "false"
"true"

FizzBuzz問題を Elixirでエレガントに書いてみる例。defp は private function。

defmodule FizzBuzz do
  def upto(n) when n > 0 do
    1..n |> Enum.map(&fizzbuzz/1)
  end

  defp fizzbuzz(n)  when rem(n, 3) == 0 and rem(n, 5) == 0, do: "FizzBuzz"
  defp fizzbuzz(n)  when rem(n, 3) == 0, do: "Fizz"
  defp fizzbuzz(n)  when rem(n, 5) == 0, do: "Buzz"
  defp fizzbuzz(n), do:  n
end

実行してみる。

iex(1)> FizzBuzz.upto(15)
[1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

ガード条件 (Guard Clause)の例。

defmodule Users do
  def live(user) do
    case user do
      %{state: some_state} = person ->
        IO.puts "#{person.name} lives in #{some_state}"
      _ ->
        IO.puts "No matches"
    end
  end
end

_ (underscore) はそれ以外の全てにパターンマッチする。

iex(2)> Users.live(%{ name: "Dave", state: "TX", likes: "programming" })
Dave lives in TX
:ok
iex(3)> Users.live(%{ name: "Dave", likes: "programming" })             
No matches
:ok

raise関数で例外エラーを挙げることができる。メッセージは必須で, オプションで例外の型を指定する。

iex(1)> raise "Oops"
** (RuntimeError) Oops
iex(1)> raise RuntimeError, message: "Oops!" 
** (RuntimeError) Oops!

13. Organizing a Project

今回は文法を覚えることが目標なので, Elixir Projectのデザインと周辺ツールを紹介する本章は短い紹介に留めておく。Mixは build tool, ExUnitはUnit Testing Framework。
ExDocはRubyのRocのようなDocumentation toolで mix.exs に名前や, GitHubリポジトリ, 依存ライブラリなどを書いておくと mix deps.get で依存ライブラリを取得したり, mix docs でドキュメントを生成したりできる。

おわりに

13章は結構ページ数も割いているので, ガッツリElixirで何かつくりたい方は読んでおくと良いかもしれません。
改めてBlogの内容を見返してみると, 断片的なメモを繋ぎ合わせた適当な感じになってしまってたので, 気になった方は是非本書を手に取って読んでみて下さい。


[1] Programming Elixir
[2] Elixir ご紹介