概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Functional Programming in Ruby — Closures – Brandon Weaver – Medium
- 原文公開日: 2018/05/14
- 著者: Brandon Weaver
訳注: 原文ではRubyのメソッドもfunctionと表記されています。
Rubyで関数型プログラミング#2: クロージャ(翻訳)
Rubyで使える関数型プログラミングの非常に強力な機能のひとつに、「クロージャ」の概念があります。
そもそもクロージャとはどんなもので、一体どんなことができるのでしょうか?
最初の例
次の関数があり、それが別の関数を返すとしましょう。
adder = proc { |a|
proc { |b| a + b }
}
この関数を呼び出すと、別の関数を受け取ります。それをadd3
と呼ぶことにしましょう。
add3 = adder.call(3)
これのどこが便利なのでしょうか?まず、これをmap
などの関数に渡すことができます。
[1, 2, 3].map(&add3)
# => [4, 5, 6]
ちょっと待った、その値をどこに記憶してたの?
この関数は、a
が3
であるということを覚えていたのです。いったいどうやったのかおわかりでしょうか?
クロージャとは、それが作成されたコンテキストを記憶している関数のことです。
つまり、返された関数のそのまた内部では、その周囲にあるものすべてが絶対的に公正なゲームとして把握されるのです。
カウントするクロージャ
カウンタについて考えてみましょう。Rubyでは次のようにそのためのクラスを作るでしょう。
class Counter
attr_reader :count
def initialize(count = 0)
@count = count
end
def increment(n = 1)
@count += 1
end
end
c = Counter.new
# => #<Counter:0x00007fb9353179f0 [@count](http://twitter.com/count "Twitter profile for @count")=0>
c.increment
# => 1
c.increment
# => 2
次のように、この考えにクロージャを使うこともできます。
counter = proc {
count = 0
{
increment: proc { |n = 1| count += n }
}
}
c = counter.call
# => {:increment=>#<Proc:0x00007fb9352d3b10@(pry):19>}
c[:increment].call
# => 1
c[:increment].call
# => 2
「ちょっと待った、
proc
の引数はデフォルト(値)を取れるよね?」おっしゃるとおりです。メソッドのシグネチャに置いたものはすべてブロックの引数リストで公正に扱われるのです。これはこれでいろいろと面白いのですが、本記事の範疇からは外れます。ちょっとだけ先回りしてお教えすると、これらもブロックを引数として取れるのです。
Rubyの場合、このクロージャの書き方は非常に利用価値が高いというほどではありません。次のようにクラスでもっと簡潔な書き方も使えるからです。
counter = (1..Float::INFINITY).to_enum
# => #<Enumerator: ...>
counter.next
# => 1
counter.next
# => 2
指定の倍数でカウントアップすることだってできます。
counter = (1..Float::INFINITY).step(5).to_enum
# => #<Enumerator: ...>
counter.next
# => 1.0
counter.next
# => 6.0
counter.next
# => 11.0
このままではあっという間にEnumerator
やlazyの話題に横滑りするので、そちらについては別記事に取っておきます。文字の集合でも同じようなことができるのでやってみましょう。楽しみは尽きませんね。
あれ?クラスも「記憶」の場所が見当たらないんだけど?
ここが面白みです。クラスはいろんな意味でクロージャの近似と言えます。クラスは若干のステートをカプセル化することで、定義済みのAPIから変更できるようになっています(APIとは外から呼び出し可能なメソッドのちょっと気取った言い回しです)。
公正のために申し上げれば、これらのクラスは実際には関数ではないのでクロージャとは動作が違(略。
class Adder
def initialize(n)
@n = n
end
def to_proc
proc { |v| @n + v }
end
end
[1, 2, 3].map(&Adder.new(5))
# => [6, 7, 8]
一応それっぽくはありますが、new
がちょっと目障りです。次のようにProc的なメソッドをある程度使えるようにしたいところです。
class Adder
def initialize(n)
@n = n
end
def to_proc
proc { |v| @n + v }
end
def call(v)
self.to_proc.call(v)
end
alias_method :===, :call
class << self
def call(n)
new(n)
end
alias_method :[], :call
end
end
[1, 2, 3].map(&Adder[15])
# => [16, 17, 18]
[1, 2, 3].map(&Adder.(7))
# => [8, 9, 10]
メソッド自体を実装するか素直にProc
を継承すれば、クラスでもクロージャ的な振る舞いの一部をかなり近似できることが上のコードからわかります。
現実に使われているクロージャ
私はQo gemやXf gemといったライブラリでこのトリックを多用しています。主な理由としては、このトリックによって継承を最上位に配置してそれが表す意味を関数として再定義できるからです。その中でも特に楽しめる例としてQoのガードブロックマッチャーがあります。
require 'qo'
# => true
matcher = Qo.m('baweaver', :*) { |(name, _)| "It's the creator" }
# => #<Qo::Matchers::GuardBlockMatcher:0x00007fb2ae983d08
# [@array_matchers](http://twitter.com/array_matchers "Twitter profile for @array_matchers")=["baweaver", :*],
# [@fn](http://twitter.com/fn "Twitter profile for @fn")=#<Proc:0x00007fb2ae983c18@(pry):2>,
# [@keyword_matchers](http://twitter.com/keyword_matchers "Twitter profile for @keyword_matchers")={},
# [@type](http://twitter.com/type "Twitter profile for @type")="and"
# >
matcher.call(['baweaver', 'something'])
# => [true, "It's the creator"]
matcher.call(['havenwood', 'something'])
# => [false, false]
直ちにこの配列が返りますが、true
やfalse
を返すだけの普通のマッチャーとどこが違うのかちょっと覗いてみましょう。
# Overrides the base matcher's #to_proc to wrap the value in
# a status and potentially call through to the associated block
# if a base matcher would have passed
#
# [@return](http://twitter.com/return "Twitter profile for @return") [Proc[Any] - (Bool, Any)]
# (status, result) tuple
def to_proc
Proc.new { |target|
super[target] ? [true, [@fn](http://twitter.com/fn "Twitter profile for @fn").call(target)] : NON_MATCH
}
end
このガードブロックは普通のマッチャーを継承しています。つまり、それまでにマッチしたことがあれば、ガード済みの関数を呼び出してもよいということを認識します。
ではなぜこの配列を返すのでしょうか?ガード済みブロックがfalse
を返すとどうなるのでしょうか?マッチしていたことをどうやって認識するのでしょうか?秘密を明かすと、ここにステートがもうひとつ追加されています。そのステートによって、実際にマッチしていたことと、ガードブロックが返した値が実際には真ではない(falsy)ことを認識しているのです。
この概念は、今後扱う予定の記事の内容に通じるものがあります。そちらの記事では、この概念を包含するSome
型やNone
型について扱う予定です。Qo gemでこの概念を使っていない理由は、Qoを軽量にしておきたいのと、他人のアプリに余分なアイデアを押し付けたくない、というだけのことです。
最後に
前回申し上げたとおり、関数型プログラミングとオブジェクト指向プログラミングは概念においてさほど違いはありません。せいぜい、ある種のアイデアの一部を表現する方法が多少異なる程度です。
Rubyはどちらも実行できるので、2つの領域から得たアイデアをいいとこ取りすればコードの表現力をさらに高められます。
まだ触ったことがなければ、また別のオブジェクト指向/関数型言語であるScala言語をしばらく触ってみることを強くおすすめします。Scala言語は関数型言語にさらに強く依拠しています。以下の書籍はScala言語の概念を構築した力作です。
JavaScriptに馴染みの深い方向けの書籍も何冊かあります。
他の言語にも優れたアイデアは山ほどありますので、尻込みせずにそうした言語からも学んでみましょう。関数型プログラミングについて知りたい方がたくさんいらっしゃれば、関数型プログラミングの良書リストの記事も書きたいと思いますので、Twitterの@keystonelemurまでぜひコメントをどうぞ!
皆さんもどうぞお楽しみください!
次回は「フロー制御」です。