DT日記

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

カリー化ってなぁに? をRubyから。

9月4日とか5日あたりにTwitterでカリー化の話で盛り上がってたので、話についていけなかったひと向けに、自分なりにRubyを使ってカリー化の説明を試みます。

はじめに、Rubyについてかるーく。

xyz = ->(x, y, z){ return x * y * z }

puts xyz.call(2, 3, 4)
#=> 24

puts xyz.call(3, 3, 3)
#=> 27


これが、「三つの引数をとって、すべてを掛け合せた数値を返す」函数(みたいなもの)のコードになります。return はなくても良いのですが、このあとの説明が読みにくくなるので、意味を明確にするために付ける方針で統一します。

この書き方(->(n){ ... })は、もしかするとあなたが既に慣れ親しんでるかもしれない「メソッド」とは似てるけど別物なので気をつけてください。その説明は省きますが、defキーワードは今回は無視します。

これをRubyではラムダ式と読んだり、このオブジェクトの型はProcだったりするのですが、この記事に限っては函数と呼んでおくことにします。念のために補足しておきますが、メソッドは函数に似たものではありますが、函数ではありません

ここでカリー化の定義を見てみます。


カリー化 (currying) とは、計算機科学分野の技法の一つ。複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること。この技法は、Christopher Strachey により論理学者ハスケル・カリーに因んで名付けられたが、実際に考案したのは Moses Schönfinkel とゴットロープ・フレーゲである。

f(a,b) = c という関数 f があるときに F(a) = g ただし、 g(b) = c という関数 g が得られる関数 F を定義した場合、 F は、 f をカリー化したものである。

関数 f が の形のとき、f をカリー化したものを g とすると、g は の形を取る。非カリー化 (uncurrying) とは、これの逆の変換である。

ほむほむ。よくわかんないけど、 f(x) = x + y とか、中学校の数学でやった感じの函数に似てますね。

とりあえず、さっきのWikipediaからの引用の c を 勝手に x + y ってことにしておきます。で、Rubyでは大文字の変数名は使用できないので、 f = f1 , F = f2 ってことにしてみます。

これをRubyでプログラミングしてみたいのですが、ここで一番最初のコードを見てみませう。

xyzは変数で、中身にはProcオブジェクトが入ってます。このProcオブジェクトがRubyにおける函数の実体で、これは変数に入れてやりとりができます。実はメソッドも変数に代入できるんだけど、それは置いておいて……。

で、函数は変数に入ってるみたいなので、f1って名前の変数を作りたいときは上のコードのまねをして、f1 = ->( ){ ... }なんてふうにしてみれば良さげですね。作った函数を呼び出すときはf1.call(n)ですね。

ってわけで書いてみます。

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

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

  return g
}

とりあえずこれでWikipediaからの引用と同じものをRubyで再現できた気がするんですけど、どうですかね?

では、これを動かしてみます。

f1.call(5, 6)
#=> 11

gg = f2.call(5)

gg.call(6)
#=> 11

はい、動きますね。f2はちゃんとg(b) = c という関数 g が得られる関数って条件は満たしてますね!

f1からf2みたいに書換することがカリー化(currying)、f2からf1みたいに書換することが非カリー化(uncurrying)と呼ぶって解釈で良さげですね。

では、次は一番最初のコードをカリー化した状態に書き換えてみます。

xyz =->(x){
  return ->(y){
    return ->(z){
      return x * y * z
    }
  }
}

puts xyz.call(2).call(3).call(4)
#=> 24

x3 = xyz.call(3)
x3y3 = x3.call(3)
puts = x3y3.call(3)
#=> 27

puts = x3y3.call(100)
#=> 900

こんどは、Returnの中に直接函数を書いてます。そうすると、きれいに入れ子になってるのがわかりますね。

x3, xy3, xyz3 は、函数を一度呼び出した都度、その結果を変数に代入してます。これが部分適用です。

で、一度部分適用した函数は、また別の引数を渡して呼び出すことができます。換言すると、カリー化とは部分適用できない函数を部分適用可能な状態に変換することです。

さて、カリー化とか部分適用とか何がおいしいの? って感じだと思ふので、最後に適当な例を挙げて終ってみます。

talk = ->(person){
  person.capitalize!
  return ->(dialogue){
    puts "#{person}#{dialogue}"
  }
}

bob = talk.call("bob")
alice = talk.call("alice")

bob["Hi."]
alice["こんにちはこんにちは!!!"]

bob["foooooooo"]
alice["イヤッホオオオオオオオオウ!!!!"]

# Bob 「Hi.」
# Alice 「こんにちはこんにちは!!!」
# Bob 「foooooooo」
# Alice 「イヤッホオオオオオオオオウ!!!!」

あんまりRubyらしいコードではないんですけどね。