Ruby

おもしろメタプログラミング/4章

More than 1 year has passed since last update.

4章 水曜日: ブロック

水曜日のアジェンダ

  1. スコープゲートとは
  2. ブロックの基本
  3. ブロックはクロージャ
  4. 呼び出し可能オブジェクト
  5. 実践問題「上長からの逃走」

1. スコープゲートとは

  • そもそもスコープって?
    現在自分が使える、変数の範囲(怪しめ)
  • そのスコープを切り替えて、新しいスコープをオープンする3つの場所...スコープゲート
    • クラス定義: class
    • モジュール定義: module
    • メソッド: def

Q1. 2つのスコープゲート

v1 = 1
local_variables # => [:v1]

class MyClass
  v2 = 2
  local_variables # => [?]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables # => [?]
end

obj = MyClass.new
obj.my_method # => [?]
local_variables # => [?]

A1. 2つのスコープゲート

v1 = 1
local_variables # => [:v1]

class MyClass # classの入口
  v2 = 2
  local_variables # => [:v2]
  def my_method # defの入口
    v3 = 3
    local_variables # => [:v3]
  end # defの出口
  local_variables # => [:v2]
end # classの出口

obj = MyClass.new
obj.my_method # => [:v3]
local_variables # => [:v1, obj]

ここで出てくる、
「もしスコープゲートを超えて変数を共有したかったらどうするんだろう??」という問題を解決するのがブロック! -> 2.へ

2. ブロックの基本

問題を解消する前に、一旦ブロックの基本を抑える。
- ブロックは波かっこ or do...endキーワードで定義できる
- 定義できるのはメソッドを呼び出すときだけ
- メソッドはyieldキーワードを使ってブロックをコールバックする
- 呼び出す際は、メソッドと同じく引数を渡せる。そしてこちらも同じく最終行を評価した結果を返す

Q2.

def men_and_women(a, b)
  yield(a, b) + a
end

men_and_women('男女', '男男女') { |x, y| x + y } # => '?'

基本がわかったところで、
「もしスコープゲートを超えて変数を共有したかったらどうするんだろう??」という問題をブロックが解決するのを見に行こう! -> 3.へ

3. ブロックはクロージャ

  • ブロックは束縛(ローカル変数、 selfなどの環境)を一緒についれていってくれるクロージャである。
  • ブロックは定義された時点で、その場所にある束縛を取得して連れて行く。

Q3. クロージャの働き

class MyClass
  def my_method
    in_method = 'in_method'
    yield
  end
end

obj = MyClass.new
obj.my_method do
  self # => ?
  in_method # => 存在する?
end

A3. クロージャの働き

self # => main
in_method # => Error! 'undefined local variable or method'

これでようやく、スコープゲートを超えて束縛を渡すことができる!!

- class, def, moduleなどのスコープゲートを、ブロックを使ったメソッド呼び出しに置き換えると束縛を共有できる
- これの魔術をフラットスコープと呼ぶ

Q4. 上の2つを踏まえて下のコードでフラットスコープを完成させよう

my_var = '成功'

class MyClass
  # my_varをここに表示したい...

  def my_method
    # ... ここにも表示したい
  end
end

A4.

my_var = '成功'

MyClass = Class.new do
  puts my_var

  define_method :my_method do
    puts my_var
  end
end

4. 呼び出し可能オブジェクト

ブロックはオブジェクトではない。しかし、一度保管し後から実行するためにはオブジェクトであることが必要。
メソッド内ではなく、ブロック内にコードを保管するためにはそれをオブジェクト化する。

  • オブジェクト生成方法
    1. Proc.newにブロックを渡す
    2. procに渡す
    3. lambdaに渡す
    4. 矢印lambda
    5. メソッド引数&で受け取ってProcを返す
  • あとで評価するにはProc#callを呼び出す
inc = Proc.new { |n| n + 1 } # => 1.
inc = proc { |n| n + 1 } # => 2.
inc = lambda { |n| n + 1 } # => 3.
inc = ->(n) { n + 1 } # => 4.
def my_method(&my_proc) # => 5.
    my_proc
end
inc = my_method { |n| n + 1 }
....
inc.call(2) # => 呼び出しは共通

&修飾

ブロックはメソッドに渡す無名引数のようなもの。普通はメソッド内でyieldを使って実行する。
しかし、以下の場合はブロックに名前が必要になる。それには、メソッドの引数列の最後において、名前の前に&をつける。
* 他のメソッドにブロックを渡したい時
* ブロックをProcに変換したい時
&でブロックを受け取ればProcオブジェクトに

def my_method(&the_proc)
    the_proc
end
p = my_method {|name| "hello, #{name}"}
p.class #=> Proc
p.call("AIchan") # => "hello, AIchan"

&でprocを渡したらブロックに変換して渡す

def my_method
    yield
end
my_proc =  Proc.new {'AIchan'}
my_method(&my_proc) # => "hello, AIchan"

Procとlambda

基本的にはlambdaを使う(項数に厳しくreturnを呼ぶと単に終了してくれる。メソッドっぽい)

  • lambdaで作ったProcオブジェクト・・・lambda
  • それ以外のProcオブジェクト・・・Proc

違いは2点
- returnの挙動
- 引数チェック

return

  • lambda 単にlambda内から戻る。 l = lambda {return 10} l.call #=> 10
  • Proc Procからではなく、Procを定義したスコープから戻る p = Proc.new {return 10} p.call # トップレベルのスコープからは戻れないからエラー! p = Proc.new {10} #単に明示的なreturnを使わなければいい

項数(引数チェック)

lambdaの方がProcよりも引数の違いに厳しい。違った項数(引数の数)で呼び出すとlambdaはArgumentErrorになる。
一方Procは引数列を機体に合わせてくれる。

p = Proc.new {|a,b| [a,b]}
p.call(1,2,3) #=> [1,2]
p.call(1) #=> [1,nil]

5. 実践問題「校長からの逃走」

鬼の校長を恐れる生徒たちは、自身の身を守るためDSL(ドメイン固有言語)を作成し、「event毎にsetupを回して条件に一致する場合は逃走するよう通知してくれるプログラム」である、escape_from_head.rbを開発することにしました。
具体的には、次の動作が期待されます。
event.rb

setup 'もっと勉強しろや!!' do
  @monthly_studying_hours < 200
end

setup '平均超えろや!' do
  @test_score < 40
end

event do
  @monthly_studying_hours = 20
end

event do
  @okr_score = 20
end

escape_from_head.rb 実行後の結果

「もっと勉強しろや!!」
校長の怒号が聞こえる。。逃げろ!!!

「平均超えろや!!」
校長の怒号が聞こえる。。逃げろ!!!

escape_from_head.rbのイメージ

def setup
 #code...
end

def event
 #code...
end

load 'event.rb'

#code...

分からなかったら@shimatomoまで。
答え

@setups = []
@events = []

def setup(text, &block)
  @setups << { text: text, condition: block }
end

def event(&block)
  @events << block
end

require 'event.rb'

@events.each do |event|
  clean_env = Object.new # selfをリセットすることでインスタンス変数を毎回リセット
  clean_env.instance_eval(&event)
  @setups.each do |setup|
    begin
      condition = clean_env.instance_eval(&setup[:condition])
    rescue NoMethodError
      next
    end
    if condition
      puts "「#{setup[:text]}\n 校長の怒号が聞こえる。。逃げろ!!!"
    end
  end
end