BuriKaigi2021 で Ruby 2.0 〜 Ruby 3.0 までの話をしてきた

はい、ということで開催されてからだいぶ時間が経っちゃったんですが先月末に開催された BuriKaigi2021Ruby 2.0 〜 Ruby 3.0 の話をしてきました。

Ruby 2.0 から Ruby 3.0 を駆け足で振り返る

と、ここでならいつも雑に振り返りを行うのですが以下のブログに影響されてもう少し掘り下げてまとめてみようかと思います。

登壇するきっかけ

元々はわたしがよく参加している勉強会に BuriKaigi の関係者の方が参加していることが多くて『今年はコロナで大変だけど BuriKaigi をやりたいよねー』っていう話を前からしてはいたんですよね。
わたし自身は BuriKaigi 自体参加したことがなかったんですが『どうせなら登壇してみたいなー』みたいには前々からちょっと思っていました。
で、12月の頭ぐらいに『来年 BuriKaigi やりますー』って話を聞いて『○○○みたいな話をしてみたいんですが登壇者の募集とかってどうなっています?』みたいな感じで前から温めていたネタで打診してみました。
その時はまだ確定ではなかったんですが 12月下旬ぐらいに GO サインがでて登壇することが確定しました。
まあ完全に勢いとコネですね。人生にだいじだいじ。

テーマ選定

わたしは主に LT の場で話すことが多いのですが、その時のテーマ選定はだいたい

  • わたしが話したい内容か
  • 参加者に刺さる内容か
  • 時間内で話せそうか

あたりを基準として決めることが多いです。
具体的に『何を話すのか』は直前まで悩んでいることが多くて LT を申し込む時点ではキマってないことの方がおおいです。
本当の意味で『その時に話したいこと』を話しています。そっちのほうがモチベーションが上がりますしねー。
ただし、今回の BuriKaigi2021 の場合は『最初にこういう内容で登壇したい』という形で打診をしたので『最初に話す内容を決めた』っていう点でいつもとはちょっと違っていました。珍しい。

具体的なテーマ選定

今回の BuriKaigi2021 では最初から以下のような基準で話したいなーと思っていました。

  • キャッチーな話をしたい
  • あんまり Ruby のコアな話はしない
    • BuriKaigi の参加者層的な意味で
  • けど Ruby をもっと知ってもらいたい!
  • せっかくなので Ruby 3.0 の話をしたいなあ…

特に『せっかくRuby 3.0 がリリースされる(された)のでその話もしつつ普段は Ruby を知らない人にも興味がある内容を話したいなー』という点を重点的に考えていましたね。
そんなことを考えている時に『そういやわたしって Ruby 2.5 から本格的に Ruby をはじめたけどそれ以前のバージョンはよくしらんなーなんか Ruby の軌跡みたいなの話せると面白いのかなー』と思って『あーー『Ruby 2.0 〜 Ruby 3.0 で Ruby がどう変わってきたのか』みたいなのを話すと面白そうかなーー』っていう感じでテーマが漠然と決まった感じですね。
とりあえず『Ruby 2.0 〜 Ruby 3.0 のリリースノートをまとめて話そ〜〜』みたいに雑に決めました。

テーマの深堀り

ざっくりと話すことを決めたのでここでは具体的にどんな内容を話すのか考えていきます。
わたしの場合は実際にスライドを書くまでにある程度話したい内容を箇条書きにして書き溜めて行くことが多いです。
なので今回も決定してから登壇の 1週間前まで手元で話したいことを以下のような感じで書き溜めておきました。

こんな感じですね。
登壇時間は 30分だったので各 Ruby のバージョンを 2分で話すと 2 x 9 で 18分になります。
残り 12分を穴埋めするために Ruby とはRuby 2.0 以前の歴史 みたいな話も追加して話すことにしました。
更にどうせなら Ruby 3.0 のことも少し話したいなーと思い以下のように追加しました。

うーん、ちょっと 30分だと心もとなくなってきました。
で、ここからは裏話なんですが最初は登壇時間が 30分だったんですがわたしの後の枠が空いていたので +15分してもらって合計45分の時間に延長してもらいました。 いえーい。
なので、更に話すことを追加して最終的には以下のようなアウトラインが完成しました。

  • 自己紹介
  • Ruby とは
  • Ruby 2.0 以前の歴史
  • Ruby 2.0 ~ 3.0 までを振り返る
  • 最近のモダンな機能紹介・デモ
    • パターンマッチ
    • 1行 in
    • 右代入
    • Ractor
    • 型周りの話
    • irb
  • まとめ

ポイントとしては irb のデモを最後に入れたことですね。
時間が余れば irb のデモを長くすればいいですし、時間が足りなければそこを短くすることで時間を調整することができると思っていました。
まあ実際はガバガバだったんですけどね…。
これを元にしてスライドを書いていきます。

スライドを作る

上記のアウトラインを元にしてスライドを書いていきます。
実際にスライド作成に取り掛かったのは登壇の1週間ぐらい前になります。

スライド作成ツール

スライド作成はいつも reveal.js で作っています。
markdown でシュッとスライドを書くことができて、なおかつミニマムな構成で完結しているのが気に入っています。
レイアウトとかも css などをいじれば調整できますしね。

スライド作成と練習

今回のスライドはちょっと量が多かったのでまずはかける項目から書いていきました。
Ruby 2.0 ~ 3.0 までを振り返る はひたすら当時のリリースノートを参考にして Ruby を知らない人でもわかるような機能をピックアップしてスライドに書いていきました。
逆にいえば Ruby を使っている人しかわからないような細かい変更はあんまり書かないように心がけていましたね。
だいたい各バージョンが 1〜2分ぐらいで収まるような量にしつつ、わかりやすいようになるべくサンプルコードも記述していきました。
書くのに一番苦労したのはやっぱり Ruby 2.0 あたりの話ですね。
最近の Ruby の動向はリアルタイムで追っているので経緯とか把握していることが多いのですが Ruby 2.0 の時代は全く知らないので調べるのに苦労しました。
あと単純に当時の資料とかを読んでると面白くてスライド書かないでずーっと資料を読み漁っていましたね…。
Ruby 2.0 以前の歴史 あたりの話も同様に昔話を聞いてるような感じで調べつつまとめていました。
最近のモダンな機能紹介・デモ に関しては最近いろんなところで話をしているので既存のスライドから拝借して書いたのでそこまで時間はかかりませんでした。
そんな感じで内容はひたすら書いていく感じで構成とか内容自体はそんなに変更せずに書いていきました。
スライド自体が完成したのは確か前日の夜ぐらいでわたしにしては早めに完成していましたね。
練習に関しては『喋り慣れてるからまあそこまで練習しなくても大丈夫やろガハハ』って感じでそこまでやっていませんね。
それを元にして以下のように 残り時間 経過時間 を書き留めて置きます。

  • 開始 (残り時間 45分) (経過時間 0分)
  • 自己紹介
  • Ruby とは
  • Ruby 2.0 以前の歴史 (37分) (8分)
  • Ruby 2.0 ~ 3.0 までを振り返る
  • 最近のモダンな機能紹介・デモ (20分) (25分)
    • パターンマッチ
    • 1行 in (15分) (30分)
    • 右代入
    • Ractor
    • 型周りの話
    • irb (5分) (40分)
  • まとめ

登壇中はこれを見ながら残り時間を確認していました。

発表前にやること

うちの環境は2画面なんですが登壇中に見る画面には、

  • スライド
  • タイマー
  • (必要であれば)エディタ

みたいに最低限必要な情報のみ見えるようにしています。
で、もう一方の画面には Twitter や Zoom のチャットなど聞いてる人の声がわかるような形にしています。
あとは水を用意したりとかですかねえ。
あ、あと見づらい人用にスライドは発表前に公開するようにしています。

発表中にやること

スライドを見ながら話しつつタイマーで残り時間を確認しつつ余裕があれば Twitter で意見をみつつって感じで登壇しています。
流石に最近はスライドを見ながらタイマーを見るのは慣れてきましたねえ。
喋り始めは緊張するんですが喋ってる最中はもう殆ど緊張する事はなくなっています。

発表後にやること

ひたすら Twitter を見て気になる発言には個別に反応するようにしています。
なんかそっちのほうが盛り上がっている感があるのとやっぱり何かしらのフィードバックがもらえると嬉しいのでそこはちゃんとこっちもフィードバックするようにしています。

発表を終えてみて

機会があれば BuriKaigi で話してみたいなーと思っていたのでそれが達成できて個人的にはかなり満足度が高かったです。
特にこの手の大きなカンファレンスはオフラインだとなかなか参加しづらいのでオンラインで登壇できる環境っていうのは個人的にはめちゃくちゃありがたいんですよねー。
また機会があれば参加してみたいです。
運営の方々、参加者の方々、ありがとうございましたー!!!

2021/02/25 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[PR #4193] Add Hash#delete_at

  • Hash#delete_at を追加する提案
    • bugs.ruby にまだチケットはなさそう
  • 特定のキーの要素を取り除いて、取り除いた要素を返すメソッド
hash = { a: true, b: false, c: nil }
# hash から直接要素を取り除きつつ、取り除いた値を返す
a, c = hash.delete_at(:a, :c) # => [ true, nil ]
hash # => { b: false }

# Hash#except は取り除かれた結果を返す
hash = { a: true, b: false, c: nil }
hash = hash.except(:a, :c)
p hash
# {:b=>false}
  • 取り除いた要素を参照したい場合は便利そう

[PR #207] Support pattern matching in CSV rows

  • CSV::Row をパターンマッチに対応させる PR
    • CSV の行ごとにパターンマッチできる
  • 以下のようにヘッダーカラムがキーで要素が値になる
header = [:id, :name, :age]
case CSV::Row.new(header, [1, "homu", 14])
in name:, age:
  name # => "homu"
  age  # => 14
end
  • これは普通に便利そう
  • こういうパターンマッチ対応って今後流行っていくんですかねー

[Bug #17590] M.prepend M has hidden side effect

  • 先週話していたバグ
  • 以下のように M.prepend M を呼んだ場合に副作用あるバグが修正された
module M; end
class C; end

C.prepend M
C.include M

# これを読んだ時に失敗するが、副作用があった
M.prepend M rescue nil

module M2; end
M2.prepend M
C.include M2

p C.ancestors
# 3.0 => [M, C, M2, M, M2, Object, Kernel, BasicObject]
# 3.1 => [M, C, M2, Object, Kernel, BasicObject]

[Bug #17649] defined? invokes method once for each syntactic element around it

  • defined? の式で複数回メソッドが呼ばれることがあるというバグ報告
  • 以下の例だと x メソッドが defined? 時に複数回呼ばれることがある
public def x
  $times_called += 1
end

def times_called
  $times_called = 0
  yield
  $times_called
end

# without `defined?`
times_called { x }           # => 1
times_called { -x }          # => 1
times_called { --x }         # => 1
times_called { ---x }        # => 1
times_called { x+0+0 }       # => 1
times_called { x.pred.pred } # => 1
times_called { x.x.x.x.x.x } # => 6

# with `defined?`
times_called { defined? x }           # => 0
times_called { defined? -x }          # => 1
times_called { defined? --x }         # => 2
times_called { defined? ---x }        # => 3
times_called { defined? x+0+0 }       # => 2
times_called { defined? x.pred.pred } # => 2
times_called { defined? x.x.x.x.x.x } # => 15
  • これは defined? a.b.c.d を呼び出した時に a a.b a.b.c a.b.c.d が個別に呼び出されしまっているからのようです
puts RubyVM::InstructionSequence.new("defined? a.b.c.d").disasm
__END__
output:
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,16)> (catch: TRUE)
== catch table
| catch type: rescue st: 0001 ed: 0039 sp: 0000 cont: 0041
| == disasm: #<ISeq:defined guard in <compiled>@<compiled>:0 (0,0)-(-1,-1)> (catch: FALSE)
| local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| [ 1] $!@0
| 0000 putnil
| 0001 leave
|------------------------------------------------------------------------
0000 putnil                                                           (   1)[Li]
0001 putself
0002 defined                                func, :a, false
0006 branchunless                           41
0008 putself
0009 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0011 defined                                method, :b, false
0015 branchunless                           41
0017 putself
0018 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0020 opt_send_without_block                 <calldata!mid:b, argc:0, ARGS_SIMPLE>
0022 defined                                method, :c, false
0026 branchunless                           41
0028 putself
0029 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0031 opt_send_without_block                 <calldata!mid:b, argc:0, ARGS_SIMPLE>
0033 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>
0035 defined                                method, :d, true
0039 swap
0040 pop
0041 leave

2021/02/18 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Misc #17591] Test frameworks and REPLs do not show deprecation warnings by default

  • テストフレームワークRSpec や test-unite)や REPL(irb とか)で非推奨の警告をデフォルトで出すようにしよう、というチケット
  • 既に test-unit 3.4.0 ではデフォルトで有効になっており RSpec でも同様のチケットが立てられている
  • また irb に関しては『初心者が使うと混乱する』という理由で同意はされていないようです
  • 個人的には Ruby 自体が『デフォルトでは非推奨の警告を出さないように決めた』ので基本的には(少なくとも組み込みの範囲では)デフォルトでは出さないようにしてほしい気持ちが強い
    • このケースは警告がでて、このケースは警告がでないみたいにすると一貫性もないし、ユーザは混乱するだけ
    • Ruby 2.7.2 の世界では個々で必要に応じて警告を出すようにすべき

[Bug #17590] M.prepend M has hidden side effect

  • M.prepend M を呼び出すとエラーになるが副作用があるというバグ報告
  • 以下のように M.prepend M を呼んだ場合とそうでない場合で差異がある
module M; end
class C; end
C.prepend M
C.include M

module M2; end
M2.prepend M
C.include M2

# M.prepend M を呼んでない場合
p C.ancestors # => [M, C, M2, Object, Kernel, BasicObject]


M.prepend M rescue nil
module M3; end
M3.prepend M
C.include M3

# M.prepend M を呼んだ場合
# M が複数追加されている…
p C.ancestors # => [M, C, M3, M, M3, M2, Object, Kernel, BasicObject]
  • これには2つの問題があるようで、1つは以下のように M.prepend M すると M.ancestors # => [M, M] となる問題
module M
end

# 継承リストは自身だけ
pp M.ancestors
# => [M]

begin
  # 自身を prepend するとエラーになる
  # これは Ruby 2.7 でも 3.0 でも同じ
  M.prepend M
rescue => e
  puts "error : #{e.message}"
  # => error : cyclic prepend detected
end

# M.prepend M はエラーになるが Ruby 3.0 では副作用がある
# 3.1-dev では修正済み
pp M.ancestors
# 2.7 => [M]
# 3.0 => [M, M]
# 3.1 => [M]

Ruby で ["homu", "mami", "mado"].sum を呼ぶとエラーになる

今日の知見です。
Array#sum メソッドは要素を #+ で結合してその結果を返します。

p [1, 3, 5, 7].sum
# => 16

また Array#sum に引数を渡すことで足し込む値の初期値を指定することもできます。

p [1, 3, 5, 7].sum(100)
# => 116

["homu", "mami", "mado"].sum を呼ぶとエラーになる

では要素が文字列の場合はどうでしょうか。
残念ながら要素が文字列の場合はエラーになってしまいます。

# error: `+': String can't be coerced into Integer (TypeError)
pp ["homu", "mami", "mado"].sum

これは Array#sum が内部で使用している初期値に問題があります。
Array#sum は足し込むときの初期値として 0 を使用しています。
なので最初に足し込む際に 0 + "homu" みたいな処理が実行されてしまいエラーになってしまいます。
これを回避する場合は Array#sum に明示的に初期値 "" を渡すことで回避することができます。

# OK
pp ["homu", "mami", "mado"].sum("")
# => "homumamimado"

# 任意の要素を初期値にもできる
pp ["homu", "mami", "mado"].sum("joined : ")
# => "joined : homumamimado"

なぜエラーになるのかの原因がわかっているならいいんですがいきなり String can't be coerced into Integer (TypeError) ってエラーになってもぎょっとしますよね。
ちなみに単に文字列を結合したい場合は Array#join が利用できます。

pp ["homu", "mami", "mado"].join
# => "homumamimado"

2021/02/11 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #17424] Interactive Ruby で Object#method を再定義して任意の文字を入力するとエラーが発生する

  • 表題の通りでなんですが irb 上で #method を再定義するとクラッシュするというバグ報告です
  • これは irb 上で #method を定義すると Kernel#method よりも優先順位が高い Object#method が定義されてしまい、Reline でその Object#method を使用しているためです
  • これは仕様でチケットは閉じられています
  • まあしょうがなさそう

[Feature #7394] Enumerable#find ifnone parameter could be non-callable

  • Enumerable#find は要素が見つからなかった場合の処理をフォールバックすることができる
# 見つからなかった場合に第一引数の proc を呼び出す
p [1, 3, 5].find(-> { "none" }, &:even?)
# => "noge"
  • 現状は Proc オブジェクトを渡すがこれをそれ以外のオブジェクトを渡せるようにする提案
# 第一引数の値がそのまま返ってくる
p [1, 3, 5].find("none", &:even?)
# => "noge"
p [1, 3, 5].find(&:even?) || "none"

[Feature #17608] Compact and sum in one step

  • 次のように Array#sum を行う場合に #compact を介して行うことがある
a = [1, nil, 2, 3]

a.sum # !> TypeError

a.compact.sum # => 6

a.sum{_1 || 0} # => 6
  • これをワンステップで行うために #sum では nil の要素をスキップする、または Array#filter_sum のようなメソッドを追加する提案です
  • 最終的には a.sum{_1 || 0} で問題ないということで Reject されています

Vim の oldfiles の最大数を変更する

Twitter でいろいろとやり取りしていたので覚書。

Vim の oldfiles の最大数を変更する

Vim oldfiles の保存数は 'viminfo'' の設定に依存しています。
この保存数を変更する場合は vimrc に以下のような設定を追加することで変更することができます。

" v:oldfiles で保存するファイル数を設定
" vimrc に書いておく必要がある
set viminfo+='10000
set viminfo-='100     " デフォルトの設定を削除しておかないt反映されない

また、oldfiles で保存するファイルパスは Vim を再起動しないと反映されないので注意する必要があります。
ちなみに denite-file/old はこの oldfiles のデータを参照しているのでこの設定をすることで denite-file/old の保存数を変更することもできます。

2021/02/04 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #10593] Emoji is been considered as comment

#️⃣ これはコメントアウトです
#️⃣ Ruby のコードは実行されません
puts "hello, world" #️⃣ コメント
  • これは #️⃣ が囲み文字と呼ばれている絵文字で # + 特殊なコードポイント で表現されているためです
  • 実際に字句解析するとこんな感じ
require "ripper"

# コメントとして字句解析される
p Ripper.lex("#️⃣")
# => [[[1, 0], :on_comment, "#️⃣", BEG]]

# こっちは普通の文字になる
p Ripper.lex("😀")
# => [[[1, 0], :on_ident, "😀", CMDARG]]
  • これは仕様ということで閉じられている

[Bug #7877] E::Lazy#with_index should be lazy

  • Enumerable::Lazy#with_indexRuby 2.6 と 2.7 で挙動が変わっていたので調べていたらこのチケットを見つけました
# Ruby 2.6 だとブロックの中身が呼ばれるが、2.7 だと呼ばれない
pp [1, 2, 3].lazy.with_index { |it, i| l}
# 2.6 => [1, 2, 3]
# 2.7 => #<Enumerator::Lazy: ...>
  • チケット自体は Enumerable::Lazy#with_indexlazy 化してほしいというバグチケット
    • 内容的には feature っぽいが
  • この影響で既存の Enumerable::Lazy#with_index の挙動がぶっ壊れてしまったぽい
  • 上記のコードを Ruby 2.7 でも同じようにどうさせる場合は #force#each するのが無難かなあ
pp [1, 2, 3].lazy.with_index { |it, i| p [it, i] }.force
pp [1, 2, 3].lazy.with_index.each { |it, i| p [it, i] }