2021/02/11 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #17424] Interactive Ruby で Object#method を再定義して任意の文字を入力するとエラーが発生する

  • 表題の通りでなんですが irb 上で #method を再定義するとクラッシュするというバグ報告です
  • これは irb 上で #method を定義すると Kernel#method よりも優先順位が高い Object#method が定義されてしまい、Reline でその Object#method を使用しているためです
  • これは仕様でチケットは閉じられています
  • まあしょうがなさそう

[Feature #7394] Enumerable#find ifnone parameter could be non-callable

  • Enumerable#find は要素が見つからなかった場合の処理をフォールバックすることができる
# 見つからなかった場合に第一引数の proc を呼び出す
p [1, 3, 5].find(-> { "none" }, &:even?)
# => "noge"
  • 現状は Proc オブジェクトを渡すがこれをそれ以外のオブジェクトを渡せるようにする提案
# 第一引数の値がそのまま返ってくる
p [1, 3, 5].find("none", &:even?)
# => "noge"
p [1, 3, 5].find(&:even?) || "none"

[Feature #17608] Compact and sum in one step

  • 次のように Array#sum を行う場合に #compact を介して行うことがある
a = [1, nil, 2, 3]

a.sum # !> TypeError

a.compact.sum # => 6

a.sum{_1 || 0} # => 6
  • これをワンステップで行うために #sum では nil の要素をスキップする、または Array#filter_sum のようなメソッドを追加する提案です
  • 最終的には a.sum{_1 || 0} で問題ないということで Reject されています

Vim の oldfiles の最大数を変更する

Twitter でいろいろとやり取りしていたので覚書。

Vim の oldfiles の最大数を変更する

Vim oldfiles の保存数は 'viminfo'' の設定に依存しています。
この保存数を変更する場合は vimrc に以下のような設定を追加することで変更することができます。

" v:oldfiles で保存するファイル数を設定
" vimrc に書いておく必要がある
set viminfo+='10000
set viminfo-='100     " デフォルトの設定を削除しておかないt反映されない

また、oldfiles で保存するファイルパスは Vim を再起動しないと反映されないので注意する必要があります。
ちなみに denite-file/old はこの oldfiles のデータを参照しているのでこの設定をすることで denite-file/old の保存数を変更することもできます。

2021/02/04 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #10593] Emoji is been considered as comment

#️⃣ これはコメントアウトです
#️⃣ Ruby のコードは実行されません
puts "hello, world" #️⃣ コメント
  • これは #️⃣ が囲み文字と呼ばれている絵文字で # + 特殊なコードポイント で表現されているためです
  • 実際に字句解析するとこんな感じ
require "ripper"

# コメントとして字句解析される
p Ripper.lex("#️⃣")
# => [[[1, 0], :on_comment, "#️⃣", BEG]]

# こっちは普通の文字になる
p Ripper.lex("😀")
# => [[[1, 0], :on_ident, "😀", CMDARG]]
  • これは仕様ということで閉じられている

[Bug #7877] E::Lazy#with_index should be lazy

  • Enumerable::Lazy#with_indexRuby 2.6 と 2.7 で挙動が変わっていたので調べていたらこのチケットを見つけました
# Ruby 2.6 だとブロックの中身が呼ばれるが、2.7 だと呼ばれない
pp [1, 2, 3].lazy.with_index { |it, i| l}
# 2.6 => [1, 2, 3]
# 2.7 => #<Enumerator::Lazy: ...>
  • チケット自体は Enumerable::Lazy#with_indexlazy 化してほしいというバグチケット
    • 内容的には feature っぽいが
  • この影響で既存の Enumerable::Lazy#with_index の挙動がぶっ壊れてしまったぽい
  • 上記のコードを Ruby 2.7 でも同じようにどうさせる場合は #force#each するのが無難かなあ
pp [1, 2, 3].lazy.with_index { |it, i| p [it, i] }.force
pp [1, 2, 3].lazy.with_index.each { |it, i| p [it, i] }

denite.nvim で絞り込みにマッチしたワードをハイライトする

ちょっと前に denite.nvim を試してみたら思ったよりも使えるようになったのでいろいろと試してみた覚書。

denite.nvim で絞り込みにマッチしたワードをハイライトする

denite.nvim ではデフォルトでは絞り込んだワードでハイライトできなくて困ってたんですが以下のオプションの仕方を教えてもらいハイライトできるようになりました。

" Search を好きなハイライトにする
Denite line -highlight-matched-char=None -highlight-matched-range=Search -match-highlight

これでまた unite.vim からの移行に近づいた…。

2021/01/28 今週の気になった bugs.ruby のチケット

内容は適当です。 今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。 あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[PR 4113] Add --warning=category option the same as -W:category

  • -W:category と同じ意味を持つ --warning=category を追加する PR
  • チケット自体は特になさそう?
  • --warning=category のほうが意味的にはわかりやすそうなのであると便利そう

[Feature #17576] Partial Functions (procs, lambdas)

  • ブロック構文に in を使ったガード節を追加する提案
  • 以下のように in を使ってガード節を書く
    • in x if x.odd? の部分がガード節の条件
partial_proc = proc do |arg|
in x if x.odd?
  "#{x} is odd"
end
  • このような定義をして以下のような操作を行う
# `defined?` で引数が有効かどうかを判定する
partial_proc.defined?(42) #=> false
partial_proc.defined?(41) #=> true

# call で実際に呼び出す
# 該当するガード節がない場合は例外が発生する
partial_proc.call(42) #=> raises NoMatchingPatternError (42)
partial_proc.call(41) #=> 41 is odd

# call_or_else で見つからなかった場合の挙動をブロックに記述する
partial_proc.call_or_else(42) { "fallback value" } #=> 'fallback value'
partial_proc.call_or_else(41) { "fallback value" } #=> 41 is odd
  • これを用いて FizzBuzz を書くと多分以下のような感じ?
p (1..20).map do |n|
in Integer if n % 15 == 0
  "fizzbuzz"
in Integer if n % 3 == 0
  "fizz"
in Integer if n % 5 == 0
  "buzz"
in Integer
  n
end
  • 最初のコードを見たときは『どういうこっちゃ?』と思ったけどガード節と見れば納得
  • カード節は普通に便利なのでほしい
  • ただ defined?call_or_else は微妙なのでこれは別に議論すべきじゃないかなーと思わなくもない

[Bug #17557] IRB array returned in multiple lines

  • irb 1.3.0 で irb のデフォルトの出力が inspect から pretty_inspect に変わったけど元の挙動に戻せる?みたいなチケット
# 以前のバージョン
's'.methods
= > [:unicode_normalize, :unicode_normalize!, :ascii_only?, :to_r, :unpack, ..., :!=, :equal?, :__id__, :instance_eval, :instance_exec]
[2021-01-28 00:26]
# irb 1.3.0
's'.methods
= >
[2021-01-28 00:26]
[:unicode_normalize,
 :unicode_normalize!,
 :ascii_only?,
 :to_r,
 :unpack,
 ...,
 :!=,
 :equal?,
 :__id__,
 :instance_eval,
 :instance_exec]
  • 元の inspect を呼び出す挙動に戻す場合は ~/.irbrc とかに以下の設定を書いておけば OK
IRB.conf[:INSPECT_MODE] = :inspect

Ruby で UTF-8 の文字列を SJIS で文字化けさせたり復元したりする

以下の記事を知人と話していたら思ったよりも盛り上がったので覚書。

ちなみにクイズのネタバレがあるので見たくない人は読まないでね!!

縺ゅ¢縺セ縺励※縺翫a縺ァ縺ィ縺 みたいな文字化けした文字列を Ruby で生成する

言及されている文字化けは『 UTF-8 な文字を SJIS で表示すると文字化ける』っていうような話になります。
では、Ruby でこのような文字化けを行う場合どうするのがいいのでしょうか。
これは要するに『 UTF-8SJIS として扱い UTF-8 で変換する』という処理で実現することができます。
まず、Ruby のデフォルトのエンコーディングutf-8 なので単に文字列リテラルを定義した場合は utf-8 の文字列になります。

# 文字列リテラルは utf-8
utf8 = "やばたにえん"
pp utf8.encoding
# => #<Encoding:UTF-8>

次に String#force_encoding を使って『内部のデータはそのままでエンコーディング情報のみ』を変更します。
NOTE: ちなみに #force_encoding#encode と違い #valid_encoding でチェックは行いません(thanks id:imaizumimr :)

utf8 = "やばたにえん"

# 内部のエンコーディング情報のみ書き換える
# 内部データはそのまま
sjis = utf8.force_encoding(Encoding::SJIS)

# エンコーディング情報のみ SJIS として扱われる
# ちなみに SJIS は Windows-31J のエイリアス
pp sjis.encoding
# => #<Encoding:Windows-31J>

# バイトコードは同じ
pp utf8.bytes == sjis.bytes
# => true

最後に『内部データは UTF-8 だけど文字コード情報 SJIS 』な文字列に対して UTF-8エンコードします。
この時に変換できない文字があるので無理やり変換されるように invalid: :replace, undef: :replace オプションを指定しています。

utf8 = "やばたにえん"

sjis = utf8.force_encoding(Encoding::SJIS)

# SJIS だけど実データは UTF-8 な文字列を無理やり UTF-8 で変換させる
pp sjis.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
# => "繧��縺溘↓縺医s"

これでいわゆる『文字化け』した文字列が生成できました。

文字化けしたコードを戻す

では、次は文字化けしたコードを復元してみましょう。
現状はこんな感じです。

utf8 = "やばたにえん"

sjis = utf8.force_encoding(Encoding::SJIS)
bake = sjis.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)

pp bake
# => "繧��縺溘↓縺医s"
pp bake.encoding
# => #<Encoding:UTF-8>

これを元に戻すのは比較的簡単で(全然簡単ではなかったけど…)先程の処理と逆のことをします。

utf8 = "やばたにえん"

sjis = utf8.force_encoding(Encoding::SJIS)
bake = sjis.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)

# まず SJIS でエンコードする。その後に文字コードを UTF-8 に指定する
pp bake.encode(Encoding::SJIS, invalid: :replace, undef: :replace).force_encoding(Encoding::UTF_8)
# => "\xE3\x82??たにえん"

これで復元することができます。
できるんですが文字化けさせるときのエンコードを無理やり行っているので上の手順で復元した場合は完璧に復元することはできません、うぐぅ…。

まとめ

文字コードなんもわからんが RubyString文字コード周りの実装がしっかりしててすごいなぁ。
普段書かないような処理なのでいろいろと知らない機能やオプションなんかの知見がしれてよかったです。
あと人とわいわいしながらコード書くのたのしいですね!!!
久々に書いてて楽しい Ruby のコードだった。

2021/01/21 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #17530] irb handles << incorrectly with variable as identifier

  • irb で以下のように入力したら意図しないエラーになったというバグ報告
irb(main):001:0' s1 = 'testing'
= > "testing"
[2021-01-20 23:11]
irb(main):002:0' s2 = 'this'
= > "this"
irb(main):003:0" s2 <<s1
irb(main):004:0" adding text here does not work
irb(main):005:0" s1
Traceback (most recent call last):
3: from /home/centos/.rubies/ruby-3.0.0/bin/irb:23:in `<main>'
2: from /home/centos/.rubies/ruby-3.0.0/bin/irb:23:in `load'
1: from /home/centos/.rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):4: syntax error, unexpected local variable or method, expecting '(')
adding text here does not work
^~~~
irb(main):006:0>
  • 意図としては 3行目の s2 <<s1s2.<<(s1) のように String#<< を呼んでほしい
  • しかし、 irb 上ではヒアドキュメントと解釈されてしまっており 4〜5行目はヒアドキュメントの複数行として扱われてしまう
  • なんですが 5行目で Ruby のコードが実行される場合はヒアドキュメント s2.<<(s1) と解釈されてしまいおかしいことになっています
    • 4行目がそのまま Ruby のコードとして実行されている
  • この問題の本質としては s2 <<s1 というコードは『 s2 という変数が定義されているかどうか』で意味が変わってしまう点です
    • 変数が定義されていれば s2.<<(s1) と解釈され、そうでなければヒアドキュメントとして扱われる
  • なので 3行目のコードを解析する際に変数の有無も考慮する必要があるのですが irb の実装としては1行ずつコードを解析しておりその前の行で s2 変数が定義されていても s2 <<s1 がディアドキュメントとして解釈されてしまっている、って感じです
  • これ、直してみたいんだけどどう治すのがいいのかなあ…

[Bug #17547] Fix Ripper.lex("a <<b")

  • Ripper.lex("a <<b") が意図する結果を返してなかったというバグ報告
    • 必要なパース結果が含まれていなかった
  • これですが先程の [Bug #17530] を調べている時に見つけたバグで直せそうだったので直してパッチ投げました
    • Ripper なので実装が難しいのかなあ、と思っていたんですが問題になっていた箇所は Ruby で書かれていたので比較的簡単でした
require "ripper"

p Ripper.lex("a <<b")
# 期待する挙動 => [[[1, 0], :on_ident, "a", CMDARG], [[1, 1], :on_sp, " ", CMDARG], [[1, 2], :on_heredoc_beg, "<<b", CMDARG]]
# 実際の挙動   => [[[1, 2], :on_heredoc_beg, "<<b", CMDARG]]
  • この修正は既にマージ済みです

[Bug #17556] ruby 2.7.2 ::YAML.dump ArgumentError: invalid value for Integer(): "20210101_"

  • YAML.dump '20210101_' すると Integer と解釈されてエラーになるというバグ報告
require "yaml"

p YAML::VERSION
# => "3.1.0"

# error: `Integer': invalid value for Integer(): "20210101_" (ArgumentError)
p YAML.dump '20210101_'
require "yaml"

p YAML::VERSION
# => "3.2.0"

p YAML.dump '20210101_'
# => "--- '20210101_'\n"

[Bug #17554] [PATCH] Fix ObjectSpace.dump to include singleton class name

  • 次のように ObjectSpace.dump に特異クラスを渡した場合に Ruby 3.0 だと "name" が含まれなくなっているというバグ報告
require "objspace"

puts ObjectSpace.dump(Object.new.singleton_class)
# 2.7 => {"address":"0x55adae76b630", "type":"CLASS", "class":"0x55adae7a76d0", "name":"Object", "references":["0x55adae7a9250", "0x55adae76b720"], "memsize":464, "flags":{"wb_protected":true}}
# 3.0 => {"address":"0x55d9048e80e0", "type":"CLASS", "class":"0x55d90476d738", "references":["0x55d90476e8b8", "0x55d9048e8158"], "memsize":472, "flags":{"wb_protected":true}}
  • なるほどね?

[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 しているのに反映されてないように見える
p B.ancestors
# 2.7 => [M, B, A, Object, Kernel, BasicObject]
# 3.0 => [B, M, A, Object, Kernel, BasicObject]
  • これの対処として [M, B, M, A, Object, Kernel, BasicObject] を返す提案がされました
  • この場合は M が重複してしまいますが、次のようなコードでも重複するので問題ないとの判断
module M; end
class A; end
class B<A; end

A.prepend M
B.prepend M

# これは Ruby 3.0 以前からこのような挙動になっている
p B.ancestors # => [M, B, M, A, Object, Kernel, BasicObject]
  • この変更は既にマージされました
  • 今後は以下のような挙動になる予定です
module M; end
module A; end
class B; include A; end

A.prepend M
B.prepend M

p B.ancestors
# 2.7 => [M, B, A, Object, Kernel, BasicObject]
# 3.0 => [B, M, A, Object, Kernel, BasicObject]
# 3.1 => [M, B, M, A, Object, Kernel, BasicObject]
  • このあたりはちょっと注意して使う必要がありそうですねえ