Yusuke Endoh
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
      • No invitee
    • Publish Note

      Publish Note

      Everyone on the web can find and read all notes of this public team.
      Once published, notes can be searched and viewed by anyone online.
      See published notes
      Please check the box to agree to the Community Guidelines.
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
No invitee
Publish Note

Publish Note

Everyone on the web can find and read all notes of this public team.
Once published, notes can be searched and viewed by anyone online.
See published notes
Please check the box to agree to the Community Guidelines.
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
--- 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) ```

Import from clipboard

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template is not available.
Upgrade
All
  • All
  • Team
No template found.

Create custom template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

How to use Slide mode

API Docs

Edit in VSCode

Install browser extension

Get in Touch

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Upgrade to Prime Plan

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

No updates to save
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Upgrade

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Upgrade

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully