LoginSignup
28
26

More than 5 years have passed since last update.

Ruby でブロックを簡潔に定義する

Last updated at Posted at 2017-05-11

ブログ記事の転載です。

[Ruby といえば]

さて、表題のブロックですが、Ruby といえばブロック、ブロックといえば Ruby というぐらい Ruby という言語を語る上では切っては切れない機能の一つになります。

特に #map#select などと言ったリスト操作とブロックの組み合わせは Ruby を書く上での醍醐味と言っても過言ではないと思います。

[ブロックの不満点]

そんな Ruby では多用されるブロックですが、コードを書く上で少し不満点があります。

例えば次のようなコードをみてみましょう。

["foo", "bar", "baz"].map { |it| it.upcase }
#=> ["FOO", "BAR", "BAZ"]

コード自体は特に問題はないのですが、ブロックで引数を受け取る関係上 it を2回も記述する必要があります。

こちらでも書かれていますが、Ruby を書いていると『こういう簡単なブロック』を書くことが多くあり無駄に感じます。

なにより簡潔ではありません。

あと単純にブロック自体書くのがめんどくさいです。

[Symbol#to_proc を利用する]

ここで Ruby に精通する人はピンと来ると思いますが、上記のブロックのように『レシーバのメソッドを呼び出すだけ』のブロックは次のように Symbol#to_proc を利用して書き換えることが出来ます。

# 第一引数の #upcase を呼び出すブロックとして処理される
["foo", "bar", "baz"].map &:upcase
#=> ["FOO", "BAR", "BAZ"]

おお、これなら簡潔に記述することができるじゃないか!

と、思いますが、残念ながらこのテクニックは次のような場合に利用出来ません。

# 引数メソッドに引数を渡す場合
["0x12", "0x34", "0x56"].map { |it| it.to_i(16) }
# => [18, 52, 86]

# 呼び出すメソッドをチェーンする場合
[:foo, :bar, :baz].map { |it| it.to_s.upcase }
# => ["FOO",  "BAR",  "BAZ"]

この場合は Symbol#to_proc を使用して簡潔に定義することはできませんね。

NOTE: instance method Symbol#to_proc (Ruby 2.3.0)

生成される Proc オブジェクトを呼びだす(Proc#call)と、 Proc#callの第一引数をレシーバとして、 self という名前のメソッドを 残りの引数を渡して呼びだします。

[lambda_driver ではどうか]

ブロックを簡潔に定義するといえば lambda_driver という有名な gem が存在します。

lambda_driver についてはこちらに詳しく書かれています。

lambda_driver は Symbol と複数の記号を組み合わせることで関数合成などを抽象化して定義することができます。

# [:foo, :bar, :baz].map { |it| it.to_s.upcase }
[:foo, :bar, :baz].map(&:to_s >> :upcase )
# => ["FOO",  "BAR",  "BAZ"]

これは確かに便利だとは思います。

しかし、全く知らない人から見ると式が抽象化され過ぎてて難読化するのがやや気になります。

例えば、次のようなコードは記号が多用されており、パッと見なにをやっているのかわかりづらいです。

# [:foo, :hoge, :bar, :fuga].select{|s| s.to_s.length > 3}
[:foo, :hoge, :bar, :fuga].select(&:to_s >> :length >> 3._(:<))
# => [:hoge, :fuga]

以上を踏まえて、Ruby で簡潔にブロックを定義する方法を考えていきたいと思います。

[Ruby 以外の言語に目を向ける]

もっと簡潔に定義できる方法はないか考えてみましょう。

こういう時は Ruby 以外の言語を参考にしてみるのもひとつの手です。

そんなわけで今回は C++ にちょっと目を向けてみます。

Ruby を書いてる人にはちょっと馴染みがないかも知れませんが、C++ に Boost というメジャーなライブラリがあります。

この Boost はいくつかのライブラリが 1つにまとまったものになるのですが、その中に Boost.Lambda というライブラリがあります。

Boost.Lambda とはその名の通り『C++ で簡潔にラムダ式を定義する』ために考えだされたライブラリです。

使い方自体はそんなに難しくなくて、簡単に説明するとプレースホルダというものを引数に置き換えるような形でラムダ式を定義します。

// _1 や _2 などは該当する引数の順番に置き換えられる
auto plus3 = _1 + 3;
plus3(2); // => 5

auto sum = _1 + _2 + _3;
sum(1, 2, 3); // => 6

auto is_even = _1 % 2 == 0;
is_even(2);  // true
is_even(-3); // false

NOTE: 無名関数 - boostjp

C++ をよく知らないという人でもなんとなく使い方のイメージはできると思います。

と、いうことで Ruby でもこんな感じでブロックを定義してみよう!というのが今回の本題です。

[プレースホルダをつくる]

さて、まずはプレースホルダを定義していきたいと思います。

今回定義するプレースホルダは以下のような性質をもたせます。

# n 番目の引数の値を返す
_1.(1, 2, 3)
# => 1
_2.(1, 2, 3)
# => 2
_3.(1, 2, 3)
# => 3

これを以下のように実装していきます。

class Lazy
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end
end


def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

# n 番目の引数の値を返す
_1.(1, 2, 3)
# => 1
_2.(1, 2, 3)
# => 2
_3.(1, 2, 3)
# => 3

Lazy#call が呼ばれたら Lazy.new 時に渡しがブロックが呼び出される構造になります。

今回は Proc クラスを拡張したくなかったので Lazy というクラスで薄くラップして拡張していきます。

[メソッドの呼び出しを遅延評価する]

次にプレースホルダから遅延してメソッドを呼びだせるように #__send_delay__ というメソッドを定義します。
イメージとしては以下のような感じです。

# 第一引数に対して #upcase を呼び出す
to_upcase = _1.__send_delay__(:upcase)

# このタイミングで引数に対して #upcase が呼ばれる
to_upcase.("homu")
# => "HOMU"

to_hex = _1.__send_delay__(:to_s, 16)
to_hex.(42)
# => "2a"

このように『あとで #call が呼び出された時』に指定したメソッドが評価されるようにします。

class Lazy
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end

    def __send_delay__ name, *args, &block
        # Lazy オブジェクトを返す
        Lazy.new { |*args_, &block_|
            call(*args_, &block_).__send__(name, *args, &block)
        }
    end
end

def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

# 第一引数に対して #upcase を呼び出す
to_upcase = _1.__send_delay__(:upcase)

to_upcase.("homu")
# => "HOMU"

to_hex = _1.__send_delay__(:to_s, 16)
to_hex.(42)
# => "2a"

ここで肝なのが『#__send_delay__Lazy オブジェクトを返す』ところです。

これにより『メソッドをチェーンして』遅延評価することが出来ます。

to_upcase_hex = _1.__send_delay__(:to_s, 16).__send_delay__(:upcase)
to_upcase_hex.(20161201)
# => "133A2B1"

いい感じです。
ただ、毎回 #__send_delay__ を呼び出すのはやや冗長ですね。

[method_missing を経由して #__send_delay__ を呼び出す]

さて、こんな時はメタプログラミングの出番です。

Ruby には『レシーバに存在しないメソッドを呼び出した時に処理をフックする』ことが出来ます。
この時に定義するメソッドが #method_missing になります。
これを利用することで #__send_delay__ を経由することなく、自然にメソッドの遅延処理を定義することが出来ます。

NOTE: instance method BasicObject#method_missing (Ruby 2.3.0)

呼びだされたメソッドが定義されていなかった時、Rubyインタプリタがこのメソッド を呼び出します。

class Lazy
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end

    def __send_delay__ name, *args, &block
        ::Lazy.new { |*args_, &block_|
            call(*args_, &block_).__send__(name, *args, &block)
        }
    end

    # method_missing 経由で #__send_delay__ を呼び出す
    def method_missing name, *args, &block
        __send_delay__ name, *args, &block
    end
end

def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

# プレースホルダをレシーバとして直接メソッドを呼び出すことができる
to_upcase = _1.upcase
to_upcase.("homu")
# => "HOMU"

to_upcase_hex = _1.to_s(16).upcase
to_upcase_hex.(20161201)
# => "133A2B1"

こんな感じで『プレースホルダをレシーバとして直接メソッドを呼び出す』ことができるようになりました。
かなり自然にメソッドの呼び出しを書けるようになりましたね。

そして Ruby では『演算子もメソッド』であるため、以下のように演算子も定義することも出来ます。

# _1.+(3) と同等
plus3 = _1 + 3
plus3.(5)
# => 8

NOTE: クラス/メソッドの定義 (Ruby 2.3.0)

演算子式において、「再定義可能な演算子」に分類された演算子の実装 はメソッドなので、定義することが可能です。

これは素晴らしい。

ちなみに #method_missing を利用したかったのが『Proc クラスを拡張しなかった』理由の一つです。

[Object で定義されているメソッドは #method_missing で呼び出されない]

さて、これにより『Lazy クラスで定義されていないメソッド』は #method_missing を利用することで簡潔に呼び出す事ができるようになりました。
しかし、 通常 Ruby ではクラスを定義した場合は内部で Object クラスが自動的に継承されるため、デフォルトでも多数のメソッドが定義されています。

NOTE: class Object (Ruby 2.3.0)

全てのクラスのスーパークラス。 オブジェクトの一般的な振舞いを定義します。

ですので次のように『Object クラスで定義されてるメソッド』は #method_missing 経由で呼び出すことは出来ません。

# 第一引数の #class を遅延評価して欲しいが Lazy#class が呼び出されてしまう
_1.class
# => Lazy

[BasicObject を継承して空のクラスを定義する]

今回は Object クラスで定義されてる『多数のメソッドも遅延評価したい』ので Lazy オブジェクトでは定義してほしくありません。

このような場合は BasicObject を継承することで解決することが出来ます。

NOTE: class BasicObject (Ruby 2.3.0)

Object クラスは様々な便利なメソッドや Kernel から受け継いだ関数的メソッド を多数有しています。 これに対して、 BasicObject クラスはオブジェクトの同一性を識別したりメソッドを呼んだりする 最低限の機能の他は一切の機能を持っていません。

明示的に BasicObject クラスを継承することで、最低限のメソッドのみが定義されているクラスを定義することが出来ます。

# BasicObject を継承することで最低限のメソッドのみ定義するようにする
class Lazy < BasicObject
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end

    def __send_delay__ name, *args, &block
        ::Lazy.new { |*args_, &block_|
            call(*args_, &block_).__send__(name, *args, &block)
        }
    end

    def method_missing name, *args, &block
        __send_delay__ name, *args, &block
    end

    # BasicObject では #== が定義されているので、Lazy の性質に合わせて再定義する
    def == *args, &block
        __send_delay__ :==, *args, &block
    end

    def ! *args, &block
        __send_delay__ :!, *args, &block
    end
end

def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

to_class = _1.class

to_class.("homu")
# => String
to_class.([])
# => Array

また、この時に注意するのが #== など一部のメソッドは BasicObject でも定義されているので用途に合わせて再定義する必要があります。

NOTE: instance method BasicObject#== (Ruby 2.3.0)

このメソッドは各クラスの性質に合わせて、サブクラスで再定義するべきです。 多くの場合、オブジェクトの内容が等しければ真を返すように (同値性を判定するように) 再定義 することが期待されています。

このあたりのテクニックは #method_missing を使用する際は覚えておくとよいです。

[引数にプレースホルダを渡す]

さて、次に『遅延評価するメソッドの引数にプレースホルダを渡す』ことを考えてみましょう。

以下のようにメソッドの引数にもプレースホルダを渡したいですよね。

# #to_s の引数は第二引数で受け取るようにする
to_s = _1.to_s(_2)

to_s.(42, 2)
# => "101010"

ですので、Lazy オブジェクトを『再帰的に評価する』ように修正します。

class Lazy < BasicObject
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end

    def __send_delay__ name, *args, &block
        ::Lazy.new { |*args_, &block_|
            # 式を組み立てるときに渡された引数が Lazy オブジェクトであれば評価する
            apples = args.map { |it| ::Lazy === it ? it.call(*args_, &block_) : it }
            call(*args_, &block_).__send__(name, *apples, &block)
        }
    end

    def method_missing name, *args, &block
        __send_delay__ name, *args, &block
    end

    def == *args, &block
        __send_delay__ :==, *args, &block
    end

    def ! *args, &block
        __send_delay__ :!, *args, &block
    end
end

def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

to_s = _1.to_s(_2)

to_s.(42, 2)
# => "101010"
to_s.(42, 16)
# => "2a"

こんな感じです。

ちょっとわかりづらいかもしれませんが、要は『#to_s の引数が Lazy だったら先に評価する』ようにしてるだけですね。

これにより次のような演算子を使った式も簡潔に定義できます。

expr = _1 + _2 * _3
expr.(1, 2, 3)
# => 7

実にわかりやすいですね。

[プレースホルダの弱点]

この便利そうなプレースホルダですが、1つだけ弱点があります。

それは『プレースホルダをレシーバとして呼び出す』必要があることです。

どういうことかというと例えば、

_1 + "homu"

という風に式を記述することは出来ますが、

"homu" + _1

という風に左辺値にプレースホルダ以外を置いた場合、Lazy オブジェクトとして定義することは出来ません。

なぜなら #+String クラスで定義されており、Lazy クラスは干渉出来ないからです。

これを解決したい場合、Ruby では String#+ を拡張するという手段はあるんですが、型のない言語で『特定のクラスに対する処理』というのはあまりやりたくありません。

[Object#to_lazy を定義する]

そこで今回は Object#to_lazy というヘルパメソッドを定義して解決したいと思います。

class Lazy < BasicObject
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end

    def __send_delay__ name, *args, &block
        ::Lazy.new { |*args_, &block_|
            apples = args.map { |it| ::Lazy === it ? it.call(*args_, &block_) : it }
            call(*args_, &block_).__send__(name, *apples, &block)
        }
    end

    def method_missing name, *args, &block
        __send_delay__ name, *args, &block
    end

    def == *args, &block
        __send_delay__ :==, *args, &block
    end

    def ! *args, &block
        __send_delay__ :!, *args, &block
    end
end

def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

class Object
    # 自身を Lazy のオブジェクトとして返す
    def to_lazy
        Lazy.new { self }
    end
end

# 任意のオブジェクトのメソッドを遅延評価できる
homu_plus = "homu".to_lazy + _1
p homu_plus.("mami")
# => "homumami"

ary = [1, 2, 3]
add = ary.to_lazy << _1
add.(1)
add.(2)
add.(3)
p ary
# => [1, 2, 3, 1, 2, 3]

Object#to_lazy は単に『自身を返す Lazy オブジェクト』を返すだけです。

これで任意のメソッドを遅延評価することができるようになりました。

[ブロックに渡せるように #to_proc を定義する]

最後にブロックに渡せるように #to_proc を定義します。

Ruby では #to_proc を定義することでそのオブジェクトをブロックに渡せるようになります。

class Lazy < BasicObject
    def initialize &block
        @block = block
    end

    def call *args, &block
        @block.call *args, &block
    end

    def __send_delay__ name, *args, &block
        ::Lazy.new { |*args_, &block_|
            apples = args.map { |it| ::Lazy === it ? it.call(*args_, &block_) : it }
            call(*args_, &block_).__send__(name, *apples, &block)
        }
    end

    def method_missing name, *args, &block
        __send_delay__ name, *args, &block
    end

    def == *args, &block
        __send_delay__ :==, *args, &block
    end

    def ! *args, &block
        __send_delay__ :!, *args, &block
    end

    # #to_proc を定義することにより Lazy オブジェクトをブロック引数として渡せる
    def to_proc
        ::Proc.new { |*args, &block| call *args, &block }
    end
end

def placeholder index
    Lazy.new { |*args| args[index] }
end

_1 = placeholder 0
_2 = placeholder 1
_3 = placeholder 2

class Object
    def to_lazy
        Lazy.new { self }
    end
end


# ブロックに直接 Lazy オブジェクトを渡せる
p (1..10).map &_1.to_s(2)
# => ["1", "10", "11", "100", "101", "110", "111", "1000", "1001", "1010"]

# こんな感じで定義することもできる
evens = _1.select(&_1 % 2 == 0)
p evens.(1..10)
# => [2, 4, 6, 8, 10]

# Kernel のメソッドも遅延評価できる
(1..10).each &to_lazy.puts(_1)

これで一応完成になります。

[他のコードと比較してみる]

lambda_driver などと比較してみるとこんな感じです。

# default
[:foo, :bar, :baz].map { |s| s.to_s.upcase }
# or
[:foo, :bar, :baz].map(&:to_s).map(&:upcase)
# => ["FOO",  "BAR",  "BAZ"]

# lambda_driver
[:foo, :bar, :baz].map(&:to_s >> :upcase)

# placeholder
[:foo, :bar, :baz].map(&_1.to_s.upcase)


# default
[:foo, :hoge, :bar, :fuga].select { |s| s.to_s.length > 3 }
# => [:hoge, :fuga]

# lambda_driver
[:foo, :hoge, :bar, :fuga].select(&:to_s >> :length >> 3._(:<))

# placeholder
[:foo, :hoge, :bar, :fuga].select(&_1.to_s.length > 3)


# default
def twice n
    n + n
end

puts twice(65).to_s.length

# lambda_driver
_.twice >> :to_s >> :length >> _.puts < 65

# lambda_driver に合わせて to_lazy を短くする
alias _ to_lazy
_.puts(_.twice(_1).to_s.length).(65)

プレースホルダを使用することでかなり簡潔にブロックを定義できるようになったと思います。

[唯一の弱点]

さて、ほぼやりたいことはできたんですが、1つだけどうしても出来ないことがあります。

それは && 演算子と || 演算子の遅延評価です。

例えば、

cond = _1 >= 5 && _1 <= 10

みたいな式を定義したい場合、これでは上手く動作しません。

これは +[] 演算子などはメソッドとして扱われますが、&&|| は Ruby 本体の制御構造として扱われる為です。

Ruby では &&|| などの演算子の再定義も禁止されています。

つまり上記の場合は && の左辺値(5 <= _1) が真になるため、cond には 5 <= _1 が代入されます。

この性質は Ruby の言語仕様上の問題なので現状はどうすることも出来ません。

NOTE: 演算子式 (Ruby 2.3.0)

[まとめ]

そんな感じでいい感じにブロックを簡潔に定義できるようになりました。

なんか1日目からガッツリと書いてしまって重くないか心配です。

最初にも書きましたが Ruby ではどうしてもブロックを多用する言語なので、そのブロックが簡潔に定義できることはかなり便利です。

またこういう『他の言語から影響を受けて実装する』というのはなんか他の言語のいいとこ取りをしている感じがしているのでいいですね、どんどんパクっていきましょう。
特に Ruby は柔軟な言語なのでメタプログラミングでサクッと実現できることが多いので書いていて楽しいです。

Ruby といえば Rails というイメージですが、Ruby 単体でも十分に面白い言語なのでみんな使ってみるといいです。Ruby = Rails と思ってるやつしねばいいのに

もうすぐ Ruby 2.4 もリリースらしいですし、将来的に Ruby に型システムを導入するみたいな話出ていますし、今後は他の言語と比べてどういう風に進化していくのかが楽しみですね。

ちなみに今回実装したコードは ioliteというライブラリで gem 化してます。

使い方などは基本的に同じですが、つくったのが結構前なのでだいぶレガシーなコードで味わい深いです。

興味がある人は example やここなどみてみると面白いです。

そんな感じで2日目に続きます。

28
26
6

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
28
26