2018/09/13 Yusuke Endoh
メソッドの引数に名前を付ける機能
def foo(x: 1, y: 2, z: 3)
p [x, y, z]
end
foo(y: "Y", x: "X") #=> ["X", "Y", 3]
普通の引数と混在できる
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]
最後の引数にハッシュを渡すのと(ほぼ)同じ
def foo(h)
p h
end
foo(y: "Y", x: "X") #=> {:y=>"Y", :x=>"X"}
↑ハッシュとして受け取れる
def foo(x:1, y:2, z:3)
p [x, y, z]
end
hash = { y: "Y", x: "X" }
foo(hash) #=> ["X", "Y", 3]
↑ハッシュとして渡せる
(この仕様は歴史的経緯)
double splat **
で展開的なことができる
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"}]
def foo(opt=42, **kw)
p [opt, kw]
end
foo({}, **{}) #=> ???
答え:
foo({}, **{}) #=> [42, {}]
理由:
foo({}) #=> [42, {}] # キーワード引数扱いになる
foo({}, **{}) #=> [42, {}] # foo({})と同じなので
foo({}, {}) #=> [{}, {}] # [{}, {}]にする唯一の手段
リテラルかどうかで挙動が違う
def foo(opt=42, **kw)
p [opt, kw]
end
foo({}, **{}) #=> [42, {}]
empty_hash = {}
foo({}, **empty_hash) #=> [{}, {}]
どちらに合わせるかべきかは
議論の余地がある(最後に説明)
安心してメソッドを拡張できること
def foo(...)
end
foo(...)
↓
def foo(..., option1: false)
end
foo(...) # 従来通りに動く
foo(..., option1: true) # 拡張したモードで動く
この書き換えが常にうまく行ってほしい
p
みたいなメソッドを書いた
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}
出力先を制御できるようにしよう
↓
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 !!!
既存の呼び出しが死んだ!
HTMLの要素を作るメソッドを書いた
def create_element(name, attrs={})
end
create_element("a", href: "URL")
子要素のリストを受け取れるようにしよう
↓
def create_element(name, attrs={}, children: elements)
end
create_element("a", href: "URL") #=> unknown key: href !!!
また死んだ!
メソッドがキーワード引数を取るようになると
既存コードが死ぬ
Thread.new(stack_size: 100000)
Struct.new(keyword_init: true)
WDYT?
現状のまま放置
rest/optional引数とキーワード引数の両方を
受け取る場合だけ問題が起きる(たぶん)
def foo(*args, **kw) # エラー(または警告)
end
最後の引数とキーワード引数の相互変換をやめる
def foo(*args, **kw)
p [args, kw]
end
foo({ k: 1 }) #=> [[{:k=>1}], { }]
foo( k: 1 ) #=> [[{ }], {:k=>1}]
make test-all
を走らせる**
を付けてキーワードに寄せるだけ相互変換の片方だけをやめる
def foo(key: 1); p key; end
foo({}) #=> wrong number of arguments
def foo(a); p a; end
foo(k: 1) #=> OK: {:k=>1}
def foo(a)
p a
end
foo(k: 1) #=> OK: {:k=>1}
↑を許すと、foo
をキーワード拡張できない
def foo(a, output: $stdout)
$stdout.puts a.inspect
end
foo(k: 1) #=> unknown keyword: k
キーワード引数を完全分離したい
ERB#result_with_hash
は両方↓動いてほしい
erb.result_with_hash(k:1)
hash = {k:1}
erb.result_with_hash(hash)
Sequelのwhere
は両方↓動いてほしい(らしい)
where(k: 1) #=> {:k=>1}
where("k"=>1) #=> {"k"=>1}
Kernel#spawn
はいろいろ受け取る↓
spawn(..., out: File::NULL, 10=>11)
こういう既存APIを定義したいとき、どうする?
def result_with_hash(h1={}, **h2)
h = h1.merge(h2)
...
end
別案:互換APIを定義する方法を用意する?
define_last_hash_method(:result_with_hash) do |h|
...
end
spawn
のように混ぜて受け取るやつ
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の挙動がよいかも……)
def forward(*args, &blk)
target(*args, &blk)
end
↑のコードは動かなくなる、どうする?
委譲の記法を入れる?
def forward(...)
target(...)
end
悪くないと思う
キーワード引数も明示的に委譲する?
def forward(*args, **kw, &blk)
target(*args, **kw, &blk)
end
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, {}]
foo(**{})
の意味をいい感じにする必要がある
foo(**{})
の2.5での意味(1)↓リテラルと非リテラルで意味が違う
def foo(*args)
p args
end
foo(**{}) #=> []
empty_hash = {}
foo(**empty_hash) #=> [{}]
この挙動はバグで異論ないと思う
どちらに合わせるべきか?
foo(**{})
の2.5での意味(2)**empty_hash
は消えてほしい**{}
は消えないでほしい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
**empty_hash
も**{}
も消す方に揃えたい
詳しい意味の素案は次ページ
method(a, b, last)
, then last
will be promoted to a keyword argument if possible, i.e. if:method
takes keyword arguments (any of key:
, key: val
, or **options
in signature)method
are provided (here by a
and b
)last
is hash-like (i.e. responds_to? :to_hash
)last.to_hash
are symbolsOtherwise, last
will remain a positional argument.
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.method
does not accept keyword argumentsとてもややこしい
キーワード引数の現状と将来案について色々語った
我々はどこを目指すべきか
個人的な所感
↓はキーワード引数のままでよいか?
foo(:key => 42)