4章 水曜日: ブロック
水曜日のアジェンダ
- スコープゲートとは
- ブロックの基本
- ブロックはクロージャ
- 呼び出し可能オブジェクト
- 実践問題「上長からの逃走」
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. 呼び出し可能オブジェクト
ブロックはオブジェクトではない。しかし、一度保管し後から実行するためにはオブジェクトであることが必要。
メソッド内ではなく、ブロック内にコードを保管するためにはそれをオブジェクト化する。
- オブジェクト生成方法
- Proc.newにブロックを渡す
- procに渡す
- lambdaに渡す
- 矢印lambda
- メソッド引数&で受け取って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