Patrick Stormさんの2015年7月13日付のブログ記事Function currying in Elixirの翻訳です。
今までHaskellかじりーの、Scalaかじりーの…してて何度も出くわしてきた「カリー化」ですがなんかよく使いドコロがわからなくてスルーしてきました。
Elixir頭でならメリットとか理解できるかなあ…と思い翻訳しました。
カリー化って?
カリー化はアリティNの関数を全てがアリティ1を持つ関数のシーケンスに変換するためのテクニックです。関数のアリティとは関数が取る引数の個数のことです。
# この add 関数はアリティ2を持つ
add = fn x, y -> x + y end
カリー/カリー化という用語は今を遡る60年代に論理学者ハスケル・カリー(Haskell Curry)からその名をとって考え出されました。彼の名を持つプログラミング言語1にはカリー化関数がビルトインされていますし、他の多数の言語でもライブラリを使って同様の振る舞いが実装されています。
Elixirでカリー化を実装する
Elixirは関数型言語ですがHaskellとは異なりビルトインされた関数カリー化機構を持ちません。
Haskellでこのようにできます。
let add x y = x + y -- add はデフォルトでカリー化されている
let increment = add 1
increment 3 -- 4が返る
馬鹿正直なアプローチ
Elixirの関数はファーストクラスなので関数は関数を返したり、値として受け取ったりできます。
add = fn x ->
fn y -> x + y end
end
そしてこのadd関数はHaskell版と同じように扱えます。
increment = add.(1)
increment.(3) # => 4が返る
これはこれで動くんですが、欲しいのは明らかにもっと汎用的で再利用可能なものです。実際、上記のものは部分適用されただけの普通の関数と考えることができます。2
もっとうまくできるはず
今更ながら、Elixirは素晴らしい言語であり、もっと汎用的な解を得る道具立てを持っています。課題をやっつけるためにパターンマッチングと再帰を使いましょう。
最初の節でアリティについて話しましたね。アリティはElixir及びErlangではとても重要です。この言語は同じ名前を持つ関数であってもアリティが違うものは全く別のものと考えます。これが後に整数がついたスラッシュ、例えばsome_function/2
といった表記を言語のドキュメントの至るところで目にする理由です。
自前のカリー化関数を作ろう
curry/1
カリー化の実装に最初に必要な関数から始めましょう。ここでは関数はアリティ1を持ちます。
:erlang
アトムで(呼び出しコストゼロで)Erlangのコードを呼び出すことができます。これによってカリー化される関数の情報を取得し、パターンマッチングで2番めの戻り値だけ見て最初の戻り値は_
で除外することでコンパイラに「未使用の変数がある」と怒られないようにします。最終的にまたカリー関数を呼び出しますが、今度は違うアリティのものを呼び出します。
def curry(fun) do
{_, arity} = :erlang.fun_info(fun, :arity)
curry(fun, arity, [])
end
curry/3
今度はカリー化関数はアリティ3を持ちます。カリー化される関数、関数のアリティ、関数への引数です。この引数は最初の呼び出しでは単に空のリストです。
この関数は引数arg
を取る関数を返し変数アリティを1減らして自分自身を再帰呼び出しします。ここでの変数arity
はときにアキュムレータと呼ばれ、再帰関数に広く利用されます。最終的にもともと空のリストであった最後の引数には与えられた引数で埋められます。
def curry(fun, arity, arguments) do
fn arg -> curry(fun, arity - 1, [arg | arguments]) end
end
curry/3
待って!Elixirでは関数は名前とアリティで区別されるのではありませんでしたっけ?では同じ名前とアリティを持つ2つの関数をElixirではどうすれば区別できるのでしょうか?
Elixirがどのように関数を呼び出しているかテストして答を見つけましょう。
defmodule Callme do
def im_unique {:unique, "callme"} do
IO.puts "Ring ring"
end
def im_unique {:unique, "callyou"} do
IO.puts "No answer"
end
end
Callme.im_unique {:unique, "callme"} # => Ring ring
どちらの関数も同じ名前、同じアリティを持ち、さらに同じタイプの入力まで受け付けます。しかしそこにはちょっとした違いが、つまりタプルが同じではありません。ですから関数の起動にはパターンマッチングが使われていると結論付けることができます。
カリー化関数の最後の部品を作る
2番めのカリー化関数はカリー化したかった元の関数が持っていたアリティの数だけ再帰呼び出しされるので、二度とパターンマッチしません。そしてElixirは最後のカリー化関数にマッチするでしょう。アリティパラメータは0
まで減って元の関数を引数に適用することができます。
パラメータごと再帰させたのでリストの順序を逆順に並び替える必要があります。ユーザーが渡したパラメータの元の順序をグチャグチャにしないように。fun
関数は引数のリストに集められたアイテムの個数と同じアリティを持つはずです。
def curry(fun, 0, arguments) do
apply(fun, Enum.reverse arguments)
end
全部まとめると
ではコードをひとつにまとめてテストしてみましょう。
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
このカリー化関数をどのように使うか見てみましょう。
import Curry
curried = curry(fn a, b, c, d -> a * b + div(c, d) end)
# => #Function<0.122306914/1 in Curry.curry/3>
# 関数が返されてきた!
# いくつかパラメータを適用してみよう!
curried.(5).(5).(10).(2)
# => 30
# 全パラメータが渡された時の結果が返ってきた!
five_squared = curried.(5).(5)
five_squared_plus_five = five_squared.(10).(2)
# => 30
# 結果が得られた!
例をもう少し
カリー化関数を使うと小さくて再利用可能な関数を作ることができます。そういった関数はテストしやすいし、中身を推測するのも容易なのです。
defmodule Curried do
import Curry
def match term do
curry(fn what -> (Regex.match?(term, what)) end)
end
def filter f do
curry(fn list -> Enum.filter(list, f) end)
end
def replace what do
curry(fn replacement, word ->
Regex.replace(what, word, replacement)
end)
end
end
# 空白文字がひとつ以上あればマッチする
has_spaces = Curried.match(~r/\s+/)
# 上のhas_spacesをフィルタで再利用する
sentences = Curried.filter(has_spaces)
# ヤバイものを検閲(j,r,u,e,s,b,t,n,iのどれかの文字が検閲対象)
disallowed = Curried.replace(~r/[jruesbtni]/)
# 検閲済みマーク(*)を挿入する
censored = disallowed.("*")
# 引数をカリー化された関数に渡す
allowed = sentences.(["justin bibier", "and sentences", "are", "allowed"])
# => 中に空白文字がある["justin bibier", "and sentences"]が返る
# 最初の文をパイプして検閲フィルタをかませる
allowed |> List.first |> censored.() # => "****** ******"が返る
見てきた通り、カリー化は関数の再利用と高階関数の作成を極めて簡単にします。パイプ演算子で関数をチェインできるのでElixirは他の大半の言語よりさらに簡潔ですね。