DT日記

家を離れた自宅警備員の日記

Rubyでのカリー化、をまじめに。

DT戦記: 2011-09-10 カリー化ってなぁに? をRubyから。では、カリー化とは何かといふことを、かなり廻りくどいやりかたで説明しました。廻りくどいだけで、これ自体は別に悪いアプローチではないのですが。

この記事でも前回の記事と同じく、Rubyラムダ式、つまり->(arg){ ... }lambda{ |arg| ... } で生成した Proc(lambda)クラスのオブジェクトを「函数」と呼ぶことにします。

前回のコードをひとつ引用してみます。

f1 = ->(x, y){
  return x + y
}

f2 = ->(x){
  return ->(y){
    return x + y
  }
}


このカリー化されたコードの問題は、「値をふたつとって、足した値を返したい」って明確な目的があるのに、f2ではxとyを分けて書かざるを得ず、ばらばらになってコードが読みにくくなることが挙げられます。2引数のこの例ならまだともかく、5個の引数を、あるいは10個の引数を要求する函数が必要な場合は……?

func = ->(a){
  ->(b){
    ->(c){
      ->(d){
        ->(e){
          ->(f){
            ->(g){
              ->(h){
                ->(i){
                  ->(j){
                    a + b * c - d * f + g / h - i * j
}}}}}}}}}}

やめて! と叫びたくなりますね。Rubyではreturnが必須ではないので省きましたが、それでもこんな量に……。

この場合は、部分適用が必要でなければ、非カリーな函数を作った方が見通しは良くなる気がします。

func = ->(a, b, c, d, e, f, g, h, i, j){
  return a + b * c - d * f + g / h - i * j
}

これならふつうに見やすいですね。

どうやらこの問題は、Rubyでの「カリー化函数を書く記法の煩雑さ」が元凶のような気がします。

その点で、F#では(おそらくOCamlも互換)、こんな風にコードを書くことができます。

let curried_func a b c d e f g h i j = a + b * c - d * f + g / h - i * j
let uncurry_func (a, b, c, d, e, f, g, h, i, j) = a + b * c - d * f + g / h - i * j

上がカリー化函数、下が非カリー函数*1です。下の場合はタプル(Rubyに同様の型はないが、配列がほぼ同じ目的に利用できる)で渡すようになってるので、カリー化されてない函数を書く方が煩雑なのです。

よって、カリー化函数を書くためのコストは言語の設計に依存することがわかります。

ではRubyでカリー化函数のメリットを十分に享受することはできないのか? 答は否。Rubyではもっと簡易にカリー化函数を利用できる仕組みが提供されてゐるのです。

func = ->(a, b, c, d, e, f, g, h, i, j){
  return a + b * c - d * f + g / h - i * j
}

curried_func = func.curry

curried_func.call(2).call(8).call(2).call(8)

あらふしぎ、.curryを噛ませるだけでfuncが部分適用可能な函数(=カリー化函数)になっちゃいました。何が起こってるのかわからないって方は、自分で引数の少ない簡単な函数を作ってみて、動かして試してみてくださいな。

……実はここが数日前のカリー化議論に関ってくるポイントで、Rubyのこのcurryメソッド(ラムダ式{=この場では「函数」}はProcオブジェクトなので、Rubyの流儀ではこのメソッドは Proc#curry と表記します)は、函数(Procオブジェクト)を部分適用可能な状態に変換します。

ところがGroovyといふ言語のcurryメソッド(Closure#curry)は、Groovy: カレー化クロージャーによるファンクショナル・プログラミングのように*2、「函数を部分適用可能な状態にする」のではなく、「函数を部分適用する」操作なのです。誤用だ誤用だ!

import functools

def xyz(x,y,z):
    return x + y + z

functools.partial(xyz, 1, 2)(3)
functools.partial(xyz, 1)(2, 3)

Pythonではこんなコードが書けます。これは "partial" の名前通り、カリー化ではなく、飽くまで部分適用です。functional.partialに一つしか引数を渡さなかったときに2引数の函数が帰ってくることから、これがカリー化でないことは明らかですね。

あとはC++にあるboost::bind は部分適用らしいですが、使ったことがないのでよくわからないでs…ああっ囲まないで! 囲まないで! 闇が!? ……けふんけふん。C++0xではC++11では言語組込みで std::bind があったりラムダ式があったり、なかなか素敵かもしれません。

まとめ

前回のぶんと併せて。

  • カリー化とは、部分適用可能な状態に変換すること
    • 部分適用とは、多引数の函数を、引数ひとつのみをとる函数入れ子にすることで、「部分的に引数が渡された状態」のままで扱へるようにすること
  • 多引数の函数に対するカリー化や部分適用は言語や組込みライブラリで提供されてることもある
    • Rubyでカリー化可能な多引数の函数を作りたいときは、非カリー函数で作ってから Proc#curry でカリー化函数に変換するのが良し
    • PythonとかC++11の組み込みライブラリで部分適用をすることはできるみたい

おまけ

Rubyでカリー化「函数」を実装してみた。

curry = ->(f){
  args = []
  return rec = ->(a){
    args << a
    return f[*args] if (args.length == f.arity)
    return rec
  }
}

if __FILE__ == $0
  func = ->(a,b,c){ a * b * c }
  
  puts func[1,2,3]
  puts curry[func][1][2][3]
end

Gist - currying_func.rb

Proc#curry への反逆。自分でやってみたかっただけ。やってみただけ。適当。

*1:F#の流儀(OCamlでも?)では「タプル函数」と呼ぶらしい

*2:カレー化って何だよ、カレー化って…