エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

難読RubyクイズReturns Day0 @RubyWorldConference2022

エンジニアリンググループのowlです。好きなマスコットはGopherくん、好きな言語はRuby。

さて、RubyKaigi2019では弊社ブースにて自分の難読RubyQuizを多くの方に楽しんで頂きました。

www.m3tech.blog

Rubyコミッターの方も首を捻っていたこのクイズですが……、3年振りにRubyWorldConference2022で弊社ブースで再びお楽しみ頂けます。tada! 🎉

2022.rubyworld-conf.org

今回は2日間それぞれで3問ずつ公開していきますが、いきなり普段やり慣れない難読Rubyクイズに挑戦しては怪我をするかもしれません。そこで事前のエクササイズとして追加の3問を解説付きでご用意いたしました。この記事で意味不明なRubyコードを読む感覚を取り戻した上で、ぜひ弊社ブースで本番にご挑戦ください。

エクササイズ問題

本番と同様の形式で、表示されているRubyのコードを実行した場合にどんな値が返されるかを4つの選択肢から答えて頂きます。

0-1. マイナスのマイナスは?


0-2. アンダースコアの意味


0-3. 綺麗で不可解で無意味


解説と答え

0-1. マイナスのマイナスは?

-a=--1.to_s

まずは優しい問題から見ていきましょう。この問題を解くのに必要な知識は1つです。

  1. Stringの前に-をつけた場合、selfあるいはクローンを返す

まずは=の右側について考えてみます。Integer#.to_sは問題ありません、数値を文字列へ変換するだけです。問題はこのto_sメソッドが何をレシーバーとするのかという点でしょう。先に言ってしまうと評価順は-((-1).to_s)となります。Rubyでは-1-(1)のように演算子-と整数リテラル1に分けて解析されるのではなく、それだけで符号つき整数のリテラルとして扱われます。

では - "-1"とは一体なんでしょう? RubyはStringに対する前置演算子として+/-を持っています。これらはfreezeされたStringに対してselfを得るか、それの複製を得るかを指定するのに使われます。

a="abc".freeze
=> "abc"
a.object_id
=> 280
(-a).object_id
=> 280
(+a).object_id
=> 300

ただし、この前置演算子は問題のようにfreezeされていない文字列に対して使うと+ならselfが、-なら複製が返されます。いずれにしても中身の文字列自体は同じ内容が返されてくるわけで、-"-1"の評価結果は"-1"となります。つまり、変数aに文字列"-1"が代入され、最後に文字列を持つ変数aに-の前置演算子が加わって評価されるというコードです。

つまり、このコードをわかりやすく分解するとこうなります。

-(a=-((-1).to_s))
=> "-1"

(補足) 他の言語では-a = 1のような記述はたいてい何らかのエラーになりますが、Rubyではエラーにならず上記のように-(a=1)の優先順で解決します。

-a = 1
=> -1
puts a
=> 1

0-2. アンダースコアの意味

[1_1, 1_2, 1_3].map{_2}|[]

「_」、それはRubyで色々な役割を持つ文字ですが、単に見た目だけの意味であったり重要な意味を持ったりします。この問題に必要な知識は3つだけ。

  1. 数値の中に入った「_」はコードの見た目以外に影響を与えない
  2. _1〜_9はブロックの仮引数として使える
  3. Array# | は和演算である

1についてはご存じの方が多いでしょう。数値の中には「_」を書くことができ、これは解析時に無視されます。本来は「12_000」のように桁数などをわかりやすく表記するためのものです。

しかし、2についてはコードゴルフ(可能な限り短くコードを書く競技)を嗜んでいる方でもなければ馴染みがないかもしれません。例えば以下のコードは仮引数の定義を省略してこのように書くこともできます。

[[1,2],[2,3],[3,4]].map{|a,b| (b*10).to_s}
=> ["20", "30", "40"]

[[1,2],[2,3],[3,4]].map{(_2*10).to_s}
=> ["20", "30", "40"]

ちょっと待った! [1_1, 1_2, 1_3].map{_2}はただの数値を要素とする配列に対して存在しない_2を呼んでいるじゃないか、するとエラーが発生するのか? と考えるかもしれません。答えはNoです。

前述の通り[1,2,3].map{_2}[1,2,3].map{|a,b| b}と同義です。そして、これらは両方とも[nil, nil, nil]を返しエラーは発生させません。

では最後に3の|メソッドについて説明しましょう。これは配列に対する和演算……つまり、左右の配列のいずれかに含まれた要素を重複なしに返します。例えばこんな風にです。

[1,2,3]|[2,4]
=> [1, 2, 3, 4]

ということは[nil, nil, nil]|[]はどうなるでしょう? もうおわかりですね、これは最終的に[nil]を返します。


0-3. 綺麗で不可解で無意味

_=_|_=__=_|_=_

記事の冒頭でこのコードの画像を表示していますが皆さんは最初これがRubyの実行可能なコードであることに気付かれたでしょうか?

今回のRubyクイズとして10問を作問しましたが、このコードがもっとも綺麗だと思います。直線だけ、シンメトリー、芸術点◎ですね。

意外にもこのコードを読み解くには2つの知識しか必要ありません。

  1. Rubyのローカル変数は代入を実行しなくとも宣言可能である
  2. nilが実行可能な|メソッドは論理和の演算をしてtrue/falseを返す

例えばこんなコードは実行可能でしょうか?

a = a

実は可能です。まだ一度も代入されていないローカル変数を呼んでいるにも関わらずNameErrorは発生しません。また、こんな書き方をしてもエラーなく実行可能です。

v = 1 if false 
puts v
=> nil

このようにRubyではローカル変数は代入がまだ実行されていない、あるいはそもそも実行不可能であっても変数の呼び出しよりも先に代入式さえ存在していればローカル変数がnilで初期化されます。

この仕様によってa=aは後者のaが評価される時点ですでに初期化されてnilとなっているためNameErrorにはならず、単にnilが代入されるのです。
("仕様"なのか? と思うかもしれませんがこれはドキュメントにも明記されています)

さて、ローカル変数「_」には初期値としてnilが入り、nil | hogeは右側hogeを評価して真ならばtrue、偽ならばfalseを返します。

必要な前提知識は揃いました。では_=_|_=__=_|_=_の評価を順番に追ってみましょう。

_=_|_=__=_|_=_
_=_|_=__=_|_=nil
_=_|_=__=_|nil
_=_|_=__=false
_=_|_=false
_=_|false
_=false

# 最終的に_と__の2つのローカル変数のどちらにもfalseが代入されている

こうしてこの実行できそうにもないRubyコードはfalseを返します。 tada! 🎉


To be continued

というわけでエクササイズとして3問ほどお付き合い頂きましたが如何でしたか? 解けた方も解けなかった方もぜひRuby World Conference当日は弊社ブースにて本番のクイズをお楽しみください。

イベント中の二日間それぞれで問題が日替わりとなるので両日ともに遊びに来ていただければ幸いです。

2022.rubyworld-conf.org

We are hiring!

エムスリーではRuby/Railsエンジニアを絶賛募集中です、RubyWorldConfのブースやWebから気軽にお声掛けください。

なお弊社で普段書かれているRubyのコードはとても治安が良いため、記事のような難読コードに業務で遭遇することはありません。 また難読コードでマージリクエストを出されてもLGTMしかねますのでご了承下さい。

jobs.m3.com