Edited at

【Elixir】関数で関数を返す(カリー化、部分適用)


はじめに

Qiitaを見漁っていたらこちらの記事を読みました→3歳娘「パパ、関数をカリー化して?」

ちょうど自分が学習しているところでも使えるのではないかと思ったので【Elixir初級】関数について(無名関数、名前付き関数)の学習ついでに少し寄り道してみました。

もっと詳しくカリー化について知りたいのであればこちらの記事が参考になると思います。

食べられないほうのカリー化入門


無名関数で関数を返す

無名関数で関数を返すとこのようなコードとなります。

IO.puts hello.()を実行すると関数を返してしまうのでエラーとなります。

なのでIO.puts hello.().()のように2つ引数を指定して内側の関数を呼ばなくてはいけません。

(Elixirでは引数の値がなくても()をつけなくてはいけません)


Elixir

hello = fn -> fn -> "Hello world" end end

IO.puts hello.().()
# Hello world
IO.puts hello.() #エラー
# (Protocol.UndefinedError) protocol String.Chars not implemented for #Function

では次に外側の関数に引数を追加してみましょう。

hello関数をsayhello関数に結び付けるとsayhello.()で実行できるようになります。


Elixir

hello = fn a -> fn -> "#{a} world" end end

IO.puts hello.("Hello").()
# Hello world
sayhello = hello.("Hello")
IO.puts sayhello.()
# Hello world

それではこのsayhello関数(内側の関数)にも引数を追加してみましょう。


Elixir

hello = fn a -> fn b-> "#{a} #{b}" end end

IO.puts hello.("Hello").("TANAKA")
# Hello TANAKA
sayhello = hello.("Hello")
IO.puts sayhello.("YAMADA")
# Hello TANAKA

ここまでくるとだいぶカリー化の基礎にまで近づいて気ました。

このhello関数はカリー化されています。


カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

出典: フリー百科事典『ウィキペディア(Wikipedia)』



パラメータのスコープは保存される

少し疑問に思ったのがsayhello = hello.("Hello")を実行した時点で変数aへのスコープは外れて内側の関数では使えないのではないか?

というところなのだが、Elixirの関数は定義されたスコープにある変数の束縛を持ち回るからIO.puts sayhello.("YAMADA")を実行時にも変数aは使えるそうだ。


カリー化と部分適用

カリー化することにより別機能を持つ関数を簡単に作ることができます。

このように一部の引数だけを渡すことを部分適用といいます。


Elixir

sum = fn a ->

fn b->
a + b
end
end

tasu1 = sum.(1)
tasu10 = sum.(10)
tasu100 = sum.(100)

IO.puts tasu1.(2) # 3
IO.puts tasu10.(2) # 12
IO.puts tasu100.(2) #102



もっといけてるカリー化

こちらの記事を参考にしました。Function currying in Elixir

とりあえず見てみましょう。


Elixir

defmodule Curry do

def curry(fun) do
{_, arity} = :erlang.fun_info(fun, :arity)
curry(fun, arity, [])
end

def curry(fun, 0, arguments) do
apply(fun, Enum.reverse arguments)
end

def curry(fun, arity, arguments) do
fn arg -> curry(fun, arity - 1, [arg | arguments]) end
end

end

hello = Curry.curry(fn a,b -> "#{a} #{b}" end)
IO.puts hello.("Hello").("TANAKA")
# Hello TANAKA


Curry.curry(fn a,b -> "#{a} #{b}" end)とするだけでhello関数(カリー化)をつくることができました。

このコードの中によくわからない部分がいくつかあるので一つずつ見ていきましょう。

まず初めに{_, arity} = :erlang.fun_info(fun, :arity)ですが:erlangを使うことによってErlangのコードを呼び出すことができます。

でfun_info/2とはいったい何なのかというと文字通り関数の情報を返す関数です。


Elixir

sum = fn a,b -> a + b end

IO.inspect {_, _} = :erlang.fun_info(sum, :name)
# {:name, :"-__FILE__/1-fun-1-"}
IO.inspect {_, _} = :erlang.fun_info(sum, :pid)
# {:pid, #PID<0.73.0>}
IO.inspect {_, _} = :erlang.fun_info(sum, :arity)
# {:arity, 2}

{_, arity} = :erlang.fun_info(fun, :arity)
# arity変数に受け取る引数の数を束縛している。


すると以下のcurry/1が理解できるでしょう。

次はcurry/1の中で再帰的に呼ばれているcurry/3について見ていきます。(Elixirは同じ名前を持つ関数であっても引数(アリティ)が違うものは全く別のものと考えます。)


Elixir

  def curry(fun) do

{_, arity} = :erlang.fun_info(fun, :arity)
curry(fun, arity, [])
end

先ほどの例でいうとパラメータの内容はこのようになっているはずです。

curry(sum, 2, [])

次に以下の部分です。


Elixir

  def curry(fun, arity, arguments) do

fn arg -> curry(fun, arity - 1, [arg | arguments]) end
end

def curry(fun, 0, arguments) do
apply(fun, Enum.reverse arguments)
end


安直な考えできっとこんなことをしているのだろうと思い、このようにしてみました。

curry/3の中で同じ関数(curry/3)を呼ぶことでループさせ、引数をListにつっこんでいきます。


Elixir

hello = fn arg -> 

fn arg ->
apply(fn a,b -> "#{a} #{b}" end, [arg,arg])
end
end
IO.puts hello.("Hello").("TANAKA")
# 結果は…"TANAKA TANAKA"あれ…

同じ変数名なので外側のargがどうやら使われていないようです。

でも元のコードを見る限り変数はargしか使っていないです…。

ここで重要な見落としをしていました。「Elixirは同じ名前を持つ関数であっても引数(アリティ)が違うものは全く別のものと考えます。」ということです。

同じ変数名でも使われている関数が違うので別物なのです。同じ関数名のcurryも引数の値で別の関数となるので中で使うarg変数も別物。


Elixir

defmodule Hoge do

def hoge(hello,2,[]) do
fn arg -> hoge(hello,1,[arg,[]]) end
end

def hoge(hello,1,arguments) do
fn arg -> apply(hello, [arg,arguments]) end
#argumentsにはhoge(hello,2,[])のarg変数がlist([arg,[]])になって結びついている。
#ここでのargとは別物
end
end


そして最後にパラメータを再帰したので、リストを元に戻すためにEnum.reverseをして整えて終了です。

だいぶざっくり理解でごめんなさい。