Ruby 3のキーワード引数
について考える
The Ruby Team
2018/09/15 (Sat.) 大江戸Ruby会議7
1
今日の議題
• Ruby 2のキーワード引数の問題
• Ruby 3に向けた改善の提案
2
『キーワード拡張』
• メソッドに新たにキーワード引数を持たせること
• 『キーワード拡張』は常に安全か?
– もともとあった呼び出しは元の通りに動いてほしい
def foo(...)
end
foo(...)
def foo(..., extension: false)
end
foo(...)
foo(..., extension: true)
3
キーワード拡張は安全でない
def foo(*args)
p args
end
foo(1, 2, 3) #=> [1, 2, 3]
foo(k: 42) #=> [{:k=>42}]
4
キーワード拡張は安全でない
def foo(*args, extension: false)
p args
end
foo(1, 2, 3) #=> [1, 2, 3]
foo(k: 42) #=> unknown key: k
5
問題:キーワード拡張が安全でない
• rest引数をとるメソッドのキーワード拡張は危険
– 既存コードが動かなくなるリスクがある
– optional引数も同様にダメ
• バグ報告が多数来ている
– #8316,#11967,#12104,#12717,#12821,#13336,#13647,#14130
• 実際にAPI拡張で困っている
– Thread.new(stack_size: 100000)とか
– Struct.new(keyword_init: true)とか
6
問題の原因
• キーワードとハッシュを自動変換していること
def foo(h)
p h #=>{:k=>42}
end
foo(k: 42)
def foo(k: 1)
p k #=> 42
end
foo({ k: 42 })
キーワード➔ハッシュ ハッシュ➔キーワード
7
Ruby 3での解決提案
• キーワードハッシュの変換をやめる #14183
– キーワードかハッシュか、明示してください
注意:Ruby 3の決定事項ではない
def foo(h)
p h #=>{:k=>42}
end
foo(k: 42)
def foo(k: 1)
p k #=> 42
end
foo({ k: 42 })
def foo(h)
p h[:k] #=> 42
end
foo({ k: 42 })
def foo(h)
p h #=>{:k=>42}
end
foo({ k: 42 })
def foo(**h)
p h #=>{:k=>42}
end
foo(k: 42)
def foo(k: 1)
p k #=> 42
end
foo(**{ k: 42 })
8
互換性の問題
• 修正箇所は少なくないが、修正は簡単
– だいたいは、**をつけるだけ
• 一部、むずかしいケースがある
9
互換性問題1:既存API
• キーワードとハッシュのどちらも許すAPI
• こういうAPIをRuby 3で定義するには?
– 修正方法:両方受け取ってマージしてください
erb.result_with_hash(k: 1)
erb.result_with_hash(hash)
def result_with_hash(h1={}, *h2)
h = h1.merge(h2)
...
end
10
互換性問題2:委譲
• 引数を丸投げするコード
• こういうコードをRuby 3で書くには?
– 修正方法:**kw も委譲してください
def forward(*args, &blk)
target(*args, &blk)
end
def forward(*args, **kw, &blk)
target(*args, **kw, &blk)
end
11
議題
• 議題0:問題と提案に対するお気持ちは?
• 議題1:foo(:key=>1)はキーワード?
• 議題1':foo("str"=>1, key:2)は?
• 議題2:既存APIのための記法?
• 議題3:委譲のための記法?
12
議題1
• Ruby 3で、以下はキーワード?ハッシュ?
– 案1:キーワードにする?(Ruby 2と互換)
– 案2:=>だったら常にハッシュにする?(非互換)
– 案3:文法エラー?(=>は{}を書かないとダメ)
foo(:key=>1)
13
議題1'
• Ruby 3で、以下はキーワード?ハッシュ?
– 案1:キーの種類で自動分割する?
• foo({"str"=>1}, key:2)
– 案2:非Symbolがあったら全部ハッシュ?
• foo({"str"=>1, key:2})
– 案3:文法エラーにする?(混ぜるな危険)
• 参考:こういうAPIは実在する(Kernel#spawn)
foo("str"=>1, key:2)
14
議題1''
• Ruby 3で、以下はキーワード?ハッシュ?
– 案1:キーの種類で自動分割する?
• foo({"s"=>2,"ss"=>4}, k:1, kk:3)
– 案2:非Symbolがあったら全部ハッシュ?
• foo({k:1, "s"=>2, kk:3, "ss"=>4})
– 案3:文法エラーにする?(混ぜるな危険)
foo(k:1, "s"=>2, kk:3, "ss"=>4)
15
議題2
• Ruby 2のように動くメソッドを簡単に定義したい
– 案1:諦めてもらう
• 手動マージしてください
– 案2:互換定義用のAPIを提供する
def result_with_hash(***h); end
ruby2 def result_with_hash(h); end
define_ruby2_method(:result_with_hash){}
16
議題3
• Ruby 2.6 と Ruby 3 の両方でうごく委譲は?
– 委譲専用の記法を導入する? #3447
def forward(...)
target(...)
end
17
議題3'
• Ruby 2.6 と Ruby 3 の両方でうごく委譲は?
– ↓は Ruby2.6で若干問題がある
– forward() が target(**{}) を呼ぶ
• target()とtarget(**{})は微妙に意味が違う
• 実用上は問題ない?
def forward(*args, **kw, &blk)
target(*args, **kw, &blk)
end
18
議題3''
• Ruby 2.5 は **{} がバグっている #15052
• 2.6で、どう直す?
def foo(opt=42)
p opt
end
foo(**{}) #=> 42
h={}
foo(**h) #=> {}
19
議題3'''
• Ruby 2.6で、以下の意味は?
foo(**{})
def foo(opt=42,**kw)
p [opt, kw]
end
foo({}, **{})
#foo({})と同じ
#=> [42, {}]
def foo(opt=42)
p opt
end
foo(**{}) #=> {}
案1:「無」とみなす? 案2:空ハッシュを渡す?
案3:いい感じに(難しい)
20

Ruby 3のキーワード引数について考える