Ruby のメタプログラミングで遊んでみよう

7.4K Views

January 29, 22

スライド概要

https://toyama-eng.connpass.com/event/233459/

シェア

埋め込む »CMSなどでJSが使えない場合

各ページのテキスト
1.

Ruby のメタプログラミングで 遊んでみよう Burikaigi2022

2.

自己紹介 名前:osyo Twitter : @pink_bangbi github : osyo-manga ブログ : Secret Garden(Instrumental) Rails エンジニア 好きな Ruby の機能は Refinements BuriKaigi2021 Ruby 2.0 から Ruby 3.0 を駆け足で振り返る RubyKaigi Takeout 2021 Use Macro all the time ~ マクロを使いまくろ ~

3.

Ruby のメタプログラミングで 遊んでみよう

4.

メタプログラミングとは…?

5.

メタプログラミングとは メタプログラミング - Wikipedia メタプログラミングとは、コンピュータプログラムそのものをデータのように扱えるプログラミング技 術を意味している。プログラムによって新たなプログラムを生成し、既存プログラムを閲覧・分析・修正・変形・拡張できることがメ タプログラミングである。それは走行中の自己プログラムに対してさえも可能にされている メタプログラミング - Wikipedia より 簡単に言うと『コードを書くためのコードを書く機能』のこと 一言で『メタプログラミング』と言ってもプログラミング言語によって指す機能が違ったりする ので一概にこれ!っていうのはむずかしい

6.
[beta]
Ruby のメタプログラミング

Ruby だと『動的に XXX する』ことを指すことが多い

1
2
3
4
5

class X
%w(homu mami mado).each { |name|
# define_method
define_method(name) { "name is #{name}" }
}

6
7
8
9

end

10
11
12
13
14
15

で動的にメソッドを定義する

x = X.new
# send

で動的にメソッドを呼び出す

p %w(homu mami mado).map { |name| x.send(name) }
# => ["name is homu", "name is mami", "name is mado"]

メソッドのメタ情報を取得する

#
p x.method(:homu)
# => #<Method: X#homu() /path/to/test.rb:4>

7.
[beta]
Ruby のメタプログラミング

Ruby だと『動的に XXX する』ことを指すことが多い

1
2
3
4
5

class X
%w(homu mami mado).each { |name|
# define_method
define_method(name) { "name is #{name}" }
}

6
7
8
9

end

10
11
12
13
14
15

で動的にメソッドを定義する

x = X.new
# send

で動的にメソッドを呼び出す

p %w(homu mami mado).map { |name| x.send(name) }
# => ["name is homu", "name is mami", "name is mado"]

メソッドのメタ情報を取得する

#
p x.method(:homu)
# => #<Method: X#homu() /path/to/test.rb:4>

直接記述する事は少ないが Rails を利用していれば恩恵を受けている
モデルでカラムと同じ名前のメソッドが暗黙的に定義されている
migrate やモデルの DSL の仕組み

8.

やりたいこと

9.
[beta]
ブロックの定義

Ruby ではブロック構文を用いてメソッドに処理を渡す
特にリスト操作などで多用される
を

に変換して大文字にする

1
2
3

# Symbol
String
p [:homu, :mami, :mado].map { |it| it.to_s.upcase }
#=> ["HOMU", "MAMI", "MADO"]

4
5
6
7
8

# 16
10
p ["0x12", "0x34", "0x56"].map { |it| it.to_i(16) }
# => [18, 52, 86]

9
10
11

進数の文字列を 進数の整数に変換する

降順でソートする

#
p ["homu", "mami", "an", "mado"].sort { |a, b| b <=> a }
# => ["mami", "mado", "homu", "an"]

10.
[beta]
ブロックの定義

Ruby ではブロック構文を用いてメソッドに処理を渡す
特にリスト操作などで多用される
を

に変換して大文字にする

1
2
3

# Symbol
String
p [:homu, :mami, :mado].map { |it| it.to_s.upcase }
#=> ["HOMU", "MAMI", "MADO"]

4
5
6
7
8

# 16
10
p ["0x12", "0x34", "0x56"].map { |it| it.to_i(16) }
# => [18, 52, 86]

9
10
11

進数の文字列を 進数の整数に変換する

降順でソートする

#
p ["homu", "mami", "an", "mado"].sort { |a, b| b <=> a }
# => ["mami", "mado", "homu", "an"]

しかし、こういう簡単なブロックで `|it|` の部分を毎回書くのは地味にめんどくさい…
もっと簡潔にブロックの処理を記述したい…

11.

こう書きたい `arg1` `arg2` … というオブジェクトを用いて抽象的に処理を書きたい を String に変換して大文字にする 1 # Symbol 2 3 4 5 6 p [:homu, :mami, :mado].map &arg1.to_s.upcase #=> ["HOMU", "MAMI", "MADO"] 7 8 9 # => [18, 52, 86] 10 11 進数の文字列を 進数の整数に変換する # 16 10 p ["0x12", "0x34", "0x56"].map &arg1.to_i(16) # 降順でソートする p ["homu", "mami", "an", "mado"].sort &arg2 <=> arg1 # => ["mami", "mado", "homu", "an"]

12.

こう書きたい `arg1` `arg2` … というオブジェクトを用いて抽象的に処理を書きたい を String に変換して大文字にする 1 # Symbol 2 3 4 5 6 p [:homu, :mami, :mado].map &arg1.to_s.upcase #=> ["HOMU", "MAMI", "MADO"] 7 8 9 # => [18, 52, 86] 10 11 進数の文字列を 進数の整数に変換する # 16 10 p ["0x12", "0x34", "0x56"].map &arg1.to_i(16) # 降順でソートする p ["homu", "mami", "an", "mado"].sort &arg2 <=> arg1 # => ["mami", "mado", "homu", "an"] あれ、 かっこよくない???

13.

Ruby のメタプログラミングを使って 遅延評価する機能を実装しよう

14.

0. やりたいこと

15.

0. やりたいこと `arg1` `arg2` オブジェクトに対してメソッドを遅延して呼び出せる仕組みを実装したい この `arg1` `arg2` のことをプレースホルダーと呼ぶ `#call` メソッドを呼び出すことで実際にメソッドを呼び出すようにする は評価すると引数の n番目を返すだけ メソッドで評価する 1 2 # arg1, arg2 # #call 3 4 5 pp arg1.call(1, 2) pp arg2.call(1, 2) 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # => 1 # => 2 プレースホルダーを経由して to_s(16) を遅延して呼び出すオブジェクトを生成する # to_s16 = arg1.to_s(16) すると # #call arg1 pp to_s16.call(42) が 42 に置き換わり結果的に 42.to_s(16) が実行される # => "2a" 遅延するメソッドの引数自体にも arg2 を渡せるようにする # to_s = arg1.to_s(arg2) が実行される # 42.to_s(2) pp to_s16.call(42, 2) # => "101010" 演算子も呼び出せるようにする # pp (arg1 + arg2).call(2, 3) # => 5

16.

1. 遅延評価するクラスを定義する

17.

遅延評価するクラスを定義する 遅延評価のベースとなる `Lazy` クラスを定義する ブロックを受け取って `#call` で呼び出すだけ `#call` を呼び出すことを『評価』と呼ぶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Lazy def initialize(&block) @block = block end def call @block.call end end homu = Lazy.new { "homu" } に渡したブロックを呼び出す # Lazy.new pp homu.call # => "homu"

18.

2. `arg1` `arg2` … を定義する

19.

`arg1` `arg2` … を定義する に引数を渡せるようにする `arg1` `arg2` で引数を返すように評価する `#call` この `arg1` `arg2` のことをプレースホルダーと呼ぶ 1 2 class Lazy # ... 3 4 5 6 7 # call def call(*args) @block.call(*args) end end 8 9 10 11 12 13 14 15 16 17 の引数をそのままブロックの引数に渡す # arg1 arg1 = arg2 = arg3 = は受け取った引数を返すだけ ~ 3 Lazy.new { |*args| args[0] } Lazy.new { |*args| args[1] } Lazy.new { |*args| args[2] } を評価するとその引数の 番目を返す # arg1 ~ 3 n pp arg1.call("homu", "mami", "mado") pp arg2.call("homu", "mami", "mado") pp arg3.call("homu", "mami", "mado") # => "homu" # => "mami" # => "mado"

20.

3. 遅延してメソッドを呼び出す

21.
[beta]
3. 遅延してメソッドを呼び出す

評価時に呼び出されるメソッドを `__lazy_send__` で定義する
`__lazy_send__`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

が Lazy オブジェクトを返すことで再帰的に遅延処理が行われる

class Lazy
# ...

した結果に対して
を呼び出すような Lazy オブジェクトを返す
これにより
の戻り値に対して評価すると実際にメソッドが呼び出される
のブロックで受け取った引数はそのまま call にフォワードする

# call
name
#
__lazy_send__
def __lazy_send__(name)
# Lazy.new

::Lazy.new { |*args| call(*args).send(name) }
end
end
homu = Lazy.new { "homu" }

となるような Lazy オブジェクトを返す

# Lazy.new { homu.call.upcase }
homu_upcase = homu.__lazy_send__(:upcase)
# Lazy.new { homu.call.upcase }.call
pp homu_upcase.call
# => "HOMU"

更にネストして呼び出す事もできる

が呼ばれる

#
# Lazy.new { Lazy.new { homu.call.upcase }.call.cahrs }.call
pp homu_upcase.__lazy_send__(:chars).call
# => ["H", "O", "M", "U"]

が呼ばれる

22.

4. `__lazy_send__` に引数を渡す

23.
[beta]
4. `__lazy_send__` に引数を渡す

メソッド名, そのメソッドの引数)` を渡せるようにする

`__lazy_send__(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class Lazy
# ...
def __lazy_send__(name, *args_)
::Lazy.new { |*args|
# __lazy_send__
call(*args).send(name, *args_)

で受け取った引数をそのまま呼び出すメソッドの引数に渡す

}
end
end
mami = Lazy.new { "mami" }
# Lazy.new { Lazy.new { "mami" }.call.send(:count, "m") }.call
pp mami.__lazy_send__(:count, "m").call # => 2

を利用するとこういう事もできる
と同じ意味

# arg1
# "mamimami".count("m")
pp arg1.__lazy_send__(:count, "m").call("mamimami")

# => 4

と同じ意味

24.

5. 引数を評価してからメソッドを呼ぶ

25.

5. 引数を評価してからメソッドを呼ぶ `__lazy_send__` の引数に `Lazy` オブジェクトを渡せるようにする 評価する時にその引数を評価してからメソッドの引数として渡す 1 2 3 4 class Lazy # ... def __lazy_send__(name, *args_) ::Lazy.new { |*args| で受け取った引数を評価してから遅延評価するメソッドの引数に渡す 評価するのは オブジェクトのみにする 5 6 7 8 # __lazy_send__ applyed_args = args_.map { |it| # Lazy ::Lazy === it ? it.call(*args) : it 9 10 11 12 13 14 15 16 17 18 19 20 21 } call(*args).send(name, *applyed_args) } end end count = Lazy.new { [1, 2, 1, 2, 2] } の引数を後から渡せるようにする の第一引数が に置き換わる # #count # #call arg1 count = count.__lazy_send__(:count, arg1) # [1, 2, 1, 2, 2].count(1) pp count.call(1) # => 2 と同じ意味

26.

6. `method_missing` を利用する

27.
[beta]
6. `method_missing` を利用する

を利用すると未定義のメソッド呼び出しに処理をフックできる
これを利用して `method_missing` 経由で `__lazy_send__` を呼び出す
`method_missing`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class Lazy
# ...

定義されてないメソッドが呼ばれるとこのメソッドが呼び出される
これを利用して定義されていないメソッドは遅延評価されるようにする

#
#
def method_missing(name, *args)
__lazy_send__(name, *args)
end
end
mami = Lazy.new { "mami" }

というメソッドは未定義なので

# upcase
mami.method_missing(:upcase)
#
mami.__lazy_send__(:upcase)
pp mami.upcase.call
# => "MAMI"

結果的に

の戻り値も

# mami.upcase
Lazy
pp mami.upcase.chars.call
# => ["M", "A", "M", "I"]

と同じ意味になる

が呼び出される

オブジェクトなのでチェーンして呼び出すことができる

28.

7. 暗黙的に定義されている メソッドも呼び出せるようにする

29.

7. 暗黙的に定義されているメソッドも呼び出せるようにする や `#to_s` などは暗黙的に定義されているメソッドになる なので `arg1.class` を呼び出しても `#class` メソッドは既に定義されているので `method_missing` は呼ばれない `BasicObject` を継承する事でこの問題を対処することができる `#class` `BasicObject` 1 2 3 4 5 6 7 8 9 10 11 12 13 を継承する事で最小限のメソッドのみが定義されるようになる を継承する事で最小限のメソッドのみが Lazy クラスに定義される # BasicObject class Lazy < BasicObject # ... end や などといった暗黙的に定義されているメソッドも 経由で遅延評価できるようになる # to_s class # method_missing to_s = arg1.to_s(arg2) p to_s.call(42, 2) # => "101010" p to_s.call(42, 16) # => "2a" to_class = arg1.class p to_class.call(42) # => Integer

30.

8. ブロック引数に渡せるようにする

31.
[beta]
8. ブロック引数に渡せるようにする
`#to_proc`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

を定義することでブロック引数として渡される

class Lazy < BasicObject
# ...
# &
to_proc
#
Proc
def to_proc
::Proc.new { |*args| call(*args) }

で渡した時に
ここで返した

メソッドが呼ばれる
オブジェクトがブロックとして呼び出される

end
end
# [1, 2, 3, 4, 5].map { |it| (arg1 + arg1).call(it) }
pp [1, 2, 3, 4, 5].map &arg1 + arg1
# => [2, 4, 6, 8, 10]

と同じ意味

# [1, 2, 3, 4, 5].map { |it| arg1.to_s.length.call(it) }
p [:foo, :hoge, :bar, :fuga].select &arg1.to_s.length
# => [:foo, :hoge, :bar, :fuga]

と同じ意味

32.

9. オブジェクトを Lazy 化する

33.
[beta]
9. オブジェクトを Lazy 化する
`#to_lazy`

で任意のオブジェクトを `Lazy` オブジェクトに変換する

これを利用すると任意のオブジェクトのメソッド呼び出しを遅延評価する事ができる

1
2
3
4

class Lazy < BasicObject
# ...
end

5
6
7
8

class Object
def to_lazy
Lazy.new { self }
end

9
10
11
12
13
14
15
16
17
18
19
20
21

end

に対して任意のメソッドを遅延評価する
評価すると
が呼び出される

# 42
#
42.to_s(arg1)
_42_to_s = 42.to_lazy.to_s(arg1)
pp _42_to_s.call(2)
# => "101010"
pp _42_to_s.call(16) # => "2a"

ブロックに渡すこともできる

#
result = []
(1..10).each &result.to_lazy << arg1 + arg1
pp result
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

34.

完成!!!

35.

最終的な実装コード 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Lazy < BasicObject def initialize(&block) @block = block end def call(*args) @block.call(*args) end def __lazy_send__(name, *args_) ::Lazy.new { |*args| applyed_args = args_.map { |it| ::Lazy === it ? it.call(*args) : it } call(*args).send(name, *applyed_args) } end def method_missing(name, *args) __lazy_send__(name, *args) end def to_proc ::Proc.new { |*args| call(*args) } end end class Object def to_lazy Lazy.new { self } end end

36.
[beta]
使用例
1

p (1..5).map &arg1 + 3

2
3
4
5
6

# => [4, 5, 6, 7, 8]
p (1..5).inject &arg1 + arg2
# => 15

7
8
9
10

pp [{name: :homu}, {name: :mami}].map &arg1[:name]
# => [:homu, :mami]

11
12
13
14
15
16
17
18
19
20

(1..10).each &result.to_lazy << arg1 + arg1
pp result
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

result = []

1.to_lazy.upto(arg1, &to_lazy.puts("number is ".to_lazy + arg1.to_s)).call(5)
# => number is 1
#
number is 2
#
number is 3
#
number is 4
#
number is 5

37.

注意点

38.
[beta]
注意点
`&&`

や `||` などの演算子は Ruby では構文になるので今回の実装では使用できない

や `||` はメソッドではないので `method_missing` でメソッド呼び出しをフックしたり `send` で
呼び出すことができない
`&&`

や

はメソッドなので次のように記述できる

1
2
3
4
5

# +
[]
pp (arg1 + arg2).call(1, 2) # => 3
pp (arg1[:name]).call({ name: "homu" })

6

pp (arg1 && arg2)

# &&

# => "homu"

はメソッドではないのでこういう書き方はできない…

39.

まとめ

40.

まとめ

41.

まとめ 簡単に Ruby のメタプログラミングを使った技術を紹介してみました 簡単な実装でこれだけ抽象的なコードをかける Ruby ってすごくない?

42.

まとめ 簡単に Ruby のメタプログラミングを使った技術を紹介してみました 簡単な実装でこれだけ抽象的なコードをかける Ruby ってすごくない? 普段あんまりメタプロを書く機会はないがいろんなところで間接的に恩恵を受けている RUby で DSL っぽい構文がかけるのは Ruby のメタプロが強力だから

43.

まとめ 簡単に Ruby のメタプログラミングを使った技術を紹介してみました 簡単な実装でこれだけ抽象的なコードをかける Ruby ってすごくない? 普段あんまりメタプロを書く機会はないがいろんなところで間接的に恩恵を受けている RUby で DSL っぽい構文がかけるのは Ruby のメタプロが強力だから 話を聞いてメタプログラミングが面白そうと思った方はぜひぜひメタプロについて調べて! 今回話したのは Ruby のメタプロのほんの一部 他には instance_eval や binding, Refinements, AST の話なんかも面白い

44.

まとめ 簡単に Ruby のメタプログラミングを使った技術を紹介してみました 簡単な実装でこれだけ抽象的なコードをかける Ruby ってすごくない? 普段あんまりメタプロを書く機会はないがいろんなところで間接的に恩恵を受けている RUby で DSL っぽい構文がかけるのは Ruby のメタプロが強力だから 話を聞いてメタプログラミングが面白そうと思った方はぜひぜひメタプロについて調べて! 今回話したのは Ruby のメタプロのほんの一部 他には instance_eval や binding, Refinements, AST の話なんかも面白い gem 化してあるので気になる人はそっちも試してもらえると! iolite 実は以前から存在していたけど今回の登壇にあたって実装を一新

45.

ちなみに…

46.
[beta]
Numbered parameters

Ruby 2.7 で Numbered parameters という機能が追加された
`_1` `_2` という名前でブロックの引数を参照する事ができる

1
2
3
4
5
6
7
8
9
10

pp [:foo, :hoge, :bar, :fuga].select { _1.to_s.length > 3 }
# => [:hoge, :fuga]
pp [2, 5, 1, 3, 4].sort { _2 <=> _1 }
# => [5, 4, 3, 2, 1]
result = []
(1..10).each { result << _1 + _1 }
pp result
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

47.

参考 【Ruby Advent Calendar 2016 1日目】 Ruby でブロックを簡潔に定義する - Secret Garden(Instrumental)