プログラミングElixir第7章~リストと再起

  • リストの再帰的構造
  • リストの横断と構築
  • アキュムレータ
  • mapとreduceの実装

ヘッドとテイル

リスト中、縦棒をはさむことでリストの結合を表現できる?。今までの表現では、縦棒が出てこなかったけれども省略して書いていたと思えばよいだろう。 Lispのリストがこんなのだった気がする。要素1つと次へのポインタみたいな塊(間違ってたらごめんなさい)

iex(1)> [1,2,3,4]
[1, 2, 3, 4]
iex(2)> [ 1 | [ 2 | [ 3 | [ 4 | [] ] ] ] ]
[1, 2, 3, 4]
iex(3)> [h|t]=[ 1 | [ 2 | [ 3 | [ 4 | [] ] ] ] ]
[1, 2, 3, 4]
iex(4)> h
1
iex(5)> t
[2, 3, 4]
iex(6)> [h|t] = [1,2,3,4]
[1, 2, 3, 4]
iex(7)> h
1
iex(8)> t
[2, 3, 4]

[head|tail] で受けるようだけれど, tailに残りすべてが放り込まれる. 要素数ゼロだとエラーになる。縦棒があるので要素数が1つはあることをマッチング条件としているからかな..

iex(11)> [h|t]=[1]
[1]
iex(12)> h
1
iex(13)> t
[]

iex(14)> [h|t]=[]
** (MatchError) no match of right hand side value: []

リストの構築

defmodule TestList do
  def len([]), do: 0
  def len([_head|tail]), do: 1 + len(tail)
end
iex(15)> TestList.len( [ 4, 5, 67, 333] )
4

※関数引数名を "_" で始めると、その変数は未使用であることを宣言する。未使用引数はコンパイル時に警告が出る。

引数に渡された数値リストを、要素の値を二乗したリストを返す関数を考える:

defmodule TestList do
  def squre([]), do: []
  def squre([head|tail]), do: [ head*head, squre(tail)]
end
iex(3)> TestList.squre( [1,2,3,4])
[1, [4, [9, [16, []]]]]

リストになっていない... カンマで区切ると, squre()の返り値を1つの要素としてしまうのね.

defmodule TestList do
  def squre([]), do: []
  def squre([head|tail]), do: [ head*head | squre(tail)]
end
iex(5)> TestList.squre( [1,2,3,4])
[1, 4, 9, 16]

なるほど, 縦棒は偉大なり..

map関数の作成

Enum.map が存在するが、これを自作してみようという話. リストの要素ごとに 任意の関数を呼び出して結果をリストとして返す. squreの場合は fn (x) -> x*x end を実行させたようなものだ.

defmodule TestList do
  def map([], _func), do: []
  def map([head|tail], func), do: [ func.(head) | map(tail, func)]
end
iex(8)> TestList.map [1,2,3], &(&1*&1)
[1, 4, 9]
iex(9)> TestList.map [1,2,3], fn (x) -> x*x end
[1, 4, 9]

なるほど、素直に作れますね... 自作の場合、マッチングに制限をかけるとかして 想定外の結果を返さないようにするなどできそうね.

再起中の値の保持

上までは リストを受けて同じ要素数のリストを返す方法をみてきた. リスト全体の要素を見て、結果を返すことを考える. 簡単な例で数列の和を返す sum() 関数を実装する.

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

iex(2)> TestList.sum([1,2,3,4,5,6,7,8,9,10],0)
55

なるほど, よさそうだ. リストの前方から順に結果を得られる場合(リスト後方の値に依存しない場合)には有用そうだ. 数列だけではなくいろいろと妄想がはかどるが, 基本的には 状態 を 引数として渡すしかないようである.

これには 連想配列やキーワードリストが効果的な気がするね..

次にこれをリファクタリングする:

defmodule TestList do
  def sum(list), do: _sum(list,0)

  defp _sum([], total), do: total
  defp _sum([head|tail], total), do: _sum(tail,head+total)
end

モジュール外から使用するときに, 状態保持用の第二引数をいちいち記述する必要がないからだ。計算の途中から実行したいという需要はあるかもしれないが、それはそういうインタフェースを作ればよいだろう. デフォルトパラメータとして sum(list, total \\ 0) としないのは、内部でのみ扱う 状態 を外に意識させないためだろう.

プライベート関数の宣言に defp を使っていることに注目するとよい.

やってみよう

状態が1つであれば そのまま関数の返り値として扱えるかな。関数の処理成功/失敗や

defmodule TestList do
  def sum([]), do: 0
  def sum([head|tail]), do: (
    head + sum(tail)
  )
end

iex(3)> TestList.sum( [1,2,3,4,5,6,7,8,9,10])
55

Enum.reduce の差異発明

sumの場合, 2つの値の和を返す処理を実装していた。この部分を汎用化すると, 関数を渡すことになるだろう。

defmodule TestList do
  def reduce([], value, _func), do: value
  def reduce([head|tail], value, func), do: reduce(tail, func.(head,value), func)
end

iex(2)>TestList.reduce [1,2,3,4,5], 0, fn m,n -> m+n end
15
defmodule People do
  def males([]), do: []
  def males([[name, :male, age]|tail]), do: [[name, :male, age] | males(tail)]
  def males([_|tail]), do: males(tail)

  def test_data do
  [
    ["YNK",  :male,   18],
    ["John", :male,   21],
    ["Mary", :female, 19],
    ["Beck", :male,   32]
  ]
  end
end

性別を選べるようにしよう:

defmodule People do
  def filter([], _sex), do: []
  def filter([[name, sex, age]|tail], sex), do: [[name, sex, age] | filter(tail,sex)]
  def filter([_|tail], sex), do: filter(tail,sex)
# ... test_data
end

関数の引数で与えた値が パターンマッチにも適用できている... 後方で与えている引数なのに, 前方のマッチングにも使われる.のか..

iex(5)> filter test_data,  :male
[["YNK", :male, 18], ["John", :male, 21], ["Beck", :male, 32]]
iex(6)> filter test_data,  :female
[["Mary", :female, 19]]
iex(7)> filter test_data,  :none
[]
defmodule People do
  def filter([], _sex), do: []
  def filter([head=[_, sex, _]|tail], sex), do: [ head | filter(tail,sex)]
  def filter([_|tail], sex), do: filter(tail,sex)
# ... def test_data do
end

リストのマッチングを head=で受けている。名前付き前方参照みたいな書式で、マッチングしたリストを 関数内で headとして参照できる. いちいち関数引数のパラメタを書き直す必要がないので 便利が良い.

やってみよう

fromからtoまでの数のリストを返す関数を書いてみる.

defmodule Test do
  def span(from,to) when from<to, do: [from | span(from+1,to)]
  def span(to,to), do: [to]
  def span(_from,_to) when _from>_to, do: []
end
iex(4)> Test.span(4,3)
[]
iex(5)> Test.span(4,4)
[4]
iex(6)> Test.span(2,6)
[2, 3, 4, 5, 6]

whenを使っていいのかな..

Listモジュール

ざっくりと紹介されている. helpで眺めるとかするとよいか.

困ったら 本家 を見るとよさそうね..