owned this note
owned this note
Published
Linked with GitHub
---
lang: ja-jp
tags: Ruby, keyword-argument
---
# キーワード引数の現状と将来構想
2018/09/13 Yusuke Endoh
---
## アジェンダ
* <span><!-- .element: class="fragment highlight-red" -->背景:キーワード引数とは</span>
* 問題:キーワード引数拡張が危険
* 手法:キーワード引数拡張を安全にする
* 課題:移行パスについて
---
## キーワード引数とは(1)
メソッドの引数に名前を付ける機能
```ruby
def foo(x: 1, y: 2, z: 3)
p [x, y, z]
end
foo(y: "Y", x: "X") #=> ["X", "Y", 3]
```
---
## キーワード引数とは(2)
普通の引数と混在できる
```ruby
def foo(name, age=42, x:1, y:2, z:3)
p [name, age, x, y, z]
end
foo("mame", 36, y: "Y", x: "X")
#=> ["mame", 36, "X", "Y", 3]
```
---
## キーワード引数とは(3)
最後の引数にハッシュを渡すのと(ほぼ)同じ
```ruby
def foo(h)
p h
end
foo(y: "Y", x: "X") #=> {:y=>"Y", :x=>"X"}
```
↑ハッシュとして受け取れる
```ruby
def foo(x:1, y:2, z:3)
p [x, y, z]
end
hash = { y: "Y", x: "X" }
foo(hash) #=> ["X", "Y", 3]
```
↑ハッシュとして渡せる
(この仕様は歴史的経緯)
---
## キーワード引数とは(4)
double splat `**` で展開的なことができる
```ruby
def foo(x: 1, **h)
p [x, h]
end
foo() #=> [1, {}]
foo(x: "X") #=> ["X", {}]
foo(y: "Y") #=> [1, {:y=>"Y"}]
hash = {x: "X"}
foo(y: "Y", **hash) #=> ["X", {:y=>"Y"}]
```
---
## クイズ
```ruby
def foo(opt=42, **kw)
p [opt, kw]
end
foo({}, **{}) #=> ???
```
答え:
<!-- .element: class="fragment" data-fragment-index="1" -->
```ruby
foo({}, **{}) #=> [42, {}]
```
<!-- .element: class="fragment" data-fragment-index="1" -->
理由:
<!-- .element: class="fragment" data-fragment-index="2" -->
```ruby
foo({}) #=> [42, {}] # キーワード引数扱いになる
foo({}, **{}) #=> [42, {}] # foo({})と同じなので
foo({}, {}) #=> [{}, {}] # [{}, {}]にする唯一の手段
```
<!-- .element: class="fragment" data-fragment-index="2" -->
---
## ただのバグ?
リテラルかどうかで挙動が違う
```ruby
def foo(opt=42, **kw)
p [opt, kw]
end
foo({}, **{}) #=> [42, {}]
empty_hash = {}
foo({}, **empty_hash) #=> [{}, {}]
```
どちらに合わせるかべきかは
議論の余地がある(最後に説明)
<!-- .element: class="fragment" data-fragment-index="1" -->
---
## アジェンダ
* 背景:キーワード引数とは
* <span><!-- .element: class="fragment highlight-red" -->問題:キーワード引数拡張が危険</span>
* 手法:キーワード引数拡張を安全にする
* 課題:移行パスについて
---
## キーワード引数に何を期待するか
安心してメソッドを拡張できること
```ruby
def foo(...)
end
foo(...)
```
↓
```ruby
def foo(..., option1: false)
end
foo(...) # 従来通りに動く
foo(..., option1: true) # 拡張したモードで動く
```
<!-- .element: class="fragment" data-fragment-index="1" -->
この書き換えが常にうまく行ってほしい
<!-- .element: class="fragment" data-fragment-index="2" -->
---
## 裏切られる例1
`p`みたいなメソッドを書いた
```ruby
def my_p(*args)
args.each {|v| puts v.inspect }
end
my_p([1, 2, 3]) #=> [1, 2, 3]
my_p(k: 1) #=> {:k=>1}
```
出力先を制御できるようにしよう
↓
<!-- .element: class="fragment" data-fragment-index="1" -->
```ruby
def my_p(*args, out: $stdout)
args.each {|v| out.puts v.inspect }
end
my_p([1, 2, 3]) #=> [1, 2, 3]
my_p(k: 1) #=> unknown key: k !!!
```
<!-- .element: class="fragment" data-fragment-index="2" -->
既存の呼び出しが死んだ!
<!-- .element: class="fragment" data-fragment-index="3" -->
---
## 裏切られる例2
HTMLの要素を作るメソッドを書いた
```ruby
def create_element(name, attrs={})
end
create_element("a", href: "URL")
```
子要素のリストを受け取れるようにしよう
↓
<!-- .element: class="fragment" data-fragment-index="1" -->
```ruby
def create_element(name, attrs={}, children: elements)
end
create_element("a", href: "URL") #=> unknown key: href !!!
```
<!-- .element: class="fragment" data-fragment-index="2" -->
また死んだ!
<!-- .element: class="fragment" data-fragment-index="3" -->
---
## 『問題』のまとめ
メソッドがキーワード引数を取るようになると
既存コードが死ぬ
* 実際にバグ報告が多数来ている
* 実際にAPI拡張できなくて困っている
* `Thread.new(stack_size: 100000)`
* `Struct.new(keyword_init: true)`
* 今の意味は複雑すぎる
* 型シグネチャで表現できない
WDYT?
---
## アジェンダ
* 背景:キーワード引数とは
* 問題:キーワード引数拡張が危険
* <span><!-- .element: class="fragment highlight-red" -->手法:キーワード引数拡張を安全にする</span>
* 課題:移行パスについて
---
## いくつかの解決案
1. 何もしない
2. 引数の混在を禁止する
3. キーワード引数を完全分離する
4. その他、素晴らしいアイデア
---
## 1. 何もしない
現状のまま放置
* メリット:
* 完全互換
* デメリット:
* 『問題』は解決せず、広がっていく
---
## 2. 引数の混在を禁止する
rest/optional引数とキーワード引数の両方を
受け取る場合だけ問題が起きる(たぶん)
```ruby
def foo(*args, **kw) # エラー(または警告)
end
```
* メリット:
* 『問題』に気づける
* 警告だけなら互換性は保たれる
* デメリット:
* 可変長引数メソッドはキーワード拡張できない
---
## 3. キーワード引数を完全分離する
最後の引数とキーワード引数の相互変換をやめる
```ruby
def foo(*args, **kw)
p [args, kw]
end
foo({ k: 1 }) #=> [[{:k=>1}], { }]
foo( k: 1 ) #=> [[{ }], {:k=>1}]
```
* メリット:
* 『問題』がそもそも起きえない
* 可変長引数メソッドも拡張できる
* デメリット:
* 互換性はまあまあ厳しい?
---
## 互換性は厳しいのか?
* 実験
* 相互変換したら警告を出すパッチを作った
* 警告有効で`make test-all`を走らせる
* 警告箇所を修正していく
* 感想
* 私見では、大体straightforward
* `**` を付けてキーワードに寄せるだけ
* 1.9のencodingに比べれば全然余裕
* 一部困難はあった(後で述べる)
---
## Jeremyの妥協案
相互変換の片方だけをやめる
* 「ハッシュ引数→キーワード」は禁止する
```ruby
def foo(key: 1); p key; end
foo({}) #=> wrong number of arguments
```
* 「キーワード→ハッシュ引数」は維持する
```ruby
def foo(a); p a; end
foo(k: 1) #=> OK: {:k=>1}
```
* 根拠
* 後者はRuby 1.6から動いていた
* 後者の挙動についてのバグ報告はない(当然)
---
## Jeremy案の問題点
```ruby
def foo(a)
p a
end
foo(k: 1) #=> OK: {:k=>1}
```
↑を許すと、`foo`をキーワード拡張できない
```ruby
def foo(a, output: $stdout)
$stdout.puts a.inspect
end
foo(k: 1) #=> unknown keyword: k
```
<!-- .element: class="fragment" data-fragment-index="1" -->
* 元コードを禁止するしか無い(と思う)
* 目先の互換性問題 vs. 将来の互換性問題
<!-- .element: class="fragment" data-fragment-index="2" -->
---
## 手法のまとめ
キーワード引数を完全分離したい
* 目先の互換性問題は、対処容易
* (個人の感想)
* 将来的な互換性問題は、広がっていく
---
## アジェンダ
* 背景:キーワード引数とは
* 問題:キーワード引数拡張が危険
* 手法:キーワード引数拡張を安全にする
* <span><!-- .element: class="fragment highlight-red" -->課題:移行パスについて</span>
---
## 分離する場合の移行パス
* 相互変換が起きたら警告する
* 2.6 or 2.7
* (Cメソッドはrest引数を受け取る問題)
* 推奨する書き換え方法を決める
* 問題1: 既存API
* 問題2: 委譲のコード
---
## 問題1: 既存API(1)
`ERB#result_with_hash`は両方↓動いてほしい
```ruby
erb.result_with_hash(k:1)
hash = {k:1}
erb.result_with_hash(hash)
```
Sequelの`where`は両方↓動いてほしい(らしい)
<!-- .element: class="fragment" data-fragment-index="1" -->
```ruby
where(k: 1) #=> {:k=>1}
where("k"=>1) #=> {"k"=>1}
```
<!-- .element: class="fragment" data-fragment-index="1" -->
`Kernel#spawn`はいろいろ受け取る↓
<!-- .element: class="fragment" data-fragment-index="2" -->
```ruby
spawn(..., out: File::NULL, 10=>11)
```
<!-- .element: class="fragment" data-fragment-index="2" -->
こういう既存APIを定義したいとき、どうする?
---
## 問題1: 既存API(2)
* 解決案:両方受け取って頑張る
* 微妙な非互換はある
```ruby
def result_with_hash(h1={}, **h2)
h = h1.merge(h2)
...
end
```
別案:互換APIを定義する方法を用意する?
<!-- .element: class="fragment" data-fragment-index="1" -->
```ruby
define_last_hash_method(:result_with_hash) do |h|
...
end
```
<!-- .element: class="fragment" data-fragment-index="1" -->
---
## 問題1: 既存API(3)
`spawn`のように混ぜて受け取るやつ
```ruby
def foo(h1, **h2)
p [h1, h2]
end
foo(key: 1, "str" => 2) #=> [{"str"=>2, :key=>1}, {}]
foo("str" => 2, key: 1) #=> [{"str"=>2, :key=>1}, {}]
def foo(h1={}, **h2)
p [h1, h2]
end
foo(key: 1, "str" => 2) #=> [{"str"=>2}, {:key=>1}]
foo("str" => 2, key: 1) #=> [{"str"=>2}, {:key=>1}]
```
↑は2.5での挙動、trunkではエラー
(移行パス的には、2.5の挙動がよいかも……)
---
## 問題2: 委譲のコード(1)
```ruby
def forward(*args, &blk)
target(*args, &blk)
end
```
↑のコードは動かなくなる、どうする?
---
## 問題2: 委譲のコード(2)
委譲の記法を入れる?
```ruby
def forward(...)
target(...)
end
```
悪くないと思う
---
## 問題2: 委譲のコード(3)
キーワード引数も明示的に委譲する?
```ruby
def forward(*args, **kw, &blk)
target(*args, **kw, &blk)
end
```
* 実は、↑は2.5で動かない場合がある
<!-- .element: class="fragment" data-fragment-index="1" -->
```ruby
def target(*args)
p args
end
def forward(*args, **kw, &blk)
target(*args, **kw, &blk)
end
target(1, 2, 3) #=> [1, 2, 3]
forward(1, 2, 3) #=> [1, 2, 3, {}]
```
<!-- .element: class="fragment" data-fragment-index="1" -->
`foo(**{})` の意味をいい感じにする必要がある
<!-- .element: class="fragment" data-fragment-index="2" -->
---
## `foo(**{})`の2.5での意味(1)
↓リテラルと非リテラルで意味が違う
```ruby
def foo(*args)
p args
end
foo(**{}) #=> []
empty_hash = {}
foo(**empty_hash) #=> [{}]
```
この挙動はバグで異論ないと思う
どちらに合わせるべきか?
---
## `foo(**{})`の2.5での意味(2)
* 委譲を考えると、`**empty_hash`は消えてほしい
* 次の挙動を考えると、`**{}`は消えないでほしい
```ruby
def foo(*args)
p args.pop
end
foo(**{k1: 1, k2: 2, k3: 3}) #=> {:k1=>1, :k2=>2, :k3=>3}
foo(**{k1: 1, k2: 2}) #=> {:k1=>1, :k2=>2}
foo(**{k1: 1}) #=> {:k1=>1}
foo(**{}) #=> nil !?
```
---
## `foo(**{})`の2.5での意味(3)
* 次の挙動を考えると、`**{}`は消えないでほしい
```
def foo(opt=42, **kw)
p [opt, kw]
end
foo({} ) #=> actual: [42, {}] as expected
foo({}, **{}) #=> actual: [42, {}], expected: [{}, {}]
foo({}, {}) #=> actual: [{}, {}] as expected
```
---
## Marc-Andreの案
`**empty_hash`も`**{}`も消す方に揃えたい
詳しい意味の素案は次ページ
---
1) When calling `method(a, b, last)`, then `last` will be promoted to a keyword argument if possible, i.e. if:
a) `method` takes keyword arguments (any of `key:`, `key: val`, or `**options` in signature)
b) and all mandatory positional arguments of `method` are provided (here by `a` and `b`)
c) and `last` is hash-like (i.e. `responds_to? :to_hash`)
d) and all keys of `last.to_hash` are symbols
Otherwise, `last` will remain a positional argument.
---
2) When calling `method(a, b, key: value)` or `method(a, b, **hash)` or a combination of these, the keyword arguments (here `{key: value}` or `hash`) will be demoted to a positional argument if needed, i.e.
a) if `method` does not accept keyword arguments
b) and they are non empty
とてもややこしい
---
## まとめ
キーワード引数の現状と将来案について色々語った
* 我々はどこを目指すべきか
* 現状のクソ仕様と心中する
* 互換性を壊して成長していく
* 個人的な所感
* 現状の仕様は本当に複雑怪奇
* 書き換えの互換性の問題はそこまで大きくない
* キーワード拡張でハマる将来の互換性問題の方が大きい
---
## その他
↓はキーワード引数のままでよいか?
```ruby
foo(:key => 42)
```