2022/05/12 今回の気になった bugs.ruby のチケット
[Bug #18768] Inconsistent behavior of IO, StringIO and String each_line methods when return paragraph and chomp: true passed
- 以下のように
String#each_line
StringIO#each_line
File#each_line
に特定の引数を渡したときの挙動に一貫性がないというバグ報告"", chomp: true
を渡した時- String#each_line (Ruby 3.1 リファレンスマニュアル)
- StringIO#each (Ruby 3.1 リファレンスマニュアル)
- るりまには
chomp
の引数の記述がない…
- るりまには
- IO#each (Ruby 3.1 リファレンスマニュアル)
"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"]
- これはどれが期待する挙動になるんですかね…
いまさら聞けない!波ダッシュと全角チルダ問題についてまとめてみた
元々のきっかけは以下のように Ruby で UTF-8
の波文字を SJIS
に変換しようとしたらエラーになってしまいました。
# UTF-8 の 〜 文字を SJIS に変換するとエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "〜".encode("SJIS")
これの原因を調べてみたら 波ダッシュと全角チルダ問題 にたどり着いたのでその問題と歴史についてまとめてみました。
諸注意
波ダッシュと全角チルダ問題 に関しては一般的な話なんですが、変換ルールやエンコーディングの指定の仕方などは CRuby での話になります。
他の言語や処理系だと結果が変わるかもしれないので注意してください。
また、この記事でのソースコードはすべて UTF-8
になります。
それでは本題に入っていきましょう。
Ruby で UTF-8
の波文字(〜
)を SJIS
に変換するとエラーになる
冒頭にも記述したように元々この問題を調べようとしたきっかけは次のように Ruby で UTF-8
の波文字(〜
)を SJIS
に変換しようとするとエラーになってしまうことでした。
# UTF-8 の 〜 文字を SJIS に変換するとエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "〜".encode("SJIS")
上記のコードで変換しようとした波文字(〜
)は Unicode
のコードポイントだと U+301C
になるんですが、その文字が SJIS
への変換に失敗 しています(ちなみに Ruby では SJIS
が Windows-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 ) |
変換できる | 変換できない |
次は SJIS
と Shift_JIS
の違いについて調べてみましょう。
Ruby における SJIS
と Shift_JIS
の違い
ここでは Ruby における SJIS
と Shift_JIS
の違いについて簡単に解説します。
まず Shift_JIS
はその名の通り文字コードの Shift_JIS
のことを指しています。
Shift_JIS
とは日本語を含む文字列を表現するために用いられる文字コードになります。
また Shift_JIS
は JIS X 0208
として標準化されています。
Ruby ではこの文字コードが Shift_JIS
として利用できます。
次に SJIS
なのですが Ruby のエンコーディングにおいて SJIS
は CP932
Windows-31J
と同じエンコーディングとして定義されています。
Windows-31J
とは Microsoft が Shift_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")
なので厳密に言うと SJIS
と Shift_JIS
は 異なるエンコーディング になります。
まとめると Ruby において SJIS
と Shift_JIS
は以下のような関係になっています。
SJIS
とCP932
Windows-31J
の 3つは同じエンコーディングとして扱われているWindows-31J
(SJIS
CP932
) は Microsoft がShift_JIS
を独自拡張したエンコーディングになるShift_JIS
とWindows-31J
は異なるエンコーディングになるShift_JIS
もWindows-31J
もJIS X 0208
という規格に準じている
より詳しい違いが知りたい人は以下を参照してください。
- Shift_JIS と Windows-31J (MS932) の違いを整理してみよう |
- Python♪Windowsの「Shift JIS」の落とし穴 | Snow Tree in June
- Encoding::SHIFT_JIS (Ruby 3.1 リファレンスマニュアル)
- Encoding::CP932 (Ruby 3.1 リファレンスマニュアル)
以降は SJIS
という表記だとややこしいので CP932
と表記し『 CP932
と Shift_JIS
』について解説していきます。
JIS X 0208
の波文字とは
ここでは Shift_JIS
や CP932
の規格である 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
の波ダッシュ、つまり『 CP932
や Shift_JIS
から UTF-8
に変換するとどうなるのか』を試してみましょう。
JIS X 0208
の波ダッシュを UTF-8
に変換するとどうなる?
今度は逆に JIS X 0208
の波ダッシュを UTF-8
に変換するとどうなるのか見てみましょう。
実際に Ruby で Shift_JIS
や CP932
から波ダッシュを UTF-8
に変換してみます。
Ruby で変換する場合は JIS X 0208
の波ダッシュ文字をバイト列で定義し String#encode
に変換元となるエンコーディングの情報として CP932
と Shift_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.0
と Unicode 8.0
の波ダッシュの例示字形の問題に関しては以下の記事で詳しく解説されているので気になる人は読んでみるとよいと思います。
- UnicodeのWAVE DASH例示字形が、25年ぶりに修正された理由 - INTERNET Watch Watch
- ~波ダッシュ~って字形が変わった?|IT情報メディアサイト idearu(アイディアル)
問題は逆だった
ここからは仮説になってくるのですが先程説明したように 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
の波文字は 波ダッシュ文字(U+301C
) と 全角チルダ文字(U+FF5E
) の2種類ある
文字 | 意味 | Unicode のコードポイント | UTF-8 のバイト列 |
---|---|---|---|
〜 |
波ダッシュ(WAVE DASH) | U+301C |
\xE3809C |
~ |
全角チルダ(FULLWIDTH TILDE) | U+FF5E |
\xEFBD9E |
- Ruby では
SJIS
とShift_JIS
は異なるエンコーディングとして定義されているSJIS
はCP932
とWindows-31J
と同等のエンコーディングとして定義されている
- Ruby では以下のようなルールで変換される
変換前の文字コード | 変換後の文字コード | 変換前の文字 | 変換後の文字 |
---|---|---|---|
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 ) |
参照
余談:Ruby で Unicode の波ダッシュを SJIS の波ダッシュに変換する
以下のように #encode
の fallback
オプションで制御する事が可能です。
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
Integer#digits
と対になるArray#undigits
を追加する提案Integer#digits
は位取り記数法で表記した数値を配列を返す- Integer#digits (Ruby 3.1 リファレンスマニュアル)
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
- これは右代入の場合に指定する行数を2行目ではなくて6行目を指定する必要があるそうです
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]]
B
でfoo
が再定義されているのにowner
がB(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 #16822] Array slicing: nils and edge cases と似たようなチケットでこちらは Reject されている
[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
- https://bugs.ruby-lang.org/issues/18618#note-1 の実装をする時に利用できると書いてある
- ユースケースがかなりエッジケースだけど他に利用できる箇所とかあるかなあ
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; };
- sorbet だと
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)
String
やArray
には同様の機能があるので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", "..."]
- これは意図する挙動らしくてドキュメントにその意図を追記する対応がされている
- ちなみに以下のような正規表現だと
123
がセパレータになる
pp "...123...".rpartition(/(?<!\d)\d+/) # => ["...", "123", "..."]