プログラミング Elixir 第五章


概要

プログラミング Elixir

無名関数について解説している章。
無名関数とは、名前をつけずに関数自体を変数に束縛して、それを呼び出す方法。

iex> sum = fn (a, b) -> a + b end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex> sum.(1, 2)
3

例の様に 引数 -> 関数の実装(ボディ)fn ... end で囲み定義する。
呼び出す際は、.() で呼び出す。名前付き関数とは違い、., () が必要となる。

関数定義の際には () は省略可能である。

iex> sum = fn a, b -> a + b end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex> sum.(1, 2)
3
iex> say = fn -> IO.puts "hello" end
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> say.()
hello

関数とパターンマッチ

sum.(1, 2) と呼ぶと a に 1、b に 2 が代入されている様に感じるが、Elixir に代入は存在しない(プログラミング Elixir 第二章)。1 は a に代入ではなく、束縛され、パターンマッチが行われている。

以下の例を見るとわかりやすい。

iex> match? = fn (1, 2) -> "match" end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex> match?.(1, 2)
"match"
iex> match?.(1, 3)
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/2

関数に複数のボディを持たせる

Elixir は一つの関数に、異なる型・内容の引数、それに対応したボディを実装することができる。これとパターンマッチの機能を使って、便利な関数を実装する事ができる。

iex> fn
...>   (pattern 1) -> hogehoge
...>   (pattern 2) -> fugafuga
...>   (pattern 3) -> piyopiyo
...> end

上記の様なイメージで、パターンの異なる引数とボディを実装する。この際、引数の数を変えることはできない。
実行した際に pattern 2 にマッチする値を渡した場合は fugafuga が実行される。

以下は、この機能をうまく使った無名関数の例である。

iex> file_open = fn
...>   {:ok, file} -> "Read data: #{IO.read(file, :line)}"
...>   {_, error} -> "Error: #{:file.format_error(error)}"
...> end
#Function<6.52032458/1 in :erl_eval.expr/5>

iex> file_open.(File.open("./sample.txt"))
"Read data: Hello!!"
iex> file_open.(File.open("./not_exist.txt"))
"Error: no such file or directory"

無名関数を見てみると 2 つの引数が定義されている。一つ目のタプルは :ok を第一要素に持ったタプル。二つ目のタプルは _ を第一要素に持ち、どんな値でもマッチする。
これによって File.open の結果によって動作の異なる関数を実装することができる。

ここで、:file は Erlang の File モジュールを参照していて、それによって format_error メソッドを呼び出している。また、文字列で #{} で囲むと式が評価される。

関数を返す関数

関数を返す関数を定義することもできる

iex> hello = fn -> fn -> "Hello" end end
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> hello.()
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> hello.().()
"Hello"

hello.() で外側の関数が呼ばれ、内側の関数を返す。
そして、返ってきた内側の関数が .() で実行され、Hello を出力している。

書き方は改行とインデントを入れたり、内側の関数定義を括弧でくくることもできる。

iex> hello.().()
"Hello"
iex> hello = fn ->
...>           fn ->
...>             "Hello"
...>           end
...>         end
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> hello = fn -> (fn -> "Hello" end) end
#Function<20.52032458/0 in :erl_eval.expr/5>

束縛を引き継ぐ

以下は外側で束縛した値を内側の関数が引き継いでいる例

iex> greeter = fn name -> (fn -> "Hello #{name}" end) end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> dave_greeter = greeter.("dave")
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> dave_greeter.()
"Hello dave"

このように外側で束縛した値は、内側の関数の変数にも束縛されていて、後から呼び出すことができる。

これを利用することで以下のような関数を作ることができる。

iex> add = fn n -> (fn m -> n + m end) end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> add_one = add.(1)
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> add_three = add.(3)
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> add_one.(2)
3
iex> add_three.(2)
5

add_onen に 1 が束縛されているので、引数に 1 を足す関数。
そして、add_three n に 3 が束縛されているので、引数に 3 を足す関数となる。

関数を引数として使う

これまで見てきたように関数はただの型の一つなので、引数として使用できる。

iex> divide_half = fn n -> n / 2 end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> executor = fn (func, value) -> func.(value) end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex> executor.(divide_half, 6)
3.0

2 で割る関数を executor に渡して、executor は第二引数の value を func に渡して実行している。

引数にピン演算子を使う

ピン演算子を変数につけるとパターンで使うことができるが、ピン演算子は引数にも使用できる。

iex> greet = fn name, greeting ->
...>           fn
...>             (^name) -> "#{greeting} #{name}"
...>             (_)     -> "I don't know you"
...>           end
...>         end
#Function<12.52032458/2 in :erl_eval.expr/5>

iex> hello = greet.("Tom", "Hello")
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> hello.("Tom")
"Hello Tom"
iex> hello.("Mike")
"I don't know you"

greet.("Tom", "Hello")name"Tom" を束縛して、その後はパターンマッチを行っている。マッチした時のみ一行目が実行される。

& 記法

fn () -> ... end は & 記法を使って短く書くこともできる

iex> add_one = &(&1 + 1) # fn (n) -> n + 1 end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> add_one.(2)
3

引数を増やしたい場合は &1, &2, &3 ... としていく

また、Elixir は既にある関数を呼び出すだけの無名関数を作成した場合、無名関数を作らずに直接参照する様に最適化する。

iex> add = &(&1 + &2)
&:erlang.+/2
iex> add.(1, 2)
3

ただし、引数をそのままの順番で渡す必要がある。

iex> add = &(&2 + &1)
#Function<12.52032458/2 in :erl_eval.expr/5>

また、リストやタプルのリテラルを関数に変換する事もできる。
以下は和と差を返す関数。

iex> add_and_diff = &{ (&1 + &2), (&1 - &2) }
#Function<12.52032458/2 in :erl_eval.expr/5>
iex> add_and_diff.(3, 1)
{4, 2}

iex> add_and_diff = &[ (&1 + &2), (&1 - &2) ]
#Function<12.52032458/2 in :erl_eval.expr/5>
iex> add_and_diff.(3, 1)
[4, 2]

また、& 記法は既に存在する関数の名前とアリティ(パラメーター数)を渡すと、それを呼び出す無名関数を返す。

iex> add = &+/2
&:erlang.+/2
iex> add.(1, 2)
3

iex> l = &length/1
&:erlang.length/1
iex> l.([1, 2, 3, 4])
4