Rubyで関数型プログラミング#2: クロージャ(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

訳注: 原文では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]

ちょっと待った、その値をどこに記憶してたの?

この関数は、a3であるということを覚えていたのです。いったいどうやったのかおわかりでしょうか?

クロージャとは、それが作成されたコンテキストを記憶している関数のことです。

つまり、返された関数のそのまた内部では、その周囲にあるものすべてが絶対的に公正なゲームとして把握されるのです。

カウントするクロージャ

カウンタについて考えてみましょう。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]

直ちにこの配列が返りますが、truefalseを返すだけの普通のマッチャーとどこが違うのかちょっと覗いてみましょう。

# 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までぜひコメントをどうぞ!

皆さんもどうぞお楽しみください!

次回は「フロー制御」です。

関連記事

Rubyで関数型プログラミング#1: ステート(翻訳)

Ruby: Enumerableを`reduce`で徹底理解する#1 基本編(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ