Ruby 開発チームの遠藤です。RubyKaigi 2019 が無事に終わりました。すばらしい会議に関わったすべてのみなさんに感謝します。
開催前に記事を書いたとおり、クックパッドからはのべ 7 件くらいの発表を行い、一部メンバは会議運営にもオーガナイザとして貢献しました。クックパッドブースでは、様々な展示に加え、エンジニアリングマネージャとトークをする権利の配布やクックパッドからの発表者と質疑をする "Ask the speaker" など、いろいろな企画をやりました。
クックパッドブースの企画の 1 つとして、今年は、"Cookpad Daily Ruby Puzzles" というのをやってみました。Ruby で書かれた不完全な Hello world プログラムを 1 日 3 つ(合計 9 問)配布するので、なるべく少ない文字を追加して完成させてください、というものでした。作問担当はクックパッドのフルタイム Ruby コミッタである ko1 と mame です。
RubyKaigi の休憩時間を利用して正解発表してました↓
盛り上がっています!#rubykaigi pic.twitter.com/rwXAFfOay6
— Cookpad Tech Life (@cookpad_tech) 2019年4月20日
問題と解答を公開します。今からでも自力で挑戦したい人のために、まず問題だけ掲載します。(会議中に gist で公開したもの と同じです)
問題
Problem 1-1
# Hint: Use Ruby 2.6. puts "#{"Goodbye" .. "Hello"} world"
Problem 1-2
puts&.then { # Hint: &. is a safe # navigation operator. "Hello world" }
Problem 1-3
include Math # Hint: the most beautiful equation Out, *, Count = $>, $<, E ** (2 * PI) Out.puts("Hello world" * Count.abs.round)
Problem 2-1
def say -> { "Hello world" } # Hint: You should call the Proc. yield end puts say { "Goodbye world" }
Problem 2-2
e = Enumerator.new do |g| # Hint: Enumerator is # essentially Fiber. yield "Hello world" end puts e.next
Problem 2-3
$s = 0 def say(n = 0) $s = $s * 4 + n end i, j, k = 1, 2, 3 say i say j say k # Hint: Binary representation. $s != 35 or puts("Hello world")
Problem 3-1
def say s="Hello", t:'world' "#{ s }#{ t } world" end # Hint: Arguments in Ruby are # difficult. puts say :p
Problem 3-2
def say s, t="Goodbye " # Hint: You can ignore a warning. s = "#{ s } #{ t }" t + "world" end puts say :Hello
Problem 3-3
def say "Hello world" if false && false # Hint: No hint! end puts say
以下、ネタバレになるので空白です
自力で解いてみたい人は挑戦してみてください。
解答
では、解答です。重要なネタバレですが、すべての問題は 1 文字追加するだけで解けるようになってます。
Answer 1-1
作問担当は ko1 でした。問題再掲↓
# Hint: Use Ruby 2.6. puts "#{"Goodbye" .. "Hello"} world"
解答↓
# Hint: Use Ruby 2.6. puts "#{"Goodbye" ..; "Hello"} world"
"Goodbye" ..
の後に ;
を入れています。これにより、Ruby 2.6 で導入された終端なし Range (Feature #12912) になります。この Range は使われずに捨てられ、"Hello"
が返り値になって文字列に式展開されるので、Hello world
が出力されるようになります。
この問題の勝者は tompng さんでした。なお、tompng さんは 1-2 と 1-3 も最初に 1 文字解答を発見しましたが、勝者になれるのは 1 人 1 問だけ、としました。
Answer 1-2
作問担当は ko1 でした。問題再掲↓
puts&.then { # Hint: &. is a safe # navigation operator. "Hello world" }
解答↓
puts$&.then { # Hint: &. is a safe # navigation operator. "Hello world" }
&.
の前に $
を入れて $&.
にしています。$&
は正規表現にマッチした部分文字列を表す特殊変数です。ここでは正規表現マッチは使われていないのでこの変数は nil
になりますが、重要なのはこの書換によって puts
メソッドに $&.then { "Hello world" }
を引数として渡す、というようにパースされるようになることです。then
メソッドはブロックの返り値を返すので、この引数は文字列 "Hello world"
になり、めでたく Hello world プログラムになります。
この問題の勝者は Seiei Miyagi さんでした。
Answer 1-3
作問担当は mame でした。問題再掲↓
include Math # Hint: the most beautiful equation Out, *, Count = $>, $<, E ** (2 * PI) Out.puts("Hello world" * Count.abs.round)
解答↓
include Math # Hint: the most beautiful equation Out, *, Count = $>, $<, E ** (2i * PI) Out.puts("Hello world" * Count.abs.round)
E ** (2i * PI)
というように i
を入れました。
これはちょっと知識問題で、 という公式を使います。この公式は「オイラーの公式」と呼ばれ、ヒントにあるように「最も美しい等式」などと言われることもあります。Ruby で書くと Math::E ** (1i * Math::PI) #=> -1
です。E ** (2i * PI)
はそれの二乗になので、浮動小数点数演算の誤差もあるのでおよそ 1 になります。Count,abs,round
によって正確に 1 になって、Hello world プログラムとなります。
この問題には別の意図もありました。これらの問題は 1 文字で解けると知っていたら、ブルートフォース(いろんな箇所にいろんな文字を挿入して実行してみるのを網羅的に試す)によって頭を使わずに解けてしまうのですが、この問題はそれをじゃまするために用意しました。というのは、$<
の前に *
を挿入して *$<
とすると、標準入力を配列化する演算となり、標準入力を待ち受けて動かなくなるようになります。よって、下手にブルートフォースをするとここで実行が止まります。ただ、このトラップにひっかかった人はいたかどうかはわかりません。
この問題の勝者は pocke さんでした。
Answer 2-1
作問担当は ko1 でした。問題再掲↓
def say -> { "Hello world" } # Hint: You should call the Proc. yield end puts say { "Goodbye world" }
解答↓
def say -> { "Hello world" }. # Hint: You should call the Proc. yield end puts say { "Goodbye world" }
}.
の .
を追加してあります。これにより、yield
はブロック呼び出しではなく、上の Proc 式に対して yield
メソッドを呼び出すようになります。Proc#yield
は Proc#call
の別名なので、このラムダ式が実行され、"Hello world"
を返すようになります。
この問題の勝者は Shyouhei さんでした。
Answer 2-2
作問担当は mame でした。問題再掲↓
e = Enumerator.new do |g| # Hint: Enumerator is # essentially Fiber. yield "Hello world" end puts e.next
普通に考えたら、次の 2 文字の解答になります。
e = Enumerator.new do |g| # Hint: Enumerator is # essentially Fiber. g.yield "Hello world" end puts e.next
Enumerator の最初の要素として "Hello world"
を yield
メソッドで渡し、Enumerator#next
によってそれを取り出し、それを表示します。Enumerator についてはドキュメントの class Enumerator を参照ください。
ヒントに従って考えると、次の 6 文字の解答にたどり着きます。
e = Enumerator.new do |g| # Hint: Enumerator is # essentially Fiber. Fiber.yield "Hello world" end puts e.next
Enumerator は Fiber のラッパのようなものなので、実はブロックの中で Fiber.yield
を呼ぶことでも要素を渡すことができ、上のプログラムと同じように動きます。
ただしこれは 6 文字も追加しているのでまったく最短ではありません。どうすればよいかというと、次が 1 文字解答です。
解答↓
e = Enumerator.new do |g| # Hint: Enumerator is # essentially Fiber. yield "Hello world" end puts e.next
コメントの中の essentially
と Fiber.
の間に改行文字を追加しました。コメントの中にある Fiber.
という文字列を利用するのがミソでした。すべての問題に適当なヒントコメントが書いてあるのは、この問題にだけヒントコメントをもたせることで不自然になってしまわないようにするためでした。
余談ですが、より面白い想定回答は↓でした。
e = Enumerator.new do |g| # Hint: Enumerator is # essentially Fiber. yield "Hello world" end puts e.next
essentially
の前に改行を入れています。essentially
は関数呼び出しとみなされますが、引数が Fiber.yield "Hello world"
なのでこちらが先に評価され、essentially
が実際に呼び出されることはなく、正しく動きます。この解答にたどり着いた人はいなかったようです。
この問題の勝者は youchan さんでした。
Answer 2-3
作問担当は mame でした。問題再掲↓
$s = 0 def say(n = 0) $s = $s * 4 + n end i, j, k = 1, 2, 3 say i say j say k # Hint: Binary representation. $s != 35 or puts("Hello world")
2 文字解答はたくさんあります。35
を 35-8
に変えたり、say j
を say j*2
に変えたり、or puts
を or 0;puts
と変えたり、いろいろなやり方が発見されていました。
1 文字解答は、意外と理詰めでたどり着けるようになっています。say
メソッドは「$s
を右に 2 ビットシフトし、引数 n
を足す演算」です。ヒントにあるとおり 35 の二進数表現を考えると 10 00 11
になります。それぞれ二進数で 2, 0, 3 なので、say(2); say(0); say(3)
という順序で say
を呼び出せばいいことがわかります。say i; say j; say k
は say(1); say(2); say(3)
なので、say k
はいじらなくて良さそうです。また、say
の引数を省略したら 0
になるので、say i; say j
をうまくいじって say j; say
という意味にする方法はないか、と考えます。ということで答えです。
解答↓
$s = 0 def say(n = 0) $s = $s * 4 + n end i, j, k = 1, 2, 3 say if say j say k # Hint: Binary representation. $s != 35 or puts("Hello world")
say i
のあとに f
を足して、後置 if 文にします。条件式は次行の say j
です。これにより、先に say j
が評価されて、say j
は真の値を返すので、if
の中の say
が無引数で呼び出されます。それから say k
が呼ばれることで、所望の挙動になります。
この問題の勝者は k. hanazuki さんでした。
Answer 3-1
作問担当は ko1 でした。問題再掲↓
def say s="Hello", t:'world' "#{ s }#{ t } world" end # Hint: Arguments in Ruby are # difficult. puts say :p
解答↓
def say s="Hello", t:'world' "#{ s }#{ t } world" end # Hint: Arguments in Ruby are # difficult. puts say t:p
say :p
を say t:p
に書き換えています。これにより、シンボルの :p
を渡していたところから、キーワード t
のキーワード引数として p
を渡すように変わります。p
は Kernel#p
の呼び出しで、無引数の場合は単に nil
を返します。よって、s = "Hello"
かつ t = nil
になり、"#{ s }#{ t } world"
は "Hello world"
になります。
この問題の勝者は Akinori Musha さんでした。
Answer 3-2
作問担当は mame でした。問題再掲↓
def say s, t="Goodbye " # Hint: You can ignore a warning. s = "#{ s } #{ t }" t + "world" end puts say :Hello
解答↓
def say s, t=#"Goodbye " # Hint: You can ignore a warning. s = "#{ s } #{ t }" t + "world" end puts say :Hello
t=#"Goodbye "
というように、オプショナル引数のデフォルト式をコメントアウトしています。これにより、次の行にある式がデフォルト式になります。この場合、s = "#{ s } #{ t }"
がデフォルト式です。s
はすでに受け取った引数で :Hello
が入っています。引数 t
は未初期化の状態で参照され、これは nil
になります(コメントにあるとおり、それは問題ないです)。よってこのデフォルト式は "Hello "
という文字列になります。あとはそのまま。
この問題の勝者は DEGICA さんでした。
Answer 3-3
作問担当は mame でした。問題再掲↓
def say "Hello world" if false && false # Hint: No hint! end puts say
解答↓
def say "Hello world" if% false && false # Hint: No hint! end puts say
if
の後に %
を書き足します。答えを見ても意味がわからない人のほうが多いのではないでしょうか。
Ruby には %
記法というリテラルがあります。%!foo!
と書くと、文字列リテラル "foo"
と同じです。デリミタ(先の例では !
)には、数字とアルファベット以外の任意の文字を使うことができます。上の例は、このデリミタとして改行文字を使っています。わかりやすく、デリミタを改行文字から !
に書き換えると、こうなります。
def say "Hello world" if%! false && false! # Hint: No hint! end puts say
後置 if の条件式に文字列リテラル(常に真)を書いたことになるので、このメソッド say
は常に "Hello world"
を返します。
なお、%
記法のデリミタに改行文字や空白文字を使える仕様は、matz が「やめたい」と言っていたので、将来廃止されるのかもしれません。
この問題の勝者は cuzic さんでした。
まとめ
Cookpad Daily Ruby Puzzles の問題と解答と解説でした。今回はわりと手加減せずに Ruby の仕様の重箱の隅をつつくような問題ばかりでしたが、「クックパッドのパズルがおもしろかった」という声も結構いただきました。まだやっていないかたは、今からでも(上の解説を見ずに)楽しんでいただければ幸いです。
こういうパズルが入社試験として出るわけではありませんが、このパズルをきっかけにクックパッドに興味を持ってくれた人は、↓からぜひ応募してください。
Special thanks
- hogelog:クックパッドのブースに「超絶技巧パズル(ってなに?)置いておこう」と発案した人
- sorah:シュッとチラシをデザインした人
- ブースにいた全員:パズルの配布や運営をした人たち
- 参加してくれた全員:解けた人も解けなかった人も
おまけ
もっと遊びたい人のためにエクストラステージを用意しておきました。答えはないので考えてみてください。
Extra 1
作問担当:mame
Hello = "Hello" # Hint: Stop the recursion. def Hello Hello() + " world" end puts Hello()
Extra 2
作問担当:mame
s = "" # Hint: https://techlife.cookpad.com/entry/2018/12/25/110240 s == s.upcase or s == s.downcase or puts "Hello world"
Extra 3
作問担当:ko1
(1 文字解答が 2 つあります)
def say s = 'Small' t = 'world' puts "#{s} #{t}" end TracePoint.new(:line){|tp| tp.binding.local_variable_set(:s, 'Hello') tp.binding.local_variable_set(:t, 'Ruby') tp.disable }.enable(target: method(:say)) say