南風に乗って Ruby "enbugging" quiz が届いた

只今札幌の気温は9℃…

Ruby "enbugging" quiz

ruby-quiz-2024.storesinc.tech

これはなに?

あらかじめ用意されたコードを修正して、お題のエラーが出るように修正するよ!
変更文字数によってスコアが変わる、より小さいスコアを目指そう!

やったったぜ

最終スコア、All 1で12point!
最終スコア
全部で2時間半くらいかかったかなぁ。Extraに入ってから知らないことが多くて大変だった。
やってる途中に思ったこととか調べたことのメモをこの下に載せます。

ここから先はネタバレがあります!!!
やった人だけみるとよさそう!

やったときのメモ

Stage 1

- n = 'no error'
+ nil = 'no error'

これで Score 2 で通ったのでゲラゲラ笑ってた。とりあえずSocre2のまま次へ

Stage 2

10 このあたりで全て Score 1 でいけるという情報を得る。

Stage 3

-3 シンプル

Stage 4

Expected error: nil can't be coerced into Integer (TypeError)

nilはintにならんよってエラー。

n = 3
puts "I dunno error"[2 + n..]

nを別の変数に変える方法は2つくらい知ってる! お金で解決しました。

※ここでStage 1に戻って同様の方法でクリア

Stage 5

Expected error: index -4 too small for array; minimum: -3 (IndexError)

# Out of bounds?
ary = [1, 2, 3]
ary[3] = "no error"
puts ary[3]

これは ary[-4] = "no error" にすると発生させられる。
とりあえず ary[3-7] にしてエラーを発生させる。 それ以外のやり方がありそうだけど思い浮かばないのでScore 2のまま次へ…

Stage 6

Expected error: negative argument (ArgumentError)

puts "rhino error!!"[n * -1..-3]

わ、わかりづれーーーー
私がレビューしたら絶対(n * -1)ってカッコつけてもらうからな!!! (そこ?)

1文字リテラルにするのはすぐわかったけど書き方は覚えてなくてちょっと調べた。

Stage 7

一瞬悩んでから「これ、みたことあるぞ…これ…ツイッターでやったやつだ!」ってなって解けた。かなりの罠だよなぁ。ツイッターで見てなかったらすんなりは解けなかったかもしれないけど、Score 1で解けると考えたらすぐ気付ける可能性もある。
いまとなってはわからないこと…

Stage 8

そこまるっといらないじゃん!って笑った(ちゃんと気を取られた)

Stage 9

def x=1

この定義なに!? うちのRubyだとエラーになるんですけど…

うちの

> puts RUBY_VERSION
> 2.7.3

サイトの

3.4.0

なるほどね……

困ったときのputs

puts method(:x) してこのファイルがtest.rbであることがわかった。ちゃんと def x = 1 を書いた場所でメソッドとして定義されている。シンプルに代入されたものを返すメソッドを作れるんだなー。再代入も変数のようにできる。不思議…

エラー起こす方法は、メソッド呼ぶならここしかないな、と思ってからわりとすぐ解けた。これはできたときちょっと気持ちよかった!

Stage A

Expected error: cannot clamp with an exclusive range (ArgumentError)

def foo(...)
  "no error".clamp(...)
end

puts foo("a", "z")

まず def foo(...) ってなんだ?

https://docs.ruby-lang.org/ja/latest/class/Range.html

p(...) # Ruby 2.7 で導入されたメソッド引数の forward として解釈されてしまう

なるほど、丸投げするやつ!

それでこのエラーはなに…そもそもclampメソッドってなに…(調)
clampは範囲内にまるめるメソッドで、このエラーは引数に未満のRange(A...B:A以上B未満)を渡すとBをこえたときの値が不定になるからダメってエラー、なるほど。

つまりfooの中で呼んでるclampに("a"..."z")が渡ると例外がおこせる。 でもforward引数はそのまま渡すから、現在は("a","z")が渡っている。解法の候補は

  1. clamp("a", ? ) で同じエラーを起こせないか
  2. clamp("a"..."z") を渡せないか
  3. forward引数が渡ってきたときにRangeと解釈するようなclampの挙動がないか
  4. def foo(...) の引数をRangeとして定義できないか

みたいなこと考えてdocs.ruby-lang.orgのメソッド定義とRangeとclampのページ眺めてたらclampの引数変えればいけることに気づいた。...5
正解はシンプルだったけど色々知らないこと多くて結構時間かかったなぁ。 記法自体は知ってたけど使わないとすぐ頭からでてこないない。全然違うものに変化するので、先入観が邪魔してなかなかでてこなかったのかも。

Stage B

Expected error: 0: 1 === 0 does not return true (NoMatchingPatternError)

r = (0..1)
0 => ^r
puts "no error"

なにその2行目!?

irb(main):090:0> r = (0..1)
=> 0..1
irb(main):091:0> 0 => ^r
Traceback (most recent call last):
        3: from /Users/imaz/.rbenv/versions/2.7.3/bin/irb:23:in `<main>'
        2: from /Users/imaz/.rbenv/versions/2.7.3/bin/irb:23:in `load'
        1: from /Users/imaz/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):91: syntax error, unexpected =>, expecting end-of-input)
0 => ^r
  ^~

!!!
そろそろRuby3の世界にいくか……
今回はせっかくだからドキュメント読んで確認していこ!

=> を調べてみた

https://docs.ruby-lang.org/ja/latest/doc/symref.html#eq

rescue => XXX
例外処理で例外結果を変数 XXX に代入します。

これくらいしか載ってない…まさか3.4の機能か…!?

と思って試したらrに左の結果が入っていることがわかる。

0 => r で0になる!! 右代入できたの!!! 前回参加したRubyKaigiで聞いたぞ…(何年前だ) その時にも「例外のときに右代入が使われているんですよね」という話がでていた気がする。
ドキュメントには書いてないんだなと思ってフィードバックを送るリンクからリポジトリに飛んでissue検索したらRuby3.0の変更まとめてissue立ててあった。なんて親切なスレ…ここを見れば行けるかもしれない、3.0の世界に…!
https://github.com/rurema/doctree/issues/2458

^ を調べてみた

そんで… ^r はなんなの?ハットなんて正規表現のときにしか使わない… https://docs.ruby-lang.org/ja/latest/doc/symref.html#hat この記号のページ大好き大好き大感謝。NoMatchingPatternErrorだから正規表現ってことでいいのかなー。

=> ^ ってなんなの

エラーメッセージを見るに => ^=== と等価なのかな。

=== と同じ?

挙動は結構違う…一体何者なんだ…戻り値がないからif文とかにも使えない。 含まれていないときにエラーを出すためだけに存在している…?

puts 0 => ^r    #=> syntax error
puts (0 => ^r)  #=> void value expression 
puts r === 0    #=> true
puts (r === 0)  #=> true

右代入は戻り値がないとどこかでみたので試した。

puts 0 => r        #=> {0=>0..1}`
puts (0 => r)   #=> void value expression 

カッコつけるとvoid valueになるけど……
はっ!上の行はhashで解釈されてるのか!
右代入演算子は戻り値ないということであってるっぽい。

挙動から確認してみる

試しに 2=>^r にしたらエラーが出た。範囲に含まれるかどうか確認しているっぽいな。

test.rb:2:in '<main>': 2: 0..1 === 2 does not return true (NoMatchingPatternError)

ここでもう一回コードをみてみよう

r = (0..1)
0 => ^r
puts "no error"

今回出したいエラーは 0: 1 === 0 だから、指定のバグを出すためには2行目は変更せず、rの内容を1にする必要がある。

r を 1 にしたい

Rational 1/1 って 1じゃない?

色々試しているうちに 3r #=> 1/3 という記法に気づいた!(こういう記法のドキュメントみつからなかったけどあれば読みたい)
これ勝ったなと思って 0 => ^1r にしたけどパターンマッチではRationalはつかえないぽい…はいぼく…

ビット演算の記号でなんとかならんか

^ はビット演算子のXORなのでその前に数字とかいれてなんとかならんかなと思ったけど当然なんともならなかった。単体で使える ~ をつけてもエラー…Rangeに対してビット演算はできないのだ。そりゃそうか。
そもそも右辺をxorの式にしちゃうと右代入演算子がなにをすればいいかわからなくてsyntax errorになる。

rの定義方法をどうにかできんか

2行目を変えることができないなら r = (0..1) の方で1と認識させる必要がありそう。

  • rみたいに一文字でRange -> Integerに変換できるものとかありそうか
  • (0..1) をRangeじゃなくて引数とかそういう感じにできそうか(且つ最後の1を使いたい)
    • , だめそう、これは一番最初に試したな
    • ; これじゃエラーになるよなぁと思って入力したらなんかいけちゃった!!でもなんでこうなるかわからない…なにこれ…??
(;0..;1;) って古の顔文字みたい

r = (0..;1) でrには1が入っていることが確認できた。なにしてんのこれ…?
試しに ;1) を消してみたらsyntax errorになったので、カッコの中で使うことに意味があるっぽい。
(;0..;1;)とかでもいけた。あ、もしかしてカッコの中で関数スコープ的な感じで完結してる…!? で最後の行を返してる?
そうなんだ…演算子の優先順位的な感じで考えたとき、()より;が先なのか…! たしかにブロック引数を1行で書こうとしたときに ary.sum{|i| puts i; i+1 } って感じで書くもんなぁ。ふつうのカッコの中身が区切れてもおかしくないか…

これめっちゃ悩んだけど正解してみるとすごくシンプルだなぁ。さすがのExtra stage。

Stage C

Expected error: invalid radix 52 (ArgumentError)

puts "no " + 9219755.to_s(034)

実行すると no error がでてくる!すごい!なにこれ???
カッコの中身を0384にしたらエラーになって、03にしたら0/1/2の数列になった。ビットの単位か…? ビットというかx進数かな。

puts 9219755.method(:to_s) #=> #<Method: Integer#to_s(*)>

Integerクラスのto_sを調べる。

https://docs.ruby-lang.org/ja/latest/class/Integer.html#I_INSPECT

引数を指定すれば、それを基数とした文字列表現に変換します。

ほえ〜 なるほどね〜 基数っていうのはx進数のxのとこね!

今回出したいエラーは "invalid radix 52 (ArgumentError)"
radixは基数という意味らしい。つまり52進数なんてないって言われてるの?

puts "no " + 9219755.to_s(052)
#=> ArgumentError (invalid radix 42)

42...なんで? わからないままひとつずつずらしたところ、以下のコードで該当のエラーがでることがわかった。

puts "no " + 9219755.to_s(064)

これは…私の知ってるのはこれしかないですけど……

puts "no " + 9219755.to_s(0x34)

クリアー! 数値を変換する単純な方法を試しただけでそうなる気がしていたわけではない…

ブログを書いてる今思うけど、基数エラーの問題を基数で解決するのオシャレだなー。

…あ!052って10進数に無意味に0ついてるわけじゃなくて8進数の書き方か!
なんか違和感感じてirbで試して気付いた。そういえばそんなのあったなぁ。

irb(main):079:0> 01
=> 1
irb(main):080:0> 08
SyntaxError ((irb):80: Invalid octal digit)
irb(main):081:0> 07
=> 7
irb(main):082:0> 010
=> 8

Stage 5

ここまできて唯一スコア2で残っている Stage 5 に戻ってみる。
3を-4にするしかなさそうなんだよなぁ…と考えてStage Bで通過した大好き大好き大感謝記号ページで見た単体で使えるビット演算(反転)をやってみたらうまくいった!なんで!?

そもそもマイナスの2進法知らないんだよなぁ。いちばん左端が符号ビットになってると考えるのが自然かも。

0 -> -1 になることを考えると、すべて1だと-1になる。そこから…あ、反転か!-1基点で、普通の1が 00001 なら反転すると 11110 になり、左端が1だからマイナス、基点から-1だから-2みたいな感じか…へぇ〜おもしろ〜!ビット反転すると絶対値がずれるのがおもしろい。へぇ〜 いつ使うんだろ…

クリアしたぜ!

全部クリアしたやったー!報酬なにかなー!ってあけたらスタッフに話しかけてもらってねってあった。それはそう…これはRubyKaigi2024 STORESブースで出されし問題……( ˃̣̣̣̣̣̣ ω ˂̣̣̣̣̣̣ )

楽しかった〜!

ありがとうございました!
この問題の解説記事もあるみたい。これから読みます!