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]
  • このあたりはちょっと注意して使う必要がありそうですねえ

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

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

[Feature #17485] Keyword argument for timezone in Time.new

  • Time.newタイムゾーンを指定する場合、以下のように時間をすべて指定する必要がある
# OK
Time.new(2021, 1, 1, 0, 0, 0, "+09:00") #=> ok: 2021-01-01 00:00:00 +0900

# これは意図するタイムゾーンを設定できない
Time.new(2021, 1, 1, "+09:00")          #=> bad: 2021-01-01 09:00:00 +0900
Time.new(2021, 1, "+09:00")             #=> bad: 2021-01-09 00:00:00 +0900
Time.new(2021, "+09:00")                #=> ArgumentError (mon out of range)
  • キーワード引数 inタイムゾーンを設定できるようにするチケット
Time.new(2021, 1, 1, in: "+09:00") #=> ok: 2021-01-01 00:00:00 +0900
Time.new(2021, in: "+09:00")       #=> ok: 2021-01-01 00:00:00 +0900

[Feature #16806] Struct#initialize accepts keyword arguments too by default

  • Structkeyword_init: をデフォルトで有効化させるチケット
User = Struct.new(:name, :age)

# これは以前の挙動のまま
homu = User.new("homu", 14)

# キーワード引数を渡すと keyword_init: true と同じように初期化される
homu = User.new(name: "homu", age: 14)
  • ただし、以下のようなケースで互換性が壊れるかもしれない
User = Struct.new(:name, :age)

# 現状だと name に Hash オブジェクトが入ってしまうので既存の挙動と変わってしまう
p User.new(name: "homu", age: 14)
# => #<struct User name={:name=>"homu", :age=>14}, age=nil>

[Bug #17519] set_visibility fails when a prepended module and a refinement both exist

  • 以下のように refine 後のメソッドを特異クラスを経由して private 化しようとするとエラーになるというバグ報告
module Nothing; end

class X
  # prepend しなかったらエラーにはならない
  prepend Nothing

  def hoge
  end
end

# これは OK
X.new.singleton_class.class_eval { private :hoge }

module NeverUsed
  refine X do
    def hoge(*keys)
    end
  end
end

# `private': undefined method `hoge' for class `#<Class:#<X:0x0000558fa95b7d70>>' (NameError)
# Refinements で拡張したあとに呼ぶとエラーになる
X.new.singleton_class.class_eval { private :hoge }
  • なにもわからない…
  • これは Ruby 2.5, 2.6, 2.7 でも再現していた

[Bug #17533] Named capture is not assigned to the same variable as reserved words.

  • 次のように正規表現で任意の変数に結果をキャプチャすることができる
# マッチした部分を result 変数に保存する
/(?<result>\d+).*/ =~ "1234hoge"
pp result
# => "1234"
  • この時に予約語をキーワード引数として定義している時に正しく代入されていなかった
def test(nil: :ng)
  # nil という変数名にマッチしたテキストが代入されるのを期待する
  # しかし、代入されない
  /(?<nil>\d+).*/ =~ "1234hoge"
  binding.local_variable_get(:nil)  # => :ng
end

test
def test(nil: :ng)
  /(?<nil>\d+).*/ =~ "1234hoge"
  binding.local_variable_get(:nil)
  # 3.0 => :ng
  # 3.1 => "1234"
end

test

[Bug #17534] Pattern-matching is broken with find pattern

  • 次のような find パターンを含んだパターンマッチでぶっ壊れるという報告
case [1, 2, 3]
in y
  puts "branch1"
in [*, x, *]
  puts "branch2"
else
  puts "branch3"
end
# output:
__END__
-- raw disasm--------
   trace: 1
   0000 putnil                                                           (   2)
   0001 duparray             <hidden>                                    (   1)
   0003 dup                                                              (   2)
   0004 setlocal_WC_0        4                                           (   2)
   0006 jump                 <L002>                                      (   2)
   0008 dup                                                              (   4)
   0009 topn                 2                                           (   4)
   0011 opt_le               <calldata:<=, 1>                            (   4)
   0013 branchunless         <L013>                                      (   4)
   0015 topn                 3                                           (   4)
   0017 topn                 1                                           (   4)
   0019 opt_aref             <calldata:[], 1>                            (   4)
   0021 setlocal_WC_0        3                                           (   4)
   0023 jump                 <L012>                                      (   4)
 <L013> [sp: 2]
   0025 pop                                                              (   4)
   0026 pop                                                              (   4)
*  0027 pop                                                              (   4)
   0028 jump                 <L006>                                      (   4)
 <L012> [sp: 2]
   0030 pop                                                              (   4)
   0031 pop                                                              (   4)
   0032 pop                                                              (   4)
   0033 pop                                                              (   4)
   0034 jump                 <L004>                                      (   4)
 <L006> [sp: -1]
   0036 pop                                                              (   4)
 <L001> [sp: -1]
   0037 pop                                                              (   7)
   0038 pop                                                              (   7)
   trace: 1
   0039 putself                                                          (   7)
   0040 putstring            "branch3"                                   (   7)
   0042 opt_send_without_block <calldata:puts, 1>                        (   7)
   0044 leave                                                            (   7)
 <L002> [sp: 2]
   0045 pop                                                              (   2)
   0046 pop                                                              (   2)
   trace: 1
   0047 putself                                                          (   3)
   0048 putstring            "branch1"                                   (   3)
   0050 opt_send_without_block <calldata:puts, 1>                        (   3)
   0052 leave                                                            (   7)
 <L004> [sp: -1]
   0053 pop                                                              (   4)
   0054 pop                                                              (   4)
   trace: 1
   0055 putself                                                          (   5)
   0056 putstring            "branch2"                                   (   5)
   0058 opt_send_without_block <calldata:puts, 1>                        (   5)
   0060 leave                                                            (   7)
---------------------
/tmp/vxdawqc/180:4: warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
/tmp/vxdawqc/180:4: argument stack underflow (-1)
/tmp/vxdawqc/180: compile error (SyntaxError)
  • こんなエラーはじめてみた…
  • 結構簡単に再現しそうなので怖い

[Feature #13683] Add strict Enumerable#single

  • レシーバの要素を1つだけ返す Enumerable#single を追加する提案
  • 似たようなメソッドに Enumerable#first があるがちょっと意味が違う
    • Enumerable#first : 先頭の要素を返す。見つからなかった場合は nil を返す
    • Enumerable#single : 先頭の要素を返す。ただし、要素が2個以上、または存在しない場合は例外を発生させる
  • あんまり利便性がわからないけど便利なのかな…?
  • ちなみに ActiveSupport にも同様の PR が投げられてます
  • 現状は #single という名前ではなくて違う名前の方がいいんじゃないか、みたいな名前付けの議論で止まってるぽい
  • と、思ってたら ActiveRecord で同じような機能の ActiveRecord::FinderMethods#sole というメソッドが追加されました
  • 更に ActiveSupportEnumerable#sole を追加する PR もある
  • ActiveSupportEnumerable#sole が追加されたあとに Ruby の標準に別名で同等の機能が入った場合、混乱しそうだなあ
  • ちなみにパターンマッチで取得するのはどうか、というコメントもされている
case []; in [a]; p a; end #=> NoMatchingPatternError ([])
case [1]; in [a]; p a; end #=> 1
case [1,2]; in [a]; p a; end #=> NoMatchingPatternError ([1, 2])

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

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

[Feature #17471] send_if method for improved conditional chaining

  • 以下のような #send_if を追加する提案
class Object
  def send_if(method, *args, proc: nil)
     yield(self) ? self.send(method, *args, &proc) : self
  end
end
  • これは以下のように『条件にマッチしたときのみメソッドチェーンを行う』ような場合に利用できる
puts 'Do you want a loud Merry Christmas? (y or I take it as a no)'
answer = gets.chomp

# 標準入力を受け取り、入力によって呼び出す処理を切り替える
# 'y' が入力されたら大文字に変換して結合する
puts %w(Merry Christmas).send(:map, &->(e) { answer == 'y' ? e.upcase : e }).join(' ')

# 提案されている send_if ならこんな感じで記述できる
puts %w(Merry Christmas).send_if(:map, proc: :upcase) { answer == 'y' }.join(' ')
  • わたしもこういうのがほしいと思っていていろいろと考えているんですが今回の #send_if はかなりやってることが多くて個人的にはちょっと微妙
    • とはいえこうなってしまう理由もわかるのでうーーーーんって感じ
    • 条件条件にマッチした場合の処理 の2つのブロックをメソッドに渡したいんですが Ruby でこのあたりどう表現すべきなのかが難しい
  • ちなみに個人的には以下のような tap + break でいいじゃん、ってなっています
# Proposal
puts %w(Merry Christmas).send_if(:map, proc: :upcase ) { answer == 'y' }.join(' ')

# tap + break
puts %w(Merry Christmas).tap { break _1.map(&:upcase) if answer == 'y' }.join(' ')
  • tap + break に慣れてないとぎょっとするんですが、これはこれで Ruby としてみるとかなり素直なコードになっているので個人的には気に入っています
  • また似たようなチケットとしては以下のようなチケットもあるので気になる人はこちらも見てみると良いです

[Bug #17512] ostruct super regression

  • 以下のように super を経由して OpenStruct を参照した場合に Ruby 3.0 だと意図しない値が返ってきているというバグ報告
require 'ostruct'

class Foo < OpenStruct
  def foo
    super
  end
end

p Foo.new(foo: 123).foo
# Ruby 2.7 => 123
# Ruby 3.0 => nil
  • この問題は OpenStruct 0.3.2 で修正されている
  • 困っている人は個別に OpenStruct をインストールすると改善すると思います

[Misc #16436] hash missing #last method, make it not so consistent (it has #first)

  • Hash#first はあるけど Hash#last はないので一貫性がないよねーっていうチケット
homu = { id: 1, name: "homu", age: 14 }

pp homu.first
# => [:id, 1]

# error: undefined method `last' for {:id=>1, :name=>"homu", :age=>14}:Hash (NoMethodError)
pp homu.last
  • これは Hash#firstEnumerable#first で実装されており Enumerable#last がないからです
    • Enumerable#first があって Enumerable#last がないのは知らなかった
    • まあ確かにイテレーションを考えると #first はあっても #last がないのはなんとなくわかるよな…?
  • ちなみに以下のように #reverse_each を使うと終端の要素を取得することはできます
homu = { id: 1, name: "homu", age: 14 }

pp homu.reverse_each.first
# => [:age, 14]
  • このチケット自体には強い気持ちでの要望とかはないので、もし必要な人とかがいればコメントしてみるといいと思います

[Bug #17488] Regression in Ruby 3: Hash#key? is non-deterministic when argument uses DelegateClass

  • 次のように Ruby 3.0 で Hask#key?DelegateClass を渡すと意図しない結果が返ってくるというバグ報告
    • Ruby 3.0 では Hask#key? の結果が起動毎に変わることがある
puts "Running on Ruby: #{RUBY_DESCRIPTION}"

program = <<~EOS
  require "delegate"
  TypeName = DelegateClass(String)

  hash = {
    "Int" => true,
    "Float" => true,
    "String" => true,
    "Boolean" => true,
    "WidgetFilter" => true,
    "WidgetAggregation" => true,
    "WidgetEdge" => true,
    "WidgetSortOrder" => true,
    "WidgetGrouping" => true,
  }

  puts hash.key?(TypeName.new("WidgetAggregation"))
EOS

iterations = 20
results = iterations.times.map { `ruby -e '#{program}'`.chomp }.tally

# Ruby 3.0 で実行すると false が返ってくることがある
puts "Results of checking `Hash#key?` #{iterations} times: #{results.inspect}"
# Ruby 2.7 => Results of checking `Hash#key?` 20 times: {"true"=>20}
# Ruby 3.0 => Results of checking `Hash#key?` 20 times: {"false"=>12, "true"=>8}

[Bug #17481] Keyword arguments change value after calling super without arguments in Ruby 3.0

  • 次のように super を呼び出す前と後でキーワード引数の値が変わってしまうというバグ報告
class BaseTest
  def call(a:, b:, **)
  end
end

class Test < BaseTest
  def call(a:, b:, **options)
    p options  # =>  {:c=>{}}
    super
    # super を呼び出した後で options の値が変わってしまっている…
    p options  # =>  {:c=>{}, :a=>1, :b=>2}
  end
end

Test.new.call(a: 1, b: 2, c: {})

[Feature #17472] HashWithIndifferentAccess like Hash extension

  • ActiveSupport::HashWithIndifferentAccess をサポートする機能を Ruby で実装する?というチケット
  • ちょっとチケットの意図が読み取れてないので間違ってるかもしれませんが『Ruby 本体で ActiveSupport::HashWithIndifferentAccess を実装する(提供する)』というわけではなくて『 `ActiveSupport::HashWithIndifferentAccess を高速化できる仕組みを Ruby 側で用意する』っていうのが趣旨なのかな?
  • 具体的に言うと Ruby 3.0 で追加された Symbol#name みたいなのを Ruby 本体で ActiveSupport::HashWithIndifferentAccess を高速化できるような機能を提供する感じなんですかね
    • コメント読んでる限りこのあたりちょっと認識のズレがありそう
  • 最初読んだ時に『 ActiveSupport::HashWithIndifferentAccessRuby で提供するのはやめてくれ〜〜〜』と思っていたんですが機能提供って意味だとかなりよさそうですね

[RP] Remove deprecated URI.escape/URI.unescape

M1 の MacBook で rbenv + Ruby 2.6.6 をビルドする

rbenv で Ruby 2.7 や 3.0 のビルドは問題なかったんですが 2.6 で失敗したのでその対処方法を書いておきます。

rbenv で 2.6.6 をインストールする

rbenv で 2.6.6 をインストールしようとしたらエラーになります。

$ rbenv install 2.6.6
Downloading openssl-1.1.1i.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/e8be6a35fe41d10603c3cc635e93289ed00bf34b79671a3a4de64fcee00d5242
Installing openssl-1.1.1i...
Installed openssl-1.1.1i to /Users/anzu/.rbenv/versions/2.6.6

Downloading ruby-2.6.6.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.6.tar.bz2
Installing ruby-2.6.6...
ruby-build: using readline from homebrew

BUILD FAILED (macOS 11.1 using ruby-build 20201225-2-g5de6d5f)

Inspect or clean up the working tree at /var/folders/j_/4snw29l52s5g_hwy5fxbvqsw0000gp/T/ruby-build.20210102215022.60171.Wp1e2j
Results logged to /var/folders/j_/4snw29l52s5g_hwy5fxbvqsw0000gp/T/ruby-build.20210102215022.60171.log

Last 10 log lines:
compiling zlib.c
compiling normalize.c
compiling modify.c
compiling set_len.c
compiling enc_str_buf_cat.c
compiling new.c
linking shared-object -test-/string.bundle
linking shared-object date_core.bundle
linking shared-object zlib.bundle
make: *** [build-ext] Error 2

実際のコンパイルエラーは以下な感じです。

compiling nofree.c
linking shared-object -test-/wait_for_single_fd.bundle
linking shared-object -test-/vm/at_exit.bundle
compiling closure.c
compiling zlib.c
compiling ellipsize.c
installing default libraries
compiling conversions.c
closure.c:264:14: error: implicit declaration of function 'ffi_prep_closure' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
    result = ffi_prep_closure(pcl, cif, callback, (void *)self);
             ^
compiling normalize.c
1 error generated.
make[2]: *** [closure.o] Error 1
make[2]: *** Waiting for unfinished jobs....
linking shared-object date_core.bundle
compiling psych_to_ruby.c
compiling escape.c
compiling stringio.c
compiling modify.c
make[1]: *** [ext/fiddle/all] Error 2
make[1]: *** Waiting for unfinished jobs....

ffi 周りでコケてるっぽい…。

RUBY_CFLAGS=-DUSE_FFI_CLOSURE_ALLOC rbenv install 2.6.6 でインストールする

困ってたら以下の issues を教えてもらってそこに書いてあった RUBY_CFLAGS=-DUSE_FFI_CLOSURE_ALLOC rbenv install 2.6.6 で無事にインストールすることができました。

$ RUBY_CFLAGS=-DUSE_FFI_CLOSURE_ALLOC rbenv install 2.6.6
rbenv: /Users/anzu/.rbenv/versions/2.6.6 already exists
continue with installation? (y/N) y
Downloading openssl-1.1.1i.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/e8be6a35fe41d10603c3cc635e93289ed00bf34b79671a3a4de64fcee00d5242
Installing openssl-1.1.1i...
Installed openssl-1.1.1i to /Users/anzu/.rbenv/versions/2.6.6

Downloading ruby-2.6.6.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.6.tar.bz2
Installing ruby-2.6.6...
ruby-build: using readline from homebrew
Installed ruby-2.6.6 to /Users/anzu/.rbenv/versions/2.6.6

ありがてえありがてえ…。

【一人 cugs.ruby Advent Calendar 2020】[Feature #17016] Enumerable#scan_left【25日目】

一人 bugs.ruby Advent Calendar 2020 25日目の記事になります。
長かったアドベントカレンダーも今日で最後です。

[Feature #17016] Enumerable#scan_left

このチケットは Enumerable#scan_left を追加する提案です。
Enumerable#scan_leftEnumerable#inject と似たようなメソッドなのですが各ブロックの結果を配列で返します。

# inject は最後の結果だけ返す
[1, 2, 3].inject(0, &:+)
# => 6

# scan_left は各ブロックの戻り値を配列として返す
[1, 2, 3].scan_left(0, &:+)
# => [0, 1, 3, 6]

これは HaskellScala に存在する関数らしく Ruby でも欲しいそうです。
PR はちょっと前からあり、gem もあります。

ユースケースとしては累積和を求めるときに便利らしいです。
他にはコメントで以下のようなユースケーズも提示されています。

# 銀行の入出金履歴
gains = [+3000, -2000, +2000, -1000]

# 残高の履歴を計算
sums = [0]
(1..gains.length).each do |i|
  sums[i] = sums[i - 1] + gains[i - 1]
end
pp sums
# => [0, 3000, 1000, 3000, 2000]

# scan_left を使うとシュッとできる
sums = gains.scan_left(0, &:+)
pp sums
# => [0, 3000, 1000, 3000, 2000]

あとは以下のようなケースとか…。

module Enumerable
  # 疑似実装
  def scan_left(init = shift, &block)
    inject([init]) { |a, e| a << (block.call a.last, e) }
  end
end

# 4312.to_s.chars.sort.join.to_i の呼び出し過程を計算したりとか…
p [4312, :to_s, :chars, :sort, :join, :to_i].scan_left(&:send)
# => [4312, "4312", ["4", "3", "1", "2"], ["1", "2", "3", "4"], "1234", 1234]

ちなみに Enumerable#scan_left は以下のように #inject を使っても同じ値を取得する事はできます。

pp [1, 2, 3].inject([0]){ |a, e| a << a.last + e }
# => [0, 1, 3, 6]

ただし、この場合は普通には #lazy 化はできないので注意する必要があります。

# こういうような書き方はできない
(1..).lazy.inject([0]){|a, e| a << a.last + e} # => infinite loop
(1..).lazy.each_with_object([0]){|e, a| a << a.last + e} # => infinite loop
(1..).lazy.scan_left(0, &:+) # => Lazy enumerator

# がんばればできる
p (1..).lazy.enum_for(:inject, 0).map {|a, b| a + b }.take(10).force
# => [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

# もしくは
# p (1..).lazy.enum_for(:inject, 0).map {|a, b| a + b }.first(10)

#scan_left という名前はあんまりよろしくないと言うことで別の名前の提案がされいて今はそこで議論が止まっている感じです。
候補としては reflectproject interject tranject cumulative などなど…。
このあたりの名前決めは難しそうですねえ。

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