LoginSignup
50
43

More than 5 years have passed since last update.

[翻訳] Elixirでの関数カリー化

Last updated at Posted at 2015-08-09

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は他の大半の言語よりさらに簡潔ですね。


  1. もちろんプログラミング言語Haskellのことです。 

  2. 部分適用とカリー化の違いはこちらがわかりやすいか。部分適用は「複数の引数を取る関数にいくつか引数を渡した状態の関数を返す」「(カリー化した後に)引数が入った関数を返す」。 

50
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
43