【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

[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) } }

[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.

  • OpenStructYAML への変換をサポートしたチケット
  • すでに OpenStruct 側で取り込まれており Ruby 2.7.2 でも使えた
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

$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 されたチケット

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

  • irbmeasure というコマンドが追加された
  • これは 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

[Bug #17398] SyntaxError in endless method

  • Ruby 3.0 のエンドレスメソッド定義で次のような定義の場合シンタックスエラーになる、という旨のチケット
# 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.receiveRactor.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

この時に objfreeze するとコピーされずに 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? で処理を分岐するのはめっちゃきつそう…。
実際には極端に重くなるようなメソッドぐらいで使いそうな気がするけど…どうだろうか。

参照