#はじめに
Qiitaを見漁っていたらこちらの記事を読みました→[3歳娘「パパ、関数をカリー化して?」]
(https://qiita.com/jzmstrjp/items/99cc1c8ebcfc703b1410)
ちょうど自分が学習しているところでも使えるのではないかと思ったので【Elixir初級】関数について(無名関数、名前付き関数)の学習ついでに少し寄り道してみました。
もっと詳しくカリー化について知りたいのであればこちらの記事が参考になると思います。
食べられないほうのカリー化入門
##無名関数で関数を返す
無名関数で関数を返すとこのようなコードとなります。
IO.puts hello.()
を実行すると関数を返してしまうのでエラーとなります。
なのでIO.puts hello.().()
のように2つ引数を指定して内側の関数を呼ばなくてはいけません。
(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.()
で実行できるようになります。
hello = fn a -> fn -> "#{a} world" end end
IO.puts hello.("Hello").()
# Hello world
sayhello = hello.("Hello")
IO.puts sayhello.()
# Hello world
それではこのsayhello関数(内側の関数)にも引数を追加してみましょう。
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は使えるそうだ。
##カリー化と部分適用
カリー化することにより別機能を持つ関数を簡単に作ることができます。
このように一部の引数だけを渡すことを部分適用といいます。
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
とりあえず見てみましょう。
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とはいったい何なのかというと文字通り関数の情報を返す関数です。
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は同じ名前を持つ関数であっても引数(アリティ)が違うものは全く別のものと考えます。)
def curry(fun) do
{_, arity} = :erlang.fun_info(fun, :arity)
curry(fun, arity, [])
end
先ほどの例でいうとパラメータの内容はこのようになっているはずです。
curry(sum, 2, [])
次に以下の部分です。
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につっこんでいきます。
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変数も別物。
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
をして整えて終了です。
だいぶざっくり理解でごめんなさい。