RubyKaigi 2019に参加された方もそうでない方もお疲れ様でした。あと数日で到達するゴールデンウィークを指折り数えるowlです。
RubyKaigiでは多彩なセッションにより豊富な知見が得られた他に、Rubyist同士の交流も商店街を貸し切ってのパーティや無料屋台などのイベントによるところもあって例年にないほどの賑わいでした。
さて、そんなRubyKaigiの様子やセッションの知見をみなさんに共有するのは他の参加者による才筆へ委ねることにして、この記事では弊ブースで自分が三日間日替わりに出題していた難読Rubyコードクイズの紹介と解説をしていきます。
毎日このクイズを解きに足を運んでくださった方には感謝しております。お目汚しですが自分が考えたクイズをMatzさん始めコミッターの方にまでご覧頂けたのは光栄です。
<!-- 儀礼的でちょっと退屈な口上はここまで -->
出題
まずRubyKaigiへ参加できなかった方にも楽しんで頂くために出題リストだけを先にお見せします。ぜひ答えを想像してから解説をお読み下さい。
原則としてどの問題もirb上で提示されたコードを実行した際に何が評価値になるかを答えます。
Day1 - 1
難易度:★☆☆
irb> !????!:!?!
=> ?
選択肢:「false」 「“?”」 「:!」 「SyntaxError」
Day2 - 1
難易度:★★★
irb> %%%%%%..%%[0].size[0]
=> ?
選択肢:「1」 「0」 「""」
(お詫び: 掲載当初は問題文がRubyKaigiで出題されたものと異なっていました)
Day3 - 1
難易度:★★☆
irb> puts = :puts
irb> puts = send(puts, puts) || puts(puts) { puts = “puts” }
irb> puts
=> ?
選択肢:「"puts"」 「:puts」 「nil」
Day3 - 2
難易度:★★☆
irb> %%%%%%%%?????:??
=> ?
選択肢:「""」 「"%"」 「"?"」 「":"」
Day3 - 3
難易度:★★★
irb> a = 0.0/0; a == a ? a : irb.quit
直後に何が発生しますか?
選択肢:「ZeroDivisionError」 「undefined local variable」 「irbが終了する」 「irbが起動する」
解説
Day1 - 1
irb> !????!:!?!
=> ?
パースするだけです。
今回の問題で最も正答率が高く、特殊な訓練を積んだRubyistたちから「簡単すぎる」という声も上がったため二日目の難易度がガクっと上がりました。
またこの時点で予定していたクイズは難易度が低すぎたため、全て廃棄して新たに難易度の高い問題をイベント中に考え直しました。
この問題に必要な知識は3つあります。
1つめ。?リテラルの存在。?aのように?の直後に一文字だけつけると"a"と書くのと同様に文字列を作れます。Matzさんのキーノートで思いがけずこのリテラルについて言及され「作ったのは間違いだったかも。削除したい…」とのこと。
2つめ。否定演算子!で、!trueのように書くと!の後ろを否定してtrue/falseを返します。例えば!false == true
となります。
3つめ。三項演算子で、rubyでは短いif-elseを (条件式) ? (true時の処理) : (false時の処理)という形式で記述することが出来ます。
このコードを読むと:が目に付きますが、先頭から順番に読むほうがわかりやすいと思います。先頭の!は否定演算子以外にないので次の????!
を考えます。:が三項演算子だと仮定すると?? ? ?!
とパースできることがわかります。
一応:をシンボルのリテラルと推測することも出来ますが、:!
のように記述することは出来ても:!?!
のようには記述できず文法エラーが発生します。(メソッドの再定義も同様)
irb> !(??) ? (?!) : !(?!) => false ? "!" : false => false
Day2 - 1
irb> %%%%%%..%%[0].size[0]
=> ?
ただでさえわかりにくいコードをわざと誤解を招く見た目にしています。コミッターを始めとしてRubyに詳しい人ほど0や1と回答していました。
おそらく少なくとも#sizeメソッドが実行できるならIntegerが返るはずで、マイナーですがInteger#[]というメソッドが存在しており必ず0か1を返すことを知っているからだと思います。
このクイズを思いついたのはRubyKaigi2019の1日めに偶然お会いしたtompngさん(Rubyの超難読コンテスト、TRICKの入賞者)とお話していた時に「記号が連続するっていえば、%記法の囲い文字に%を指定することが可能なんですよ」と教えて頂いたのがきっかけです。
(ちなみに自分がエムスリーテックブックで書いた難読rubyコードの解説でもtompngさんが作成したTRICK作品の画像を紹介していたので献本させて頂きました)
つまり一般には%w[aaa bbb ccc]や%[hoge]のように記述される%記法の囲み文字([])がどんな記号でも良いため、%ですら良い(%%hoge% == "hoge")ということです。
囲み文字が?や!などの記号でも良いことは知っていましたがまさか%自体でも可能だとは知らなかったので、せっかくなのでこれを利用して難読コードを書きました。
この知識からわかるのは、Rubyでは%%%と書いて""を生成できるということです、楽しい!
多くの人はこのコードをRangeオブジェクトだと考えたようですが、それは罠です。特に[0].sizeの部分で配列か何かを扱っているように見せていますがわざとです、この部分にはなんの意味もありません。ちなみに、そもそもRangeに#[]メソッドはありません。
実際には[0].size[0]
は独立した式になっています。つまり単なるInteger:0を要素とした配列でsizeメソッドを呼び、その結果であるintegerについて#[]メソッドを利用しています。integer#[]は引数番目のビットが立っているかどうかを0か1で返します。
最後に必要となる知識はString#%メソッドです。これはsprintfの言い換えで引数を元に書式付きフォーマットを返します。
"%.4f" % 3 => "3.0000"
つまり、このコードはこんな風にパースされます。
irb> (%%%) % ( (%%..%) % ([0].size[0]) ) => "" % ".." % 1 => ""
%メソッドを使っていますが、レシーバのStringが引数を使っていないので引数は虚空に消えます。さらに内側の%メソッドについても同様です。故にこのコードで実際に意味があるのは最初の三文字、%%%
だけです。
いっぱいあるから一個くらいなくても大丈夫
実は公開直後のこの解説記事ではDay2-1の問題をRubyKaigiで掲示していた問題とは異なるコードで記載してしまいました。
@gomachan46さんにTwitterでご指摘頂いて初めて気がついたのですが(ありがとうございます!)、当初この問題を前半の%が一個少ない%%%%%..%%[0].size[0]
として掲載してしまったのです。
なぜこんなことになったのかというと、記事を書く際に掲載資料がローカルになかったので自分の記憶を元に問題を復元してirbで結果を確認したのです。
しかし、不幸にもこのコードは%が一個くらいなくなっても全く同じように動く上に、%がいっぱいあったので気が付きませんでした。
あまりにも面白かったのでどうして%を一個くらい福岡国際会議場に忘れてきても動くのかについて解説しておきます。
irb> %%%%%..%%[0].size[0]
=> ?
この%が一個少ないバージョンでは%..
は空文字として評価されます。何故だかおわかりですね? そう、この部分において.
は%記法の囲み文字として扱われます。
そして、その次に存在する%[0]
はシンプルな%記法です。当然"0"
と評価されます。さて、%..
はともかくここで本来あるべきだった[0]
の代わりに"0"
が来てしまったのですが、ご安心下さい、Stringにも#sizeメソッドがあるので全く問題ありません。ちなみに文字列の長さを返します。
irb> %%%%%..%%[0].size[0]
=> (%%) % (%..) % (%[0]).size[0]
=> "" % "" % ("0".size[0])
=> ""
こうして無事に%[0].size
は[0].size
と同じ1を返し、本来のDay 2-1と全く同様に%メソッドによって使われることなく捨てられます。めでたし。
Day3 - 1
irb> puts = :puts
irb> puts = send(puts, puts) || puts(puts) { puts = “puts” }
irb> puts
=> ?
二日目に難易度を上げすぎたことを反省して簡単にしたのがこれです。しかし実際にはそれほど簡単ではなかったようでした。
その理由がそもそもputsメソッドの返し値が何かを知っている人が少なかったことです。予想外にみなさんはputsメソッドが何を返すか興味がないようですね?
putsメソッドは標準出力する内容に関わらず必ずnilを返します。この挙動は表示内容を返し値とするpメソッドとは異なるため注意です。
あとは読むだけです。いや、一点だけ補足が必要ですね。Rubyではメソッドで定義していないブロック引数を渡してもエラーも実行もされません。
また逆にブロック引数を定義しているメソッドにブロックを渡さなくてもそれだけではエラーにはなりません。(メソッド内でyieldを呼ぼうとした時点でエラーになります)
これはTRICK 2013で登場した傑作コードの「Best way to return true」(unak氏)でも使われたRubyの奇妙な仕様ですね。
$ruby.is_a?(Object){|oriented| language} # https://github.com/tric/trick2013/tree/master/unak
故に{ puts = “puts” }
は無視され影響を与えません。
irb> puts = send(puts, puts) || puts(puts) { puts = “puts” }
=> puts = send(:puts, :puts) || puts(puts) { puts = “puts” }
=> puts = nil || puts(puts) { puts = “puts” }
=> puts = nil || nil
=> puts = nil
Day3 - 2
irb> %%%%%%%%?????:??
=> ?
パースするだけですね。2-1の説明と1-1の説明を組み合わせれば先頭の%%%が""になってString#%で後半が捨てられるので答えが""になるはずだとピンと来るでしょう、不正解です。
まずこれはRubyが%%%%%%%
という奇妙な文字列を""%""
と評価してくれる嬉しい挙動を使っています。当然この部分は""となります。
しかし優先順位の問題でこの評価結果は後半にある三項演算子の条件式として機能します。
irb> ((%%%) % (%%%) % ??) ? (??) : (??)
=> ("") ? (??) : (??)
=> "?"
Day3 - 3
irb> a = 0.0/0; a == a ? a : irb.quit
直後に何が発生しますか?
この問題を誰かが完全に解けるとは思っていませんでした、irbのコミッターでもない限り。コミッターの方が2名ほどクリアしていきましたが意外にも他の問題で引っかかってしまうことがあったり得意不得意を感じますね。
これは3-2や1-1、2-1のようなパース問題ではなく純粋な知識問題です。irb上で実行されるという前提と返し値ではなく「直後に何が発生するか」を聞いている点に注意してください。
まず回答で多かったのが「ZeroDivisionErrorが発生する」という答え。確かに一般に数値を0で割ればエラーになるということは知られています。ただしそれがInteger(整数)であればです。Floatの場合は0.0/0 => NaN
、つまり非数(Not a Number)オブジェクトが得られます。
このNaNオブジェクトは「実数であることが期待されるが計算の結果、実数で表せないもの」ですが、特に面白い(?)性質としてRubyのネイティブなオブジェクトとしては唯一obj == obj => false
となります。故に三項演算子は後者のirb.quit
が評価されます。
irb.quit
はirbもquitメソッドも見たことがある人はまずいないでしょう。irbを起動するとそこはトップレベルですがirb関連のメソッドがいくつか追加されています。例えばirbを抜けたい時に呼び出すquitメソッドなどです。irb
メソッドもその一つであり、新規にIRB::Irbオブジェクトを生成しその中に入ります。
また、IRB::Irbオブジェクト自体はquitメソッドを持ちません。故にirb.quit
は不正な呼び出しであり、NoMethodErrorが発生します。しかしこのエラーが発生するのはirbメソッドの実行が終わり、ユーザが新たに開いたコンソールを抜けた後なので、この問題で聞かれている「直後に何が発生しますか?」の答えは「irbが起動する」です。
関連記事
Rubyistも読めない? 超難読Rubyコードの読み方 - エムスリーテックブログ
「RubyKaigi 2019」にPlatinum Sponsorとして参加しました!(ブース・セッションレポート) - エムスリーテックブログ
We are hiring!
エムスリーではRuby/Railsエンジニアを絶賛募集中です! クイズが解けた方も解けなかった方もお気軽にお問い合わせください。
なお弊社で普段書かれているRubyのコードはとても治安が良いため、記事のような難読コードに業務で遭遇することはありません。 また難読コードでマージリクエストを出されてもLGTMしかねますのでご了承下さい。