今週の気になった bugs.ruby

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

Feature #16378 Support leading arguments together with ...

def method_missing(name, ...)
  if name.to_s.end_with?('?')
    self[name]
  else
    fallback(name, ...)
  end
end

Bug #16948 hash.each(&method(:something)) behavior changed without warning on master

  • ruby 2.8.0dev で hash.each(&method(:something) が動かなくなったというバグ報告
    • 2.7.1 では問題ない
def foo(a, b)
  p [a, b]
end

# error: in `foo': wrong number of arguments (given 1, expected 2) (ArgumentError)
{1 => 2}.each(&method(:foo))
def foo(a, b)
  p [a, b]
end

def bar(a, b = 2)
  p [a, b]
end

foo_lambda = method(:foo).to_proc
bar_lambda = method(:bar).to_proc

{a: 1}.each(&foo_lambda)   # => [:a, 1]
{a: 1}.each(&bar_lambda)   # => [[:a, 1], 2]

Bug #16947 private method unexpected behavior

Feature #16946 Add an intersperse method

  • 要素間に任意の値を挿入するメソッドの提案
[1, 2, 3].intersperse(0)
#=> [1, 0, 2, 0, 3]

'Hello'.intersperse('-')
#=> "H-e-l-l-o"
  • どういうケースで利用できるんだろうか
  • 既存のメソッドだと以下のような感じらしい
[1, 2, 3].flat_map { |i| [i, 0] }[0...-1]
#=> [1, 0, 2, 0, 3]

'Hello'.gsub(/(?<=.)./, '-\0')
#=> "H-e-l-l-o"

'Hello'.chars.join('-')
#=> "H-e-l-l-o"

Feature #16939 Alias _1 as _ for block numbered params

  • __1エイリアスにする提案
  • _ のほうが短いけど意味がわかりづらいかも…?
  • 確か _ は『参照しない引数』みたいな意味で使われている事が多いみたいな話があった気がする
# 第二引数は参照しないので _ を使う
hash.map { |k, _| k.to_s }
  • _ は既存のコードで多く利用されている』という理由でシュッと閉じられた

Bug #16942 instance_method causes an infinite loop with prepend, include and private

  • 特定の mixin を行った状態で Object.instance_method を呼ぶと無限ループするバグ
module M
  def x
  end
end

module M2
  include M
  private :x
end

::Object.prepend(M2)

# infinite loop
p Object.instance_method(:x)
  • 上の例だと Ruby 2.7.1 では無限ループしないが 2.8.0dev だと無限ループする
  • 下の例だと Ruby 2.7.1 でも無限ループする
module M
  def x
  end
end

module M2
  include M
  private :x
  prepend Module.new
end

::Object.prepend(M2)

# infinite loop
p Object.instance_method(:x)

Ruby の or と and の優先順位

Rubyorand は優先順位がとても低いです。 例えば

x = 1 or 2

(x = 1) or 2

と解釈され

foo || bar and hoge

(foo || bar) and hoge

と解釈されます。 ちなみに次のようなコードはシンタックスエラーになります。

# syntax error, unexpected `or', expecting ')'
f(a or b)

foo || bar && hogefoo or bar and hoge の違い

さて、 ||&& 演算子では && の方が優先されます。 なので

foo || bar && hoge
foo && bar || hoge

foo || (bar && hoge)
(foo && bar) || hoge

と解釈されます。
一方で orand 演算子では『先に書いてある方』が優先されます。
なので

foo or bar and hoge
foo and bar or hoge

(foo or bar) and hoge
(foo and bar) or hoge

となります。
or and 演算子はあんまり使ったことがないんですがこのあたりの違いは知らなかった。

今週の気になった bugs.ruby のチケット

なんとなく書き溜めるようにしてみました。
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。

Feature #16828 Introduce find patterns

  • パターンマッチで条件にマッチした一部の要素だけ取得できる提案
case ["a", 1, "b", "c", 2, "d", "e", "f", 3]
in [*pre, String => x, String => y, *post]
  p pre  #=> ["a", 1]
  p x    #=> "b"
  p y    #=> "c"
  p post #=> [2, "d", "e", "f", 3]
end

Bug #16852 Refining Enumerable fails with ruby 2.7

  • 2.7 系の Refinements のバグ
  • refine M したあとに include M を行うといろいろとぶっ壊れるぽい
  • RSpec 関連で報告があったんですが、最小構成を調べてコメントしました
  • 最小構成は以下の通りで RSpec 関係なく再現しました。
module M; end
class X
  include M
end

module RefinementBug
  refine M do
    def refined_method
      puts "Called #refined_method"
    end
  end
end

class Y
  # Error if include M
  include M
end

using RefinementBug

puts "Using Ruby #{RUBY_VERSION}"

# error: undefined method `refined_method' for #<X:0x000055d4e22468c0> (NoMethodError)
X.new.refined_method

Bug #16932 defined? on refined method call returns non-nil even before using

  • Refinements で定義したメソッドが defined? に反映されていなかったバグ
  • 修正済み
class A
end

a = A.new
p defined?(a.x) #=> nil (OK)

m = Module.new do
  refine(A) do
    def x
    end
  end
end

a.x rescue p $! #=> NoMethodError: undefined method `x'
p defined?(a.x) #=> "method" (NG)

using m
p defined?(a.x) #=> "method" (OK)

Feature #14267 Lazy proc allocation introduced in #14045 creates regression

  • Proc 同士の比較が非互換になって RSpec で困ってる(いた?)らしい
  • チケット自体はかなり前
# regression.rb
def return_proc(&block)
  block
end

def return_procs(&block)
  block.inspect if ENV['INSPECT_BLOCK']

  proc_1 = return_proc(&block)
  proc_2 = return_proc(&block)

  return proc_1, proc_2
end

proc_1, proc_2 = return_procs { }

puts RUBY_VERSION
puts "Proc equality: #{proc_1 == proc_2}"
$ chruby 2.4
$ ruby regression.rb
2.4.2
Proc equality: true
$ chruby 2.5
$ ruby regression.rb
2.5.0
Proc equality: false
  • 2.5 当時のチケット

Feature #16928 Array#include_all? & Array#include_any?

# Array#include_all?(other_array)
[1, 2, 3, 4].include_all?([2, 4]) # returns true
[1, 2, 3, 4].include_all?([2, 4, 5]) # returns false
[1, 2, 3, 4].include_all?([]) # returns true
[1, 2, 3, 4].include_all?(nil) # returns false

# Array#include_any?(other_array)
[1, 2, 3, 4].include_any?([2, 4, 5]) # returns true
[1, 2, 3, 4].include_any?([6, 7]) # returns false
[1, 2, 3, 4].include_any?([]) # returns true
[1, 2, 3, 4].include_any?(nil) # returns false
  • どういうときに便利なんだろうか
  • 名前から期待する挙動がわかりづらい
  • 既存のコードでもっといい書き方ないかな

Feature #16924 instance_eval equivalent for RubyVM::InstructionSequence

  • iseq を任意のコンテキスト(レシーバ)で評価したいっていう提案
# ___path にかかれている Ruby のコードを ___dsl_object をレシーバにして実行したい
___dsl_object.instance_eval(File.read(___path),___path,0)

これを iseq 経由で実行するようにしたい

# ___path の中身を iseq に変換
iseq = RubyVM::InstructionSequence.compile_file(___path)
# その iseq を ___dsl_object をレシーバにして評価する
iseq.eval_in_instance(___dsl_object)

Bug #16919 IRB でマジックコメントの : の後に任意の文字を入力するとエラーが発生する

  • irb 上で #coding:u と入力して確定すると irb がクラッシュする
  • そもそも確定しなくても入力中にクラッシュする
    • #coding:utf-8 を入力中等
    • これは irb が入力毎に Ripper でパースしている為`

Ruby で循環参照してる配列で deep_dup するとどうなるか

deep_dup の話が Twitter で出たので試してみました。

ActiveSupport の #deep_dup をつかった場合

ActiveSupport#deep_dup の実装があるのでそれで試してみました。

require "active_support"
require "active_support/core_ext/object/deep_dup.rb"

a = [1, 2, 3]
a[3] = a

# error: in `map': stack level too deep (SystemStackError)
p a.deep_dup

と、いうことで SystemStackError で無事死亡。

Marshal.dump を使った場合

るりま の Object#clone のドキュメントに書いてある Marshal.dump をつかった例を試してみました。

a = [1, 2, 3]
a[3] = a

a2 = Marshal.load(Marshal.dump(a))
p a2
# => [1, 2, 3, [...]]
p a2[3]
# => [1, 2, 3, [...]]
p a2[3][3]
# => [1, 2, 3, [...]]

a2[2] = "homu"

p a2
# => [1, 2, "homu", [...]]
p a2[3]
# => [1, 2, "homu", [...]]
p a2[3][3]
# => [1, 2, "homu", [...]]

雰囲気うごいてそう。

ThinkPad X270 の液晶パネルを交換した話

何か手頃なラップトップがないかなーと中古品を漁っていたら ThinkPad X270 が安く手に入ったのでついでにはじめてラップトップで液晶パネルを変えてみました。
既存の構成だと液晶は 1366×768 なんですがこれだとややつらいんですよね…。
なので今回は 1920×1080 の FHD の液晶パネルに交換しました。
実際やってみるとそれ自体は結構簡単だったのでこういうのがさくっと交換できる ThinkPad すげー。

購入した液晶パネル

今回は Amazon で液晶パネルを購入しました。

レビューでも X270 で動作したと書かれていたので基本的にはこれで問題ないと思います。
ただ、コネクタの位置が『右側』と『中央』の2種類あり、X270 は『右側』にコネクタがあるので購入する場合はその点だけ注意が必要になります。

その他購入品

X270 だと 16GB のメモリに対応しており 1枚だけ指すことができるのでこれも購入。
デュアルチャンネル?なにそれおいしいの
最近はメモリがオンボードになっていて増設できないラップトップも増えているんですが、X270 あたりだとこのあたりは簡単に増設できるので便利ですね。

あとストレージも増設。
X270 だと(購入時の構成にもよるんですが)SATA SSD と WWANカードスロットに M.2 2242 SSD の2つにストレージを追加することができます。
今回はもともと付いていた SATA SSD とは別に WWANカードスロットに M.2 2242 SSD を増設しました。
ただ、最近の SATA SSD は高速なので IO 速度的な意味だと M.2 2242 とはほとんど変わらないですね…。
デュアルストレージにする以上の意味合いはなかったのでそのうち SATA SSD も別に購入しそう。

交換作業

1. 液晶パネルの交換

f:id:osyo-manga:20200509153404j:plain

液晶パネルのフレームは爪で引っかかってるだけなので隙間を開けてカードなどで広げて行けば割と簡単に外れます。

f:id:osyo-manga:20200509153445j:plain

フレームを取ると液晶パネルはそのまま取り外す事ができるので前に倒します。
左下にコネクタがあるので引っ張らない用に注意。
コネクタ部分は金具で固定されているので、金具を上に引っ張り上げてコネクタを後ろにスライドさせるようにすれば外す事ができます。
ネットで液晶パネルの交換記事を見てるとこのコネクタを外す作業で戸惑ってる人が結構いたので youtube の交換動画を見て見るといいと思います。

f:id:osyo-manga:20200509153457j:plain

あとは交換した液晶パネルを元の位置に戻してフレームをはめ込んで完成! ちなみに全行程でフレームをはめる作業が一番時間かかりました…。

2. メモリとストレージの交換・増設

f:id:osyo-manga:20200509153511j:plain

次はメモリとストレージの増設。
中身の構成を変える場合は裏蓋を全部引っぺがす必要があります。
ネジは緩めてもフレームからは外れないようになっているので便利です。

f:id:osyo-manga:20200509153526j:plain

メモリは金具を外してそのまま交換。

f:id:osyo-manga:20200509153537j:plain

WWANカードスロットは真ん中の左側の方に空きスペースがあるのでそこに追加。

3. 完成

f:id:osyo-manga:20200509153549j:plain

あとは裏蓋を締めて完成! 特に問題なくシュッと起動しました。

f:id:osyo-manga:20200509154037j:plain

解像度もちゃんと 1920×1080 になっててよかったよかた

所感

と、いうい感じで ThinkPad の液晶パネルを交換してみました。
思ったよりもトラブルなくて交換できたのでよかったよかった。
こういうのがさくっと交換できるのが ThinkPad の強みですねー。

参照

Ruby の右代入とエンドレスメソッド定義文を組み合わせるといろいろとつらいことがわかった

さてさて、先日 Ruby の開発版に右代入とエンドレスメソッド定義構文が入りました。
この構文については以下を参照してください。

この2つの機能を組み合わせて使うといろいろとつらいことがわかってきたのでちょっとまとめてみました

def hoge(value) = value => @value

アクセッサ的に引数をそのままインスタンス変数に代入するコードはよく記述すると思います。
それを右代入とエンドレスメソッド定義で書くと

def hoge(value) = value => @value

のように記述する事ができます。
もうこの時点でよくわからん!!みたいなユーザがいると思うんですがまだ意図としてはわかりやすいと思います。
しかし、これは意図した動作を行いませんでした。

def hoge(value) = value => @value

# 42 が @value に代入されてほしい
p hoge 42

# しかし実際には @value にはなぜか :hoge が代入されている
p @value   # => :hoge

これはなぜかというと def = よりも => の方が優先順位が低く (def hoge(value) = value) => @value と解釈されている(と予想)からです。
なので (def hoge(value) = value) の結果である :hoge@value へと代入されています。
ただし、この挙動はすでに修正されており最新版では以下のように意図する動作となります。

def hoge(value) = value => @value

# 42 が @value に代入される
p hoge 42
p @value   # => 42

となります。

以降は以下の状態で動作確認しています。

p RUBY_VERSION       # => "2.8.0"
p RUBY_DESCRIPTION   # => "ruby 2.8.0dev (2020-04-13T13:57:10Z master c28e230ab5) [x86_64-linux]"

private def hoge(value) = value => @value

private def hoge(value) = value => @value のように書くこともできます。
これは hoge メソッドを private メソッドとして定義したいという意図になります。
これも先程の修正コミットよりも前はシンタックスエラーになっていたんですが現状では動作するようになっています。

class X
  # メソッドを定義しつつ private メソッドにする
  private def hoge(value) = value => @value

  def foo
    hoge(42)
    p @value
    # => 42
  end
end

p X.new.foo

def hoge(value) = value => @value => method_name

意図としては def hoge の結果を method_name に代入したい…という感じなんですがもうよくわかりませんね…。
これは以下のような動作になります。

def hoge(value) = value => @value => method_name

hoge 42
p method_name   # => :hoge
p @value        # => 42

意図する動作にはなっています、が…。

private def hoge(value) = value => @value => @value2

もう何がしたいのかよくわかりませんね…。
意図としては value@value@value2 の2つの変数に代入したいってことですが…。
これは実行時エラーになります。

# error: `private': {:hoge=>nil} is not a symbol nor a string (TypeError)
private def hoge(value) = value => @value => @value2

これは

private((def hoge(value) = value => @value) => @value2)

と解釈されて :hoge => nilprivate メソッドの引数として渡しているから…ですかね。
もうよくわからない。

所感

とにかく =>def = の優先順位がつらいって感じがしますね。
個人的には =>def == ぐらい優先順位が低いとわかりやすいのかあ、とは思うんですが => が Hash の定義として使えるのでいろいろとつらそう…。
このあたりを解決するには => の記号を変えるしかなさそうなのかなあ…。
ちなみに知り合いは def hoge(value) = @value = value みたいに = が連なってるのもつらいと言っていたので def = も記号としてはつらそう。
うーん、左から右に流れるようにかけるので書いてて気持ちよくはあるんですが上で書いたように凝った書き方をすると意図しない動作になりそうなのできびしそうですねえ…。

その他

42 => result1 => result2

42 => result1 => result2

p result1   # => 42
p result2   # => 42

[1, 2] => result1, result2

[1, 2] => result1, result2

p result1   # => 1
p result2   # => 2

if 式で => を使う

# OK
if result = value
end

# Error
if value => result
end

42 => a = b => c

これはシンタックスエラーになります。

# syntax error, unexpected '=', expecting end-of-input
42 => a = b => c

メソッドにネストした => を渡す

def hoge(h)
  p h
end

value = 42
# OK : Hash 渡しになる
hoge :key => value
# => {:key=>42}

# NG : syntax error, unexpected =>, expecting end-of-input
hoge 42 => value => value

参照

Ruby 2.7 でも 1行 in を利用して右代入を行う

さてさて、先日 Ruby の開発版に右代入を行う => 演算子が追加されました。
これは 左辺値右辺の変数 へ代入するための演算子になります。

1 + 2 => result
result # => 3

Ruby 2.7 でパターンマッチ構文が実装された

Ruby 2.7 ではパターンマッチ構文が実験的に新しく追加されました。
この構文では次のように in 句で特定の値をキャプチャする事ができます。

homu = { name: "homu", age: 14 }
case homu
in { name:, age: ..20 => age }
  p name   # => "homu"
  p age    # => 14
end

こんな感じで Hash の各要素に対してマッチするかどうかの判定とその値を任意の変数にキャプチャする事ができます。

パターンマッチの 1行 in 構文

先程のパターンマッチでは case ~ in ~ end という構文だったのですが次のようにして 1行で expr in pattern を書くこともできます。

homu = { name: "homu", age: 14 }

# expr in pattern と1行で書くことができる
# マッチしなかったら NoMatchingPatternError になる
homu in { name:, age: ..20 => age }
p name   # => "homu"
p age    # => 14

こんな感じで in が1つしかない場合は1行で簡潔に記述する事ができます。

1行 in で右代入を行う

さて、先程は in の右辺は { name:, age: ..20 => age } のような複雑なパターンだったんですが、実は右辺には『変数だけ』を定義することができます

homu = { name: "homu", age: 14 }

# in の右辺に左辺の値がそのまま代入される
homu in result
p result
# => {:name=>"homu", :age=>14}

つまり in を利用すると

1 + 2 in result
p result # => 3

と記述する事ができます。
これって右代入ですよね?
もちろんメソッドチェーンされた値も代入できます。

(1..10).to_a.shuffle.select { _1.even? }.map { _1 * 2 }.map { _1.to_s } in result
p result
# => ["20", "12", "16", "8", "4"]

完全に右代入ですね!!!

補足

このネタは 2月18日に行われた Ginza.rb の LT で発表しました。 気になる人は以下のスライドを読んでみるといいと思います。

1行 in の有効的な使いみち