今週の気になった bugs.ruby

書き溜めてはいたんですが、ブログに公開するのを忘れてました。
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。

[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 } とはかける
  • リテラルじゃなくて {a: 1, b: 2}.to_struct みたいな変換メソッドがある方が便利そう?
  • Hash リテラルと比較して以下の部分が気になる
  • label: expr みたいな定義のみ許可されていて ${**h} みたいなのは許可されていない
# Symbol でない値をキーにできるか(Symbol だけ?)
${ "a" => 1 }
${ 1 => 1 }

# 変数をキーにできるか
key = :a
${ key => 1 }

# `**` で Hash が展開できるのか
hash = { a: 1, b: 2 }
${ c: 3, **hash }

[Feature #13067] TrueClass,FalseClass to provide === to match truthy/falsy values.

  • TrueClass#=== FalseClass#=== を定義する提案
  • true ==== objfalse === obj したときに objtruthyfalsy か判定する
    • nil false だったら falsy、それ以外なら truthy
  • ary.grep(true) みたいなことができる
  • 以下の case ~ when だと挙動が変わってしまう
def normalize_hsts_options(options)
  # options が nil の場合 case false にマッチしてしまう
  case options
  when false
    # ...
  when nil, true
    # ...
  else
    # ...
  end
end
  • ary.grep(true)ary.grep(false)ary.select(&:itself)ary.reject(&:itself) で置き換えられる
  • 互換性の面からクローズされた

[Feature #16985] Improve pp for Hash and String

  • ppHashString の出力をよくしようとするチケット
pp({hello: 'My name is "Marc-André"'})
# 現状
# => {:hello=>"My name is \"Marc-André\""}
# 提案
# => {hello: 'My name is "Marc-André"'}
  • 普通に便利そう

[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? で処理を分岐するのはめっちゃきつそう
    • 実際には極端に重くなるようなメソッドぐらいで使いそうな気がするけど…どうだろう
    • 別のアプローチはないかな…

Ruby でメソッドの戻り値を受け取るかどうかを判定する RubyVM.return_value_is_used? が面白そう

こんな深夜に Ruby で面白そうなチケットを見かけたのでいろいろと試してみました。

注意

  • これはまだ開発中の機能になり今後挙動が変わる可能性があります
  • またこの機能はまだ議論されている途中で本体に組み込まれるか未定です

RubyVM.return_value_is_used? とは

RubyVM.return_value_is_used? は呼び出したメソッドの戻り値が受け取られるのか受け取られないのかを判定します。
例えば def hoge の中で RubyVM.return_value_is_used? を呼び出した場合、 hoge の戻り値が変数等に代入されれば true を返し、そうでなければ false を返します。
使い方は以下のような感じ

def hoge
  if RubyVM.return_value_is_used?
    pp "戻り値を受け取る"
  else
    pp "戻り値を受け取らない"
  end
end

# 戻り値を受け取らない
hoge          # "戻り値を受け取らない"

# 変数に代入する
value = hoge  # "戻り値を受け取る"

# 引数に渡す
Array hoge    # "戻り値を受け取る"

# 戻り値をチェーンする
hoge.nil?     # "戻り値を受け取る"

# 最後に呼び出したやつも?
hoge          # "戻り値を受け取る"

使い方自体はそこまで難しくなさそうですね。
ただし、以下のように書くと意図せず『戻り値を受け取る』ことになるので注意する必要はありそうです。

def hoge
  if RubyVM.return_value_is_used?
    pp "戻り値を受け取る"
  else
    pp "戻り値を受け取らない"
  end
end

def foo
  # hoge の戻り値をそのまま返しているので "戻り値を受け取る" ことになる
  hoge
end
foo    # "戻り値を受け取る"

def bar
  # hoge の戻り値は受け取っていないので "戻り値を受け取らない" ことになる
  hoge
  nil
end
bar    # "戻り値を受け取らない"

不要に戻り値を返さないように注意する必要がありそうですね。

ユースケース

いくつかユースケースを考えてみました。

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

こんな感じで副作用を伴うメソッドだと割と多用しそうです。

所感

アプローチとしては面白いですね。
他の言語とかにも似たような機能ってあるんだろうか? 利用できそうなケースは割とありそうだと思いつつ、毎回メソッドを定義するたびに RubyVM.return_value_is_used? を使って処理を分岐するのは結構しんどそうな気がしました。
実際に使う場合はボトルネックになりそうなケースでのみ使用しそうですかね?使いこなすのがむずかしそう。
すでにチケットや PR のコメントが盛り上がっていますが、導入されると結構な目玉機能になりそうですねー。

Ruby で 1 == true を行うと何が起きるのか

元ネタ

まあ 1true は別オブジェクトだからなーと思いつつ 1 == true は例外を返してほしいなーとか 1 == truetrue の方がいいんじゃね?と思ったりしたのでそもそも 1 == がどういう挙動なのかみてみました。

1 == obj を行うと obj.== が呼ばれる

1 == obj みたいな比較を行うと obj が数値でない場合は obj.== を呼び出し、それで比較を行います。

class X
  def ==(other)
    true
  end
end

x = X.new

# 内部で x == 1 が呼び出される
p 1 == x
# => true

またこの時に注意するのは 1 == xx == 1 が等価ではないことです。
具体的にいうと戻り値が異なります。

class X
  def ==(other)
    "X#=="
  end
end

x = X.new

p 1 == x   # => true
p x == 1   # => "X#=="

1 == true するとどうなるのか

では 1 == true を行うとどうなるのかというと内部で true == 1 が呼ばれます。
なので本来は TrueClass#== が呼ばれるはずなんですが TrueClass#== は定義されておらず、実際には親クラスの BasicObject#== が呼び出され結果的に object_id で比較を行います。
むずかしい。
また、次のように TrueClass#== を書き換えると結果を書き換える事ができます。

class TrueClass
  def ==(other)
    !!other
  end
end

p 1 == true     # => true

まあこれはこれで?

まとめ

  • 1 == obj すると obj.== が呼ばれる
  • なので 1 == obj がどうなるのかは obj.== の実装に依存する
  • TrueClass/FalseClass では #== が定義されておらず object_id で比較が行われる

余談

個人的に 1 == "hoge" みたいなのは例外になって欲しいなーと思っていたんですが、この場合は "hoge".== が呼ばれ String#== の実装に依存します。
で、 String#== の実装は比較できない場合は例外が発生するのではなくて false を返すようになっています。
流石にこれの挙動を変えるのは互換性の面から見てむずかしそうな気がする…。

ちなみに < などで比較すると例外が発生します。

# error: comparison of Integer with true failed (ArgumentError)
1 < true
1 <= false
1 > "hoge"

今週の気になった bugs.ruby

内容は適当です。 今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。

[Bug #11669] inconsitent behavior of refining frozen class

  • freeze されたクラスに対して Refinements で新しいメソッドを定義するとエラーになっていた
class X
  def foo
  end
end
X.freeze

using Module.new {
  refine X do
    # OK
    def foo
    end

    # error: can't modify frozen class: X (FrozenError)
    def bar
    end
  end
}
  • Ruby 2.8/3.0 からこれがエラーにならずに定義できるようになる

[Bug #9573] descendants of a module don't gain its future ancestors, but descendants of a class, do

  • モジュールの mixin が ancestors に反映されないケースがあった
  • 以下のように挙動が変更される
module M1
end

module M2
end

module M3
end

class C
end

p C.ancestors
#=> current: [C, Object, Kernel, BasicObject]
#=> fixed:   [C, Object, Kernel, BasicObject]

C.prepend M1

p C.ancestors
#=> current: [M1, C, Object, Kernel, BasicObject]
#=> fixed:   [M1, C, Object, Kernel, BasicObject]

M1.prepend M2

p C.ancestors
#=> current: [M1, C, Object, Kernel, BasicObject]
#=> fixed:   [M2, M1, C, Object, Kernel, BasicObject]

M2.prepend M3

p C.ancestors
#=> current: [M1, C, Object, Kernel, BasicObject]
#=> fixed:   [M3, M2, M1, C, Object, Kernel, BasicObject]

[Feature #15822] Add Hash#except

  • Hash#except が標準ライブラリ入りした
user = { id: 1, name: "homu", age: 14 }

# 引数の key を除いた Hash を返す
pp user.except(:name, :age)
# => {:id=>1}
  • ちょうど最近使いたいケースがあったのでめでたい
  • ActiveSupport には Hash#except! があるがこっちは標準には入ってない
    • これは Hash#slice! も同様

[Bug #16983] Bug #16983: RubyVM::AbstractSyntaxTree.of(method) returns meaningless node if the method is defined in eval - Ruby master - Ruby Issue Tracking System

  • RubyVM::AbstractSyntaxTree.ofeval で定義したメソッドオブジェクトを渡すと意図しない値が返ってくる、という報告
  • 以下はチケットに書いてあったコード
p 'blah'

eval <<~RUBY, binding, __FILE__, __LINE__ + 1
  def foo
  end
RUBY

method = method(:foo)
# ここで意図しない値が返ってくる
pp RubyVM::AbstractSyntaxTree.of(method)
# => (STR@3:5-3:12 "def foo\n" + "end\n")
  • p 'blah' があるかないかでも結果が変わってくるのでよくわからない…
  • 最近 RubyVM::AST をいじってるけど eval は試してなかったので知らなかった

Ruby のクラス変数とクラスのインスタンス変数の違い

Ruby のクラス変数とクラスのインスタンス変数の違いの覚書。

クラス変数

Ruby@@変数名 で変数を定義するとクラス変数として定義されます。
また、この変数は継承したクラスでも参照する事ができます。

class X
  @@class_variable = "hoge"
end

class Y < X
  pp @@class_variable
  # => "hoge"
end

クラスのインスタンス変数

Ruby ではクラスもインスタンスオブジェクトなのでインスタンス変数を定義する事ができます。
これは『継承したクラス』では参照することはできません。

class X
  @instance_variable = "hoge"
end

class Y < X
  pp @instance_variable
  # => nil
end

クラスのインスタンス変数の方が参照するスコープが小さいので意図しない場合はクラス変数よりもインスタンス変数で定義したほうが安全ですね。

Ruby の標準ライブラリに Hash#except が追加された

Ruby の標準ライブラリに Hash#except が追加されました。
ActiveSupport にある有名なやつですね。
特に問題がなければ Ruby 2.8/3.0 で追加される予定です。

チケット

コード

user = { id: 1, name: "homu", age: 14 }

# 引数の key を除いた Hash を返す
pp user.except(:name, :age)
# => {:id=>1}

最近ちょうど Hash#except が欲しいケースがあったので標準入りしたのは素直にうれしいですね。

今週の気になった bugs.ruby

ちょっと遅れましたが貯めてはいました。
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。

[Feature #15973] Let Kernel#lambda always return a lambda

  • lambda(&proc {}).lambda? の戻り値が false になるので true にしよう、という提案
  • Ruby 2.7 現在では以下のような挙動になっている
# lambda にブロックを渡す
# OK: true が返る
p lambda {}.lambda?
# => true

# lambda に lambda を渡す
# OK: true が返る
p lambda(&lambda {}).lambda?
# => true

# lambda に proc を渡す
# NG: false が返る
p lambda(&proc {}).lambda?
# => false
  • 議論は長いんですが結果的には以下のように対応することになったみたい
  • この対応はすでに ruby-dev にて実装済
# ruby-dev での挙動

# no warning
lambda {}

# warning: lambda without a literal block is deprecated; use the proc without lambda instead
lambda(&lambda {})

# warning: lambda without a literal block is deprecated; use the proc without lambda instead
lambda(&proc {})

# warning: lambda without a literal block is deprecated; use the proc without lambda instead
lambda(&method(:puts))

[Feature #12901] Anonymous functions without scope lookup overhead

  • Proc などを定義する際に『外部のスコープを参照しないこと』を明示化することでオーバーヘッドをなくしパフォーマンスが向上させるチケット
    • チケット自体は3年前につくられた
# scope: false をキーワード引数で渡すことで『キャプチャしないこと』を明示化
Proc.new(scope: false) {|var| puts var }

# これは以下のようにメソッドを定義したときと同じ意味
def anon(var)
  puts var
end


# 動作例
var = "hello"
Proc.new(scope: false) { puts var }.call
# => NameError: undefined local variable or method `var' for main:Object
  • 内容がヘビーなのでなかなか追いきれてない…
    • bindingself が絡んでくると最適化するのがむずかしい、みたいな議論がされてるぽい?
    • self をキャプチャしない場合は puts some_expression が動かないよね?みたいなことも言われてる
  • Guild/Ractor には外部スコープを参照しないようにする Proc#isolate というメソッドがあるらしい?
  • レシーバをキャプチャしない場合は UnboundMethod に近いのでは?というコメントも
# こういう構文だとどうか
plus = def->(a) = self + a
plus.bind_call(1, 2) #=> 3
plus_1 = plus.bind(1)
plus_1.call(11) #=> 12

[Feature #6869] Do not treat _ parameter exceptionally

  • def hoge(a, a) end みたいに同名の仮引数を定義すると duplicated argument name とエラーになる
  • しかし def hoge(_, _) end のように _ を仮引数としてつかった場合はエラーにならない
  • _ が特別な挙動になるのはやめましょう、とう提案
# OK
def hoge(_, _) end

# NG: duplicated argument name
def hoge(a, a) end
  • チケット自体は 8年前につくられていて最近コメントされていたので読みました
  • _ が特別な意味を持つのがもにょるのはわかるんですがどうなんでしょうね

[Feature #16954] A new mode Warning[:deprecated] = :error for 2.7

  • Warning[:deprecated] = :errordeprecated な警告をエラーにするか警告にするかを制御できるようにする提案
  • 挙動としては以下のような感じ
    1. Warning[:deprecated] = :error, to make the warning into an error which produces a full backtrace (and stops the execution).
    2. Warning[:deprecated] = :debug, to make the warning print a full backtrace (and continues the execution).
  • このあたりがユーザ側で制御できるのはいいのではなかろうか?
  • ちなみにコメントに書いてあったんですが、以下のようにすると Ruby だけで実装することもできるらしい
require 'warning' # warning gem
Warning.process do |message|
  if message =~ /: warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated)\n\z/
    if false # :error
      raise message
    else # :debug
      $stderr.puts message
      $stderr.puts caller
    end
  else
    $stderr.puts message
  end
end

def a(a, b: 1); end
a(b: 2) # keyword to positional

def a(a=1, b: 1); end
a({b: 1, 'a'=>1}) # split positional
a(b: 1, 'a'=>1) # split keyword

def a(b: 1); end
a({b: 1}) # positional to keyword
  • こういう感じでハックできるのはいろいろと捗りそう