2022/05/12 今回の気になった bugs.ruby のチケット

[Bug #18768] Inconsistent behavior of IO, StringIO and String each_line methods when return paragraph and chomp: true passed

"a\n\nb\n\nc\n".each_line("", chomp: true).to_a
#=> ["a\n", "b\n", "c\n"]

StringIO.new("a\n\nb\n\nc\n").each_line("", chomp: true).to_a
#=> ["a\n", "b\n", "c"]

File.open('chomp.txt').each_line("", chomp: true).to_a
#=> ["a", "b", "c\n"]
  • chomp.txt の中身
File.read('chomp.txt')
#=> "a\n\nb\n\nc\n"
  • String#each_line の挙動は以下の通り
# \n で区切りつつ末尾の \n を取り除く
p "a\n\nb\n\nc\n".each_line(chomp: true).to_a
#=> ["a", "", "b", "", "c"]

# `\n\n` 区切りで分割する
p "a\n\nb\n\nc\n".each_line("").to_a
#=> ["a\n\n", "b\n\n", "c\n"]
  • これはどれが期待する挙動になるんですかね…

いまさら聞けない!波ダッシュと全角チルダ問題についてまとめてみた

元々のきっかけは以下のように RubyUTF-8 の波文字を SJIS に変換しようとしたらエラーになってしまいました。

# UTF-8 の 〜 文字を SJIS に変換するとエラーになる
# error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)
"".encode("SJIS")

これの原因を調べてみたら 波ダッシュと全角チルダ問題 にたどり着いたのでその問題と歴史についてまとめてみました。

諸注意

波ダッシュと全角チルダ問題 に関しては一般的な話なんですが、変換ルールやエンコーディングの指定の仕方などは CRuby での話になります。 他の言語や処理系だと結果が変わるかもしれないので注意してください。 また、この記事でのソースコードはすべて UTF-8 になります。
それでは本題に入っていきましょう。

RubyUTF-8 の波文字()を SJIS に変換するとエラーになる

冒頭にも記述したように元々この問題を調べようとしたきっかけは次のように RubyUTF-8 の波文字()を SJIS に変換しようとするとエラーになってしまうことでした。

# UTF-8 の 〜 文字を SJIS に変換するとエラーになる
# error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)
"".encode("SJIS")

上記のコードで変換しようとした波文字()は Unicode のコードポイントだと U+301C になるんですが、その文字が SJIS への変換に失敗 しています(ちなみに Ruby では SJISWindows-31Jエイリアスとして定義されています)。

# Ruby で Unicode のコードポイントを表示する
pp "".unpack1("U*").to_s(16)
# => "301c"

SJIS にも波文字自体は存在しているのに不思議ですよね? そもそも波文字とはどういう文字なのでしょうか。 Unicode の波文字について詳しくみてみましょう。

波文字は2つあった!

実は Unicode には波文字を表す文字が2つ存在しています。 それが『波ダッシュ文字(U+301C)』と『全角チルダ文字(U+FF5E)』になります。
最初に Ruby のコードで記述していた波文字は 波ダッシュ文字(U+301C) になります。

文字 意味 Unicode のコードポイント UTF-8 のバイト列
波ダッシュ(WAVE DASH) U+301C \xE3809C
全角チルダ(FULLWIDTH TILDE) U+FF5E \xEFBD9E

この2つの文字は見た目上は似た文字として表示されていると思うのですが、実際には全く異なる文字になります。 試しにここに書いてある文字をバイナリエディタにコピペしてみたり Ruby でコードポイントを取得してみると全然違う文字だと言うことがわかると思います。

# Unicode のコードポイントを出力してみる
# これは波ダッシュ
pp "".unpack1("U*").to_s(16)
# => "301c"

# これは全角チルダ
pp "".unpack1("U*").to_s(16)
# => "ff5e"

このように が波ダッシュ(U+301C) が全角チルダ(U+FF5E)全く異なる文字になります。
この『Unicode には波文字が2種類存在する事』が SJIS に変換できない問題と深く関わってきます。

全角チルダ (U+FF5E) は SJIS に変換できる

先程紹介した Unicode の2つの波文字なのですが 波ダッシュ (U+301C)SJIS に変換するとエラーになることはわかりました。

# これは波ダッシュ
nami = ""
pp nami.unpack1("U*").to_s(16)
# => "ff5e"

# SJIS に変換するとエラーになる
pp nami.encode("SJIS")
# => "\x{8160}"

しかし、実は 全角チルダ (U+FF5E)SJIS に変換する事ができます。

# これは全角チルダ
tilde = ""

pp tilde.unpack1("U*").to_s(16)
# => "ff5e"

# SJIS に変換する事ができる!
pp tilde.encode("SJIS")
# => "\x{8160}"

これは一体どういうことなのでしょうか。

波ダッシュ (U+301C) を Shift_JIS に変換すると…

ここでは変換後のエンコーディングの話をします。
Ruby では SJIS と名前が似ている Shift_JIS というエンコーディングも存在しています。 実は SJIS ではなくて Shift_JIS であれば UTF-8波ダッシュ (U+301C)Shift_JIS に変換することができます。

# これは波ダッシュ
nami = ""

pp nami.unpack1("U*").to_s(16)
# => "301c"

# Shift_JIS に変換する事ができる!
pp nami.encode("Shift_JIS")
# => "\x{8160}"

また逆に UTF-8全角チルダ (U+FF5E)Shift_JIS に変換すると エラー になります。

# これは全角チルダ
tilde = ""

pp tilde.unpack1("U*").to_s(16)
# => "ff5e"

# SJIS に変換するとエラーになる
# error: `encode': U+FF5E from UTF-8 to Shift_JIS (Encoding::UndefinedConversionError)
pp tilde.encode("Shift_JIS")

Ruby の変換ルールとしては以下のようになっています。

変換前の UTF-8 の文字 SJIS Shift_JIS
波ダッシュ (U+301C) 変換できない 変換できる
全角チルダ (U+FF5E) 変換できる 変換できない

次は SJISShift_JIS の違いについて調べてみましょう。

Ruby における SJISShift_JIS の違い

ここでは Ruby における SJISShift_JIS の違いについて簡単に解説します。
まず Shift_JIS はその名の通り文字コードShift_JIS のことを指しています。 Shift_JIS とは日本語を含む文字列を表現するために用いられる文字コードになります。 また Shift_JISJIS X 0208 として標準化されています。

Ruby ではこの文字コードShift_JIS として利用できます。

次に SJIS なのですが Rubyエンコーディングにおいて SJISCP932 Windows-31J と同じエンコーディングとして定義されています。 Windows-31J とは MicrosoftShift_JIS を独自拡張したエンコーディングになります。
例えば という文字は Shift_JIS には存在しませんが Windows-31J には存在する文字になります。

# Windows-31J に変換可能
pp "".encode("Windows-31J")   # => "\x{8740}"

# SJIS や CP932 も同様
pp "".encode("SJIS")   # => "\x{8740}"
pp "".encode("CP932")   # => "\x{8740}"

# Shift_JIS には変換できない
# error: U+2460 from UTF-8 to Shift_JIS (Encoding::UndefinedConversionError)
pp "".encode("Shift_JIS")

なので厳密に言うと SJISShift_JIS異なるエンコーディング になります。

まとめると Ruby において SJISShift_JIS は以下のような関係になっています。

より詳しい違いが知りたい人は以下を参照してください。

以降は SJIS という表記だとややこしいので CP932 と表記し『 CP932Shift_JIS 』について解説していきます。

JIS X 0208 の波文字とは

ここでは Shift_JISCP932 の規格である JIS X 0208 の話をします。 実は JIS X 0208 で定義されている波文字は Unicode とは違って『波ダッシュ文字(\x8160)だけ』が存在しています。 (JIS X 0208 を更に拡張した JIS X 0213 には全角チルダも定義されているんですがここでは一旦置いておきます。
エンコーディングの波文字の情報は以下のようになります。

文字 Unicode のコードポイント UTF-8 のバイト列 JIS X 0208 のバイト列
波ダッシュ(WAVE DASH) U+301C \xE3809C \x8160
全角チルダ(FULLWIDTH TILDE) U+FF5E \xEFBD9E ない

なので UTF-8波ダッシュShift_JIS に変換すると Shift_JIS波ダッシュに変換されるのは期待する挙動と言えます。

# 波ダッシュを Shift_JIS に変換すると Shift_JIS の波ダッシュになる
p "\u301C".encode("Shift_JIS")   # => "\x{8160}"

では、なぜ UTF-8 の波文字を CP932 に変換するときに『対応する文字がある波ダッシュがエラー』になって『対応する文字がない全角チルダがエラー』になるのでしょうか。

# 全角チルダを CP932 に変換するとなぜか JIS X 0208 の波ダッシュになる
p "\uFF5E".encode("CP932")     # => "\x{8160}"

# 波ダッシュを CP932 に変換するとJIS X 0208 の波ダッシュは存在するのにエラーになる
# error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)
p "\u301C".encode("CP932")   # => "\x{8160}"

この疑問を解消するためにまず JIS X 0208波ダッシュ、つまり『 CP932Shift_JIS から UTF-8 に変換するとどうなるのか』を試してみましょう。

JIS X 0208波ダッシュUTF-8 に変換するとどうなる?

今度は逆に JIS X 0208波ダッシュUTF-8 に変換するとどうなるのか見てみましょう。 実際に RubyShift_JISCP932 から波ダッシュUTF-8 に変換してみます。
Ruby で変換する場合は JIS X 0208波ダッシュ文字をバイト列で定義し String#encode に変換元となるエンコーディングの情報として CP932Shift_JIS をそれぞれ指定して UTF_8 に変換してみます。

# CP932 でも Shift_JIS でもバイト列は同じ
jis_nami = "\x81\x60"

# CP932 -> UTF_8 に変換すると全角チルダになる
pp jis_nami.encode("UTF-8", "CP932").unpack1("U*").to_s(16)
# => "ff5e"

# Shift_JIS -> UTF_8 に変換すると波ダッシュになる
pp jis_nami.encode("UTF-8", "Shift_JIS").unpack1("U*").to_s(16)
# => "301c"

このように Shift_JIS -> UTF_8 は『同じ文字への変換』になっています。 しかし CP932 -> UTF_8 は『異なる文字への変換』になっていることがわかります。

文字コード 変換前 UTF-8 へ変換
CP932 波ダッシュ 全角チルダ
Shift_JIS 波ダッシュ 波ダッシュ

この『 CP932波ダッシュUTF-8 に変換したときに誤った文字になってしまうこと』がいわゆる 波ダッシュ問題 として一般的に扱われている問題になります。
では、なぜこのような変換になってしまったのでしょうか。 Unicode の歴史を調べてみましょう。

Unicode波ダッシュの例示字形が間違っていた

結論からいうと Unicode 7.0 で定義されていた波ダッシュ例示字形 が間違った形で記載されてしまっていたのが要因になっているようです。
どういうことかと言うと現在の Unicode では波ダッシュも全角チルダ のように 上がって下がる ような字形になります。 これは JIS X 0208 も同様の字形になっています。
しかし Unicode 7.0 では波ダッシュが以下のように 下がって上がる という文字として例示字形が記載されていました。

文字コード 波ダッシュ 全角チルダ
Unicode 7.0
※これが間違い
Unicode 8.0
JIS X 0208

実際に Unicode のコードチャートにも以下のように記載されています。

Unicode 7.0

Unicode 8.0

WAVE DASH の例示字形が異なる形になっているのがわかると思います。 これが CP932 -> UTF-8 に変換した際に 波ダッシュ -> 全角チルダ となってしまっている要因になっています。
この Unicode 7.0Unicode 8.0波ダッシュの例示字形の問題に関しては以下の記事で詳しく解説されているので気になる人は読んでみるとよいと思います。

問題は逆だった

ここからは仮説になってくるのですが先程説明したように JIS X 0208波ダッシュUnicode 7.0波ダッシュでは字形が異なっていました。 なのでそのまま JIS X 0208波ダッシュUTF-8波ダッシュに変換してしまうと 文字としては同じ なのですが 見た目としては違う文字 に見えてしまいます。
この問題を回避するために Windows では JIS X 0208波ダッシュUTF-8 に変換する時に『 JIS X 0208波ダッシュと同じ字形であった全角チルダに変換する』というルールができたのではないかと考えられます。 これにより Windows で使われている CP932 は『波ダッシュ -> 全角チルダ』という変換になり、逆に JIS X 0208 と同等である Shift_JIS はそのままの文字である『波ダッシュ -> 波ダッシュ』になっているのではなかろうかと思います。

文字コード 変換前 UTF-8 へ変換
CP932 波ダッシュ 全角チルダ
Shift_JIS 波ダッシュ 波ダッシュ


逆に UTF-8 -> CP932 と変換するときも同様に『 全角チルダ -> 波ダッシュ 』となり UTF-8 -> Shift_JIS だと『 波ダッシュ -> 波ダッシュ 』になっているのだと予想しています。

文字コード 変換前 CP932 へ変換 Shift_JIS へ変換
UTF-8 波ダッシュ エラー 波ダッシュ
UTF-8 全角チルダ 波ダッシュ エラー

まとめ

文字 意味 Unicode のコードポイント UTF-8 のバイト列
波ダッシュ(WAVE DASH) U+301C \xE3809C
全角チルダ(FULLWIDTH TILDE) U+FF5E \xEFBD9E
変換前の文字コード 変換後の文字コード 変換前の文字 変換後の文字
UTF-8 CP932 波ダッシュ(U+301C) エラー
UTF-8 CP932 全角チルダ(U+FF5E) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 波ダッシュ(U+301C) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 全角チルダ(U+FF5E) エラー
CP932 UTF-8 波ダッシュ(\x8160) 全角チルダ(U+FF5E)
Shift_JIS UTF-8 波ダッシュ(\x8160) 波ダッシュ(U+301C)

参照

余談:RubyUnicode波ダッシュSJIS波ダッシュに変換する

以下のように #encodefallback オプションで制御する事が可能です。

pp "".encode("SJIS", fallback: { "\u301C" => "\x81\x60".force_encoding("SJIS") })

別解としては #tr波ダッシュを全角チルダに変換してから #encode するやり方も考えられます。

pp "".tr("\u301C", "\uFF5E").encode("SJIS")

余談: iconv の変換ルール

iconv だと次のような変換ルールとなっています。

変換前の文字コード 変換後の文字コード 変換前の文字 変換後の文字
UTF-8 CP932 波ダッシュ(U+301C) 波ダッシュ(U+301C) ※これが Ruby と異なる
UTF-8 CP932 全角チルダ(U+FF5E) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 波ダッシュ(U+301C) 波ダッシュ(\x8160)
UTF-8 Shift_JIS 全角チルダ(U+FF5E) エラー
CP932 UTF-8 波ダッシュ(\x8160) 全角チルダ(\xEFBD9E)
Shift_JIS UTF-8 波ダッシュ(\x8160) 波ダッシュ(\xE3809C)

以下、実行ログ。

# utf-8 の波ダッシュ -> cp932 は変換できる
$ echo -n "〜" | iconv -f utf-8 -t cp932 | od -tx1 -An
 81 60

# utf-8 の全角チルダ -> cp932 は変換できる
$ echo -n "~" | iconv -f utf-8 -t cp932 | od -tx1 -An
 81 60
# utf-8 の波ダッシュ -> shift_jis は変換できる
$ echo -n "〜" | iconv -f utf-8 -t shift_jis | od -tx1 -An
 81 60

# utf-8 の全角チルダ -> shift_jis は変換できない
$ echo -n "~" | iconv -f utf-8 -t shift_jis | od -tx1 -An
iconv: 位置 0 に不正な入力シーケンスがあります
# cp932 -> utf-8 は全角チルダ
$ echo -n $'\x81\x60' | iconv -f cp932 -t utf-8 | od -tx1 -An
 ef bd 9e

# shift_jis -> utf-8 は波ダッシュ
$ echo -n $'\x81\x60' | iconv -f shift_jis -t utf-8 | od -tx1 -An
 e3 80 9c

UTF-8 -> CP932 の変換だけ Ruby と異なっています。

2022/05/05 今回の気になった bugs.ruby のチケット

今週は Array#undigits の提案がありました。

[Feature #18762] Add an Array#undigits that compliments Integer#digits

class Array
  def undigits(base = 10)
    each_with_index.sum do |digit, exponent|
      digit * base**exponent
    end
  end
end

pp 42.digits
# => [2, 4]

pp 42.digits.undigits
#=> 42

# 16進数で変換
pp 42.digits(16)
# => [10, 2]

pp 42.digits(16).undigits(16)
#=> 42
  • より厳密に提示されている Ruby の実装コード
class Array
  def undigits(base = 10)
    base_int = base.to_int
    raise TypeError, "wrong argument type #{base_int.class} (expected Integer)" unless base_int.is_a?(Integer)
    raise ArgumentError, 'negative radix' if base_int.negative?
    raise ArgumentError, "invalid radix #{base_int}" if base_int < 2

    each_with_index.sum do |digit, exponent|
      raise MathDomainError, 'out of domain' if digit.negative?

      reverse.reduce(0) do |acc, digit|
        acc * base + digit
      end
    end
  end
end
  • あると便利なんですかね

2022/04/29 今回の気になった bugs.ruby のチケット

今週は右代入を TracePoint でフックする場合のバグ報告がありました。

[Bug #18753] lineno= is not returning an integer

  • ARGF.send(:lineno=, 1) の戻り値が nil になっているというバグ報告
# これは 1 を返す
p (ARGF.lineno=1)
# => 1

# これは nil を返す
p ARGF.send(:lineno=, 1)
# => nil
  • この問題は最新版では修正済み

[Bug #18752] defined? String.instance_method(:abcdefg) will return a "method" string instead nil.

  • defined? String.instance_method(:abcdefg) した時に nil を返すことを期待するが "method" が返るというバグ報告
p defined? String.instance_method(:abcdefg)
# => nil

p defined? String.instance_method(:abcdefg111111111111)
# => nil
  • これは defined?String.instance_method の呼び出しに対してチェックしているので期待する挙動になる
  • 逆に定義されてないメソッドを呼び出そうとすると nil を返す
p defined? String.abcdefg111111111111
# => nil

[Bug #18629] block args array splatting assigns to higher scope _ var

  • 以下のように仮引数が _ でない場合はスコープの外の変数は書き換わらないが _ の場合は外のスコープの変数が書き換わってしまうというバグ報告
# これは書き換わらない
v = 1; [[2]].each{ |(v)| }; p v   # => 1

# これは書き換わってしまう
_ = 1; [[2]].each{ |(_)| }; p _   # => 2
  • これは最新版で修正済み

Bug #18740 Use of rightward assignment changes line number needed for line-targeted TracePoint(https://bugs.ruby-lang.org/issues/18740)

  • 次のように右代入を使用している際に TracePoint でフックできないというバグ報告
def foo
  (1..)
      .lazy
      .filter { _1.even? }
      .take(10)
      .to_a => result

  puts result
end

# foo メソッドの 2行目の式に対して TracePoint をフックする
# error: `enable': can not enable any hooks (ArgumentError)
TracePoint.new(:line) { puts 'Hi' }.enable(target: RubyVM::InstructionSequence.of(method :foo), target_line: 2)
foo
  • 右代入ではなくて左代入だと問題なく動作する
def foo
  result = (1..)
      .lazy
      .filter { _1.even? }
      .take(10)
      .to_a

  puts result
end

# foo メソッドの 2行目の式に対して TracePoint をフックする
# OK
TracePoint.new(:line) { puts 'Hi' }.enable(target: RubyVM::InstructionSequence.of(method :foo), target_line: 2)
foo
def foo
  (1..)
      .lazy
      .filter { _1.even? }
      .take(10)
      .to_a => result   # <- この行数を指定すると OK

  puts result
end

# foo メソッドの 2行目の式に対して TracePoint をフックする
# OK
TracePoint.new(:line) { puts 'Hi' }.enable(target: RubyVM::InstructionSequence.of(method :foo), target_line: 6)
foo
  • 知らなかったので知見
  • これがバグなのか仕様なのかは現時点では未定義ぽい

2022/04/21 今回の気になった bugs.ruby のチケット

今週はレシーバに対して Kernel#p が呼び出せるようにする提案がありました。

[Feature #18736] self-p for method chain

  • 次のようにメソッドチェーンの間にレシーバを出力したいという要望チケット
class Object
  def sp(method=nil, *args, &block)
    if method
        Kernel.p self.public_send(method, *args, &block)
    elsif block_given?
        Kernel.p block.call(self)
    else
      Kernel.p self
    end
    return self
  end
end

# sp のレシーバを標準出力する
p [1,2,3].map{|x| x**2}.sp.map{|x| x**2}
# output:
# [1, 4, 9]
# [1, 16, 81]

# ブロックの結果を出力する
[1,2,3].sp{|x| "my List = #{x}"}
# output:
# "my List = [1, 2, 3]"

# レシーバに対して sum(-10) を呼び出した結果を出力する
[1,2,3].sp(:sum,-10)
# output:
# -4
  • #tap すると以下のように書くことができる
# sp のレシーバを標準出力する
p [1,2,3].map{|x| x**2}.tap(&method(:p)).map{|x| x**2}
# output:
# [1, 4, 9]
# [1, 16, 81]

# ブロックの結果を出力する
[1,2,3].tap{|x| p "my List = #{x}"}
# output:
# "my List = [1, 2, 3]"

# レシーバに対して sum(-10) を呼び出した結果を出力する
[1,2,3].tap { p _1.sum(-10) }
# output:
# -4

[Bug #18729] Method#owner and UnboundMethod#owner are incorrect after using Module#public/protected/private

  • 次のように public / protected / private を呼び出した後に #owner が変わってないというバグ報告
class A
  protected def foo
    :A
  end
end

class B < A
  p [instance_method(:foo), instance_method(:foo).owner, instance_methods(false), A.instance_methods(false)]
  public :foo
  p [instance_method(:foo), instance_method(:foo).owner, instance_methods(false), A.instance_methods(false)]
end
# outptu:
# [#<UnboundMethod: B(A)#foo() owner.rb:2>, A, [], [:foo]]
# [#<UnboundMethod: B(A)#foo() owner.rb:2>, A, [:foo], [:foo]]
  • Bfoo が再定義されているのに ownerB(A)#foo() のままになっている
  • 期待する挙動は以下の通り
[#<UnboundMethod: B(A)#foo() owner.rb:2>, A, [], [:foo]]
[#<UnboundMethod: B#foo() owner.rb:2>, B, [:foo], [:foo]]

[Bug #18741] Slicing an Array using index out of range inconsistent return type

  • 範囲外の値を Array#[] に渡したときの挙動が一貫していないというバグ報告
 def test_slicing_arrays
   array = [:peanut, :butter, :and, :jelly]

   assert_equal [:peanut], array[0,1]
   assert_equal [:peanut, :butter], array[0,2]
   assert_equal [:and, :jelly], array[2,2]
   assert_equal [:and, :jelly], array[2,20]
   assert_equal [], array[4,0] # 範囲外を指定した時にこっちは [] を返す
   assert_equal [], array[4,100]
   assert_equal nil, array[5,0] # 同様に範囲外を指定しているがこっちは nil を返す
 end

[Feature #18742] Introduce a way to tell if a method invokes the super keyword

  • メソッド内で super を呼び出しているかどうかを判定するメソッドを追加する提案
class X
  def a
  end; p instance_method(:a).calls_super? #=> false

  def b
    super
  end; p instance_method(:b).calls_super? #=> true

  def c
    super if false
  end; p instance_method(:c).calls_super? #=> true

  def d
    eval 'super'
  end; p instance_method(:d).calls_super? #=> false (I doubt there's a reasonable way for this to return true)
end

2022/04/14 今回の気になった bugs.ruby のチケット

今週は Kernel#then に引数を渡せるようにする提案がありました。

[Feature #18690] Allow Kernel#then to take arguments

  • Kernel#then に引数を追加する提案
  • 通常はレシーバをブロックの引数として受け取るが
1.5.then{|x| Math.atan(x)}
  • #then に渡した引数をブロックの引数でも受け取るようにする提案
3.then(4){|x, y| Math.hypot(x, y)}
  • チケットでは
honyarara.then{|x|
  foo(x)
  bar(fugafugafuga)
  baz(hogehogehoge)
  qux(x, fugafugafuga, hogehogehoge)
}
  • の場合に
honyarara.then(fugafugafuga, hogehogehoge){|x, y, z|
  foo(x)
  bar(y)
  baz(x)
  qux(x, y, z)
}
  • とかけるユースケースが提示されている
  • then の中で複数回同じメソッドを呼ぶ場合だと便利そうですかね?
obj.bar.then(hoge.foo) { |bar, foo| foo.piyo(foo, bar, foo) }

[Bug #18688] when array's default value is empty hash adding a hash key value changes all array elements

  • 以下のように空の Hash の配列の要素を書き換えると全て書き変わってしまうというバグ報告
# {} が3つある配列を生成する
ah = Array.new(3, {})
p ah    # => [{}, {}, {}]

# 1つの要素を書き換える
ah[1][:foo] = 'bar'

# この時に配列の要素は以下のようになる
p a
# 期待する挙動 => [{}, {:foo=>"bar"}, {}]
# 実際の挙動   => [{:foo=>"bar"}, {:foo=>"bar"}, {:foo=>"bar"}]
  • これは配列の要素の参照先が全て同じになっているので意図する挙動になる
# これは参照しているオブジェクトがバラバラ
a = [{}, {}, {}]
p a[0].__id__   # => 60
p a[1].__id__   # => 80
p a[2].__id__   # => 100

# Array.new の場合は全て同じオブジェクトを参照している
a = Array.new(3, {})
p a[0].__id__   # => 120
p a[1].__id__   # => 120
p a[2].__id__   # => 120


# n 個の同じ要素の配列を定義したい場合はブロックを渡して定義するのが安全
a = Array.new(3) { {} }
p a[0].__id__   # => 60
p a[1].__id__   # => 80
p a[2].__id__   # => 100

[Feature #18685] Enumerator.product: Cartesian product of enumerables

  • 要素ごとの全ての組み合わせの Enumerator を生成する Enumerator.product を追加する提案
product = Enumerator.product(1..3, ["A", "B"])
p product.class #=> Enumerator

product.each do |i, c|
  puts "#{i}-#{c}"
end

=begin output
1-A
1-B
2-A
2-B
3-A
3-B
=end
  • Array#product はあるんですがそれとは別に Enumerable でも使いたいって感じですかね
# Enumerable に対しては使えないので Array に変換する必要がある
pp (1..3).to_a.product(["A", "B"])
# => [[1, "A"], [1, "B"], [2, "A"], [2, "B"], [3, "A"], [3, "B"]]

[Feature #18618] no clobber def

  • 以下のようにスーパークラスのメソッドがある場合は再定義しないようなコードがある
class Dog
  def bark
    'bark!'
  end
end

class Poodle < Dog
  # 既に同名のメソッドが定義されていれば例外にする
  raise if method_defined? :bark
  def bark
    'bow-wow'
  end
end
  • これを次のように ncdef でかけるようにしたいという提案
class Dog
  def bark
    'bark!'
  end
end

class Poodle < Dog
  ncdef bark        # "no clobber" def
    'bow-wow'
  end
end

# => #<MethodAlreadyDefined: Method `bark' already defined.>
  • ApplicationRecord とかのサブクラスを定義したい時にこういうことをしたいらしい
  • 意図しないメソッドの上書きってどれぐらい弊害があるんですかね…
    • これがあると全部 ncdef で定義する必要がありそう
  • 他の言語だと逆に『スーパークラス側でメソッドが上書きできないこと』を明示化するような機能は存在する
  • C++ の例だと以下のような感じ
// 基底クラス
class base {
  // final が付いていると派生クラスで再定義できない
  virtual void func_final() final;
};

// 派生クラス
class sub : base {
  // error: error: declaration of 'func_final' overrides a 'final' function
  virtual void func_final() final;
};

2022/04/09 今回の気になった bugs.ruby のチケット

今週は MatchData#[] のバグ報告などがありました。

[Bug #18670] MatchData#[start, length] pads nil values when negative start is given

  • MatchData#[] にマイナス値を渡すと nil で埋めされた配列を返すバグ報告
# [-1, 5] は最後の値から5文字分を返す

# String#[] や Array#[] は結果的に最後の値だけを返す
pp "hello"[-1, 5]     # => "o"
pp [1, 2, 3][-1, 5]   # => "3"

# MatchData#[] の場合は足りない値が nil 埋めされた配列を返す
result = /.*/.match("aaaa")
pp result          # => #<MatchData "aaaa">
pp result[-1, 5]   # => ["aaaa", nil, nil, nil, nil]
  • このバグは修正済み
# MatchData#[] の場合は足りない値が nil 埋めされた配列を返す
result = /.*/.match("aaaa")
pp result[-1, 5]
# Ruby 3.1 => ["aaaa", nil, nil, nil, nil]
# Ruby 3.2 => ["aaaa"]

[Feature #18683] Allow to create hashes with a specific capacity.

  • Hash オブジェクトを生成する時にオブジェクトのサイズを指定できるようにする提案
    • 更に rb_hash_new_capa(long) も拡張する必要がある
hash = Hash.new(capacity: 1000)
  • StringArray には同様の機能があるので Hash にも欲しいという意図らしい
String.new(capacity: XXX)
Array.new(XX) / rb_ary_new_capa(long)

[Bug #18677] BigDecimal#power (**) returns FloatDomainError when passing an infinite parameter

  • BigDecimal#power (**) に無限大を渡すと意図しないエラーになるというバグ報告
require "bigdecimal"

# error: `**': Computation results in 'Infinity' (FloatDomainError)
BigDecimal(10) ** BigDecimal("Infinity")
  • サンプル実装も書かれているんですが意図としては BigDecimal::INFINITY を返してほしい感じですかね?
require "bigdecimal/util"

class BigDecimal < Numeric
  def **(other)
    if other.infinite? == 1
      if self > 1
        BigDecimal::INFINITY
      elsif self == 1
        self
      elsif self >= 0
        BigDecimal(0)
      else
        power(other)
      end
    else
      power(other)
    end
  end
end

def puts_and_eval(string)
  puts string
  p eval(string)
end

puts_and_eval "10 ** BigDecimal::INFINITY"    # => Infinity
puts_and_eval "1 ** BigDecimal::INFINITY"     # => 0.1e1
puts_and_eval "0.1 ** BigDecimal::INFINITY"   # => 0.0
puts_and_eval "0 ** BigDecimal::INFINITY"     # => 0.0
puts_and_eval "-1 ** BigDecimal::INFINITY"    # => -0.1e1

[Bug #18673] Anonymous block forwarding fails when combined with keyword arguments

  • 以下のようにキーワード引数が定義されている時に匿名のブロック引数を別のメソッドに渡すとエラーになるバグ報告
def inner
  "yielded #{yield}"
end

def block_only(&)
  # OK
  inner(&)
end

def pos_arg(arg1, &)
  # OK
  inner(&)
end

def kwarg(arg1, kwarg1:, &)
  # NG
  inner(&)
end

def kwarg_with_default(arg1, kwarg1: "kwarg_default", &)
  # NG
  inner(&)
end
  • このバグは修正済み

[Bug #18415] String#rpartition is not sufficiently greedy compared to String#partition

  • 以下のような貪欲な正規表現String#rpartition に渡すと意図する値が返ってこないバグ報告
# [最初のセパレータより前の部分, セパレータ, それ以降の部分] を返す
pp "...123...".partition /\d+/
# => ["...", "123", "..."]

# [最後のセパレータより前の部分, セパレータ, それ以降の部分] を返すが意図した値ではない
pp "...123...".rpartition /\d+/
# => ["...12", "3", "..."]
pp "...123...".rpartition(/(?<!\d)\d+/)
# => ["...", "123", "..."]