【Ruby 3.0 Advent Calendar 2020】Ruby 3.0 がリリースされました!!!【25日目】
Ruby 3.0 Advent Calendar 2020 25日目の記事になります。
Ruby 3.0.0 リリース
予定通り本日12月25日に無事 Ruby 3.0 がリリースされました。
開発者の皆様お疲れ様でした。
それと同時にこのアドベントカレンダーも本日で終了になります。
参加していただいた皆様ありがとうございました!!
Ruby 3.0 のリリースノートは以下になります。
Ruby 3.0 では新機能はもちろん細かな変更もたくさん入っています。
具体的にどのような機能が入り、どのような機能が変更されたのかはこのアドベントカレンダーにかかれている記事が役に経つと思います。
また、12月に入ってからも Ruby の開発は行われおりアドベントカレンダーに書かれている部分が変更されている可能性もあります(主にわたしの記事とか…)
なので、オフィシャルなリリースノートや NEWS も合わせて参照してみるといいと思います。
いやー本当に無事にリリースされてよかったよかった…。
開発者の皆様本当にお疲れ様でした。
2020/12/17 今週の気になった bugs.ruby のチケット
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。
[Feature #17303] Remove webrick from stdlib
- webrick を標準ライブラリから削除するチケット
- webrick には脆弱性の問題がありサポートするのが難しいという旨が書かれている
- このチケットはすでにマージ済みで今後は独自 gem として開発されていく予定
- webrick を使っているプロダクトは別途
gem "webrick"
を追加して対応する必要がある
[Feature #17371] Reintroduce expr in pat
- Ruby 2.7 で入った1行
in
は=>
に置き換わる形で一旦削除された - これを
=>
とは違い真理値を返す形で復活させる提案 - これを利用すると条件分岐などで
in
を使えるようになる
# version 2.7 0 in 1 #=> raise NoMatchingPatternError # version 3.0 0 in 1 #=> false
# name が String 型で age が20以下の場合にマッチする if user in { name: String, age: (..20) } puts "OK" end
users = [ { name: "homu", age: 14 }, { name: "mami", age: 15 }, { name: "mado", age: 14 }, ] # こんな感じで絞り込むことができる pp users.select { _1 in { name: /^m/, age: (...15) } }
- 元々 1行
in
は真理値を返す用途で実装されたが紆余曲折あって Ruby 2.7 ではマッチしなかった場合は例外を返すような挙動になった - なので条件分岐で使えなくて残念だと思っていたのでこれは嬉しい変更
- ただし、Ruby 3.0 では
experimental warning
が出るので注意 - 参照
[Feature #17314] Provide a way to declare visibility of attributes defined by attr* methods in a single expression
- 以前も紹介したチケット
- 以下のような変更を行う内容
class Foo protected [:x, :y] # same as: protected :x, :y attr_accessor :foo, :bar # => [:foo, :foo=, :bar, :bar=] instead of `nil` attr_reader :foo, :bar # => [:foo, :bar] instead of `nil` attr_writer :foo, :bar # => [:foo=, :bar=] instead of `nil` alias_method :new_alias, :existing_method # => :new_alias instead of `Foo` end
- これを利用すると以下のようにかける
class Foo private attr_accessor :foo, :bar end
- これはまつもとさんが承認したので Ruby 3.0 に入りそう
- ただし、まだマージはされていない
[Feature #8382] Format OpenStruct YAML dump and create getters and setters after load.
require "ostruct" require "yaml" h = { name: "John Smith", age: 70, pension: 300.0 } os = OpenStruct.new(h) pp os.to_yaml # Ruby 2.7.1 # "--- !ruby/object:OpenStruct\n" + # "table:\n" + # " :name: John Smith\n" + # " :age: 70\n" + # " :pension: 300.0\n" # Ruby 2.7.2 # "--- !ruby/object:OpenStruct\n" + # "name: John Smith\n" + # "agf: 70\n" + # "pension: 300.0\n"
[Feature #17384] shorthand of Hash#merge
Hash#merge
のショートハンドの提案チケットHash#+
で#merge
する
a = {k: 1} b = {j: 2} c = a.merge(b) d = a + b # same as c
- これは以下のチケットと重複している
- 便利そうな気もするけどウーン
[PR #3904] Skip defined instruction in NODE_OP_ASGN_OR with ivar
- [Feature #17055] Allow suppressing uninitialized instance variable and method redefined verbose mode warnings で 『未初期化のインスタンス変数を参照』時に警告がでなくなった
-W
等の場合のみ出てた
$VERBOSE = true # Ruby 2.7.2 : warning: instance variable @value not initialized # Ruby 3.0.0 : no warning pp @value
- この変更で
@foo ||= 123
時に不要なオーバーヘッドがあったのでそれを削除した PR - 背景としては元々
@foo ||= 123
を@foo || (@foo = 123)
と展開してしまうと後者の場合、警告がでてしまっていた
$VERBOSE = true # no warning @foo ||= 123 # warning: instance variable @foo not initialized @foo || (@foo = 123)
- これを対処するために
@foo ||= 123
では警告が出ないようにするための命令が追加されていた- 多分擬似的に
@foo
を定義しているような命令?
- 多分擬似的に
- しかし、Ruby 3.0 で警告が出なくなった為、この命令は不要になり削除された、という経緯になる
- 単純に
@foo ||= 123
のパフォーマンスが上がったそうなので普通に便利そう
merge, fix されたチケット
- [Feature #17055] Allow suppressing uninitialized instance variable and method redefined verbose mode warnings
-W
で未初期化のインスタンス変数を参照しても警告が出なくなった
- [Feature #17303] Remove webrick from stdlib
- webrick が標準ライブラリから削除された
2020/12/24 今週の気になった bugs.ruby のチケット
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。
[Bug #17030] Enumerable#grep{_v} should be optimized for Regexp その後
- 以前紹介した
ary.select { |e| e.match?(reg) }
と比較してary.grep(reg)
の方が遅いので最適化しよう、という提案 - その後、議論が進んで最終的に
ary.grep(reg)
のようにブロック引数がない場合はMatchData
を生成しないように対応された
ary = ["homu", "mami", "mado"] reg = /.*/ ary.grep(reg) # or Regexp.last_match p $~ # 2.7 => #<MatchData "mado"> # 3.0 => nil # ブロックを渡した場合は MatchData を生成する ary.grep(reg) {} # or Regexp.last_match p $~ # 2.7 も 3.0 => #<MatchData "mado">
- この挙動は非互換な変更になるので注意する必要がります
[PR #124] Add measure command
irb
にmeasure
というコマンドが追加された- これは
measure
を呼び出した以降で実行時間を出力するような事を行う
irb(main):001:0> 3 => 3 irb(main):002:0> measure TIME is added. => nil irb(main):003:0> 3 processing time: 0.000058s => 3 irb(main):004:0> measure :off => nil irb(main):005:0> 3 => 3
- また以下のようにカスタマイズすることも可能です
IRB.conf[:MEASURE_PROC][:CUSTOM] = proc { |context, code, line_no, &block| time = Time.now result = block.() now = Time.now puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] result }
- これは普通に便利そう
[Bug #17428] Method#inspect bad output for class methods
Method#inspect
の内容がおかしいというバグ報告- 次のようにクラス名が表示されないケースがある
- おそらくこれの影響
p String.method(:prepend) # 2.7 => #<Method: String.prepend(*)> # 3.0 => #<Method: #<Class:Object>(Module)#prepend(*)>
- この問題は最新版では以下のように修正された
p String.method(:prepend) # 2.7 => #<Method: String.prepend(*)> # 3.0 => #<Method: #<Class:String>(Module)#prepend(*)>
- これは気づかなかった…
[Feature #17116] raise ArgumentError in Enumerator#new in no given blocks
Enumerator.new(obj)
みたいに.new
にブロックを渡さない場合は deprecated warning が出ている- この警告がでているのは Ruby 2.0 の頃から
obj = Object.new # warning: Enumerator.new without a block is deprecated; use Object#to_enum instead Enumerator.new(obj)
- これはもうエラーにしてしまってもいいんじゃないか、というチケット
- これはマージされて Ruby 3.0 からはエラーになるので注意しましょう
[Bug #17423] Prepend
should prepend a module before the class
- Ruby 3.0 では以下のように
prepend
の挙動が変わってしまう事がある
module M; end module A; end class B; include A; end A.prepend M B.prepend M # B.prepend M してるのに M が B よりもあとに来る p B.ancestors # 2.7 => [M, B, A, Object, Kernel, BasicObject] # 3.0 => [B, M, A, Object, Kernel, BasicObject]
- これを次のように
M
が重複するようにするという内容のチケット
module M; end module A; end class B; include A; end A.prepend M B.prepend M # このように修正する p B.ancestors # => [M, B, M, A, Object, Kernel, BasicObject]
- この問題は Ruby 3.0 リリース後に対応される予定です
- ちなみにこのチケットはまつもとさん自身が建てています
[Feature #17411] Allow expressions in pattern matching
- パターンマッチでは次のようにパターンの部分に式を書くことができません
user = { name: "homu", age: 14 } case user # syntax error, unexpected '+', expecting ')' # パターンに式を記述する事ができない in { age: (7 + 7) } end
- これを
^(expression)
のようにかけるようにしようという提案
user = { name: "homu", age: 14 } case user # OK # 式を書く場合は ^() を使う in { age: ^(7 + 7) } end
- この実装自体はすでにされていますが流石に Ruby 3.1 以降で入りそうな感じです
[Bug #17398] SyntaxError in endless method
# OK def hoge = puts("homu") # syntax error, unexpected string literal, expecting `do' or '{' or '(' def hoge = puts "homu"
【一人 bugs.ruby Advent Calendar 2020】[Bug #17423] `Prepend` should prepend a module before the class【24日目】
一人 bugs.ruby Advent Calendar 2020 24日目の記事になります。
[Bug #17423] Prepend
should prepend a module before the class
このブログでも何回か紹介しているんですが Ruby 3.0 では Module#include / #prepend
の挙動がちょっと変わります。
特に以下のように Ruby 2.7 と Ruby 3.0 でかなり挙動が変わるコードも存在しています。
module M; end module A; end class B; include A; end A.prepend M B.prepend M # B.prepend M してるのに M が B よりもあとに来る p B.ancestors # 2.7 => [M, B, A, Object, Kernel, BasicObject] # 3.0 => [B, M, A, Object, Kernel, BasicObject]
このチケットはこの問題を解決するために以下のように M
が重複することを許容しよう、という旨のチケットになります。
module M; end module A; end class B; include A; end A.prepend M B.prepend M # このように修正する p B.ancestors # => [M, B, M, A, Object, Kernel, BasicObject]
これはこれでちょっと気持ち悪い気もするんですがまあ無いよりはマシですかねえ…ぶっ壊れるときはどっちにしてもぶっ壊れそうだし…。
この問題は Ruby 3.0 リリース後に対応される予定となっています。
ちなみにこのチケットはまつもとさん自身が建てていたりします。
【Ruby 3.0 Advent Calendar 2020】Ractor の共有可能オブジェクトについて【24日目】
Ruby 3.0 Advent Calendar 2020 24日目の記事になります。
今回は Ruby 3.0 で実験的に入る Ractor の共有可能オブジェクトについて簡単に説明してみます。
NOTE: ここに書いてある内容は今後変更されるかもしれないので注意してください!!
共有可能オブジェクトとは
通常 Ractor 間でオブジェクトのやり取りを場合は #send
+ Ractor.receive
や Ractor.yield
+ #take
をつかったりします。
ractor = Ractor.new { # send の値を受け取る obj = Ractor.receive p obj # => [1, 2, 3] # take の戻り値として返す Ractor.yield obj.map { _1 + _1 } } # send で渡した値を Ractor.receive で受け取る ractor.send [1, 2, 3] # Ractor.yield の引数を受け取る p ractor.take
こんな感じで任意のオブジェクトを渡したり受け取ったりします。
この時に普通にオブジェクトを渡した場合はそのコピーが Ractor へと渡されます。
ractor = Ractor.new { obj = Ractor.receive p obj.__id__ # => 80 } obj = [1, 2, 3] p obj.__id__ # => 60 # obj のコピーを Ractor へと渡す ractor.send obj ractor.take
この時に obj
を freeze
するとコピーされずに Ractor へと渡されます。
ractor = Ractor.new { obj = Ractor.receive p obj.__id__ # => 60 } obj = [1, 2, 3] obj.freeze p obj.__id__ # => 60 # obj はコピーされずに Ractor へと渡される ractor.send obj ractor.take
このように『コピーせずに渡されるオブジェクト』の事を『共有可能オブジェクト』と呼びます。
『共有可能オブジェクト』は Ractor.shareable?
で判定する事ができます。
共有可能オブジェクトの条件は以下になります。
- 不変なオブジェクト
freeze
されているオブジェクト- 整数やシンボルなどはデフォルトで
freeze
されているのでこれに該当する - オブジェクトが参照する要素が全て不変である必要がある
- クラス・モジュール
- その他、特別なオブジェクト
Ractor
オブジェクトなどなど
# 共有可能オブジェクト p Ractor.shareable? 1 # => true p Ractor.shareable? :hoge # => true p Ractor.shareable? nil # => true p Ractor.shareable? false # => true p Ractor.shareable? [1, 2].freeze # => true p Ractor.shareable? "hoge".freeze # => true p Ractor.shareable? Array # => true class X; end p Ractor.shareable? X # => true p Ractor.shareable? Ractor.new {} # => true puts "-------------" # 共有可能オブジェクトではない p Ractor.shareable? [1, 2] # => false p Ractor.shareable? "hoge" # => false p Ractor.shareable? [1, 2, "hoge"] # => false p Ractor.shareable? [1, 2, "hoge"].freeze # => false
共有可能オブジェクト化する
共有可能オブジェクト化する手段はいくつかあるので紹介します。
.freeze
する
一番シンプルなのが .freeze
する方法です。
obj = "hoge" # 共有可能オブジェクトではない p Ractor.shareable? obj # => false # freeze すると共有可能オブジェクトとして扱われる obj.freeze p Ractor.shareable? obj # => true
ただし、配列やハッシュなどは保持している要素全てが .freeze
されている必要があります。
obj = [1, 2, "hoge"] # 共有可能オブジェクトではない p Ractor.shareable? obj # => false # obj だけ freeze してもダメ obj.freeze p Ractor.shareable? obj # => false # 要素も全て freeze されている必要がある obj[2].freeze p Ractor.shareable? obj # => true
Ractor.make_shareable
先程のネストした配列のようなオブジェクトを一発で共有可能オブジェクトにしたい場合には Ractor.make_shareable
が利用できます。
obj = [1, 2, "hoge"] # 共有可能オブジェクトではない p Ractor.shareable? obj # => false # obj の中身を全て freeze する Ractor.make_shareable obj p Ractor.shareable? obj # => true p obj.frozen? # => true p obj[2].frozen? # => true
基本的に任意のオブジェクトを共有可能オブジェクトにしたい場合は Ractor.make_shareable
を利用するといいと思います。
マジックコメントで定数を共有可能オブジェクト化する
専用のマジックコメントを記述しておくことで定数をデフォルトで共有可能オブジェクトとして定義する事ができます。
experimental_everything
: マジックコメント移行の定数定義を共有可能オブジェクトにするexperimental_copy
: 値をコピーを共有可能オブジェクトにして定数を定義するnone
:shareable_constant_value
を無効にするliteral
: 定数定義がリテラルだった場合のみ共有可能オブジェクトにすり
experimental_everything
# マジックコメント以下の定数が共有可能オブジェクトとして定義される # shareable_constant_value: experimental_everything # デフォルトで定数が共有可能オブジェクトになる A = [1, 2, 3] p Ractor.shareable? A # => true # この場合は obj も共有可能オブジェクトになる obj = [1, 2, 3] B = obj p Ractor.shareable? B # => true p Ractor.shareable? obj # => true # id も同じ p obj.__id__ # => 60 p B.__id__ # => 60
experimental_copy
# マジックコメント以下の定数が共有可能オブジェクトとして定義される # 値をコピーしてから定数を定義する # shareable_constant_value: experimental_copy # デフォルトで定数が共有可能オブジェクトになる A = [1, 2, 3] p Ractor.shareable? A # => true # experimental_copy の場合は obj は共有可能オブジェクトにはならない obj = [1, 2, 3] B = obj p Ractor.shareable? B # => true p Ractor.shareable? obj # => false # id も異なる p obj.__id__ # => 60 p B.__id__ # => 80
none
# shareable_constant_value: experimental_everything A = [1, 2, 3] p Ractor.shareable? A # => true # shareable_constant_value の設定を無効にする # shareable_constant_value: none B = [1, 2, 3] p Ractor.shareable? B # => false
literal
# shareable_constant_value: literal # これは OK A = [1, 2, 3] p Ractor.shareable? A # => true # 式を渡した場合にエラーになる # error: `ensure_shareable': cannot assign unshareable object to B (Ractor::IsolationError) B = [1, 2, 3] + [4, 5, 6]
まとめ
と、言う感じで現時点でわたしが把握している情報をまとめてみました。
冒頭にも書きましたが Ractor はまだ開発中なので今後ここに書かれている挙動は変わるかもしれません。
Ractor を実際に使用する場合はオフィシャルなドキュメント等も合わせて参照してください。
参照
【一人 bugs.ruby Advent Calendar 2020】[Feature #16986] Anonymous Struct literal【23日目】
【一人 bugs.ruby Advent Calendar 2020】[Feature #16986] Anonymous Struct literal【23日目】 一人 bugs.ruby Advent Calendar 2020 23日目の記事になります。
[Feature #16986] Anonymous Struct literal
これは Struct.new(:a, :b).new(1, 2)
を ${ a: 1, b: 2 }
のようなリテラルで定義できるようにするチケットです。
s = ${a: 1, b: 2, c: 3} s.a # => 1 s.b # => 2 s.c # => 3
いまはそんなに Struct
は使わないけどこういう記法があるとガンガン使いそうですねー。
例えばこんな感じに雑にダックタイピング呼び出しするメソッドに値を渡す場合とか?
def print(user) pp "#{user.id} #{user.name}" end name = "homu" age = 14 # Struct を経由してメソッド呼び出しされるようにする print(${ name: name, age: age })
Struct
だと obj.value
だけじゃなくて obj[:value]
みたいに添え字アクセスもできるので Hash の代わりとして使用できそうですね。
Hash と違い存在しないキーにアクセスするとエラーになるのは便利そう
# Hash の場合は typo してても気づきづらい user = { name: "homu", age: 14 } # no error user[:nmae] # Struct だと存在しないキーにアクセスするとエラーになる user = Struct.new(:name, :age).new("homu", 14) # error user[:nmae]
あとは ${}
だとブロックと差別化できるので
p { a: 1, b: 2 }
とは書けないんですが p ${ a: 1, b: 2 }
はかける的な。
関係ないけどどういう経緯でチケットが建てられたのか見ているとちょっとおもしろいです
Struct
に関してはこちらのスライドも参照してください。
[今更聞けない! Struct の使い方と今後の可能性について]
【一人 bugs.ruby Advent Calendar 2020】[Feature #17004] Provide a way for methods to omit their return value【22日目】
一人 bugs.ruby Advent Calendar 2020 22日目の記事になります。
[Feature #17004] Provide a way for methods to omit their return value
このチケットは任意のメソッドが戻り値を受け取るか受け取らないかを判定するメソッドの追加を追加するという内容のチケットです。
ということかというと RubyVM.return_value_is_used?
というメソッドを追加し、これをメソッド内で呼び出すと
- 戻り値を受け取る場合:
true
を返す - そうでない場合:
false
を返す
というような判定を行うことができます。
具体的に言うとこんな感じで判定する事ができます。
def hoge if RubyVM.return_value_is_used? pp "戻り値を受け取る" else pp "戻り値を受け取らない" end end hoge # "戻り値を受け取らない" value = hoge # "戻り値を受け取る" Array hoge # "戻り値を受け取る" hoge.nil? # "戻り値を受け取る" # 最後に呼び出したやつも? hoge # "戻り値を受け取る"
この判定メソッドを利用すると次のように『戻り値を受け取らない場合は無駄な処理を省く』事ができます。
class Hash def refresh(key) # 引数を受け取る場合のみ result を設定する if RubyVM.return_value_is_used? result = self[key] end self[key] = nil result end end homu = { name: "homu", age: 14 } homu.refresh(:name) p homu # => {:name=>nil, :age=>14} age = homu.refresh(:age) pp homu # => {:name=>nil, :age=>nil} pp age # => 14
class User < ActiveRecord::Base def update_name(name) update!(name: name) # reload した値を返す reload if RubyVM.return_value_is_used? end end
これは面白いアプローチですね。
ただし、次のように戻り値になる場合は『戻り値を受け取る』ことになるので注意。
def hoge if RubyVM.return_value_is_used? pp "戻り値を受け取る" else pp "戻り値を受け取らない" end end def foo hoge end foo # "戻り値を受け取る" def bar hoge nil end bar # "戻り値を受け取らない"
便利そうっちゃ便利そうだけどメソッドごとに RubyVM.return_value_is_used?
で処理を分岐するのはめっちゃきつそう…。
実際には極端に重くなるようなメソッドぐらいで使いそうな気がするけど…どうだろうか。