Ruby 開発チームの遠藤です。RubyKaigi 2019 が無事に終わりました。すばらしい会議に関わったすべてのみなさんに感謝します。
開催前に記事を書いたとおり、クックパッドからはのべ 7 件くらいの発表を行い、一部メンバは会議運営にもオーガナイザとして貢献しました。クックパッドブースでは、様々な展示に加え、エンジニアリングマネージャとトークをする権利の配布やクックパッドからの発表者と質疑をする "Ask the speaker"など、いろいろな企画をやりました。
クックパッドブースの企画の 1 つとして、今年は、"Cookpad Daily Ruby Puzzles"というのをやってみました。Ruby で書かれた不完全な Hello world プログラムを 1 日 3 つ(合計 9 問)配布するので、なるべく少ない文字を追加して完成させてください、というものでした。作問担当はクックパッドのフルタイム Ruby コミッタである ko1 と mame です。
RubyKaigi の休憩時間を利用して正解発表してました↓
盛り上がっています!#rubykaigipic.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
includeMath# Hint: the most beautiful equationOut, *, Count = $>, $<, E ** (2 * PI) Out.puts("Hello world" * Count.abs.round)
Problem 2-1
defsay -> { "Hello world" } # Hint: You should call the Proc.yieldend 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 = 0defsay(n = 0) $s = $s * 4 + n end i, j, k = 1, 2, 3 say i say j say k # Hint: Binary representation.$s != 35or puts("Hello world")
Problem 3-1
defsay s="Hello", t:'world'"#{ s }#{ t } world"end# Hint: Arguments in Ruby are# difficult. puts say :p
Problem 3-2
defsay s, t="Goodbye "# Hint: You can ignore a warning. s = "#{ s }#{ t }" t + "world"end puts say :Hello
Problem 3-3
defsay"Hello world"iffalse&& 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 でした。問題再掲↓
includeMath# Hint: the most beautiful equationOut, *, Count = $>, $<, E ** (2 * PI) Out.puts("Hello world" * Count.abs.round)
解答↓
includeMath# Hint: the most beautiful equationOut, *, 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 でした。問題再掲↓
defsay -> { "Hello world" } # Hint: You should call the Proc.yieldend puts say { "Goodbye world" }
解答↓
defsay -> { "Hello world" }. # Hint: You should call the Proc.yieldend 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# essentiallyFiber. 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 = 0defsay(n = 0) $s = $s * 4 + n end i, j, k = 1, 2, 3 say i say j say k # Hint: Binary representation.$s != 35or 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 = 0defsay(n = 0) $s = $s * 4 + n end i, j, k = 1, 2, 3 say if say j say k # Hint: Binary representation.$s != 35or puts("Hello world")
say iのあとに fを足して、後置 if 文にします。条件式は次行の say jです。これにより、先に say jが評価されて、say jは真の値を返すので、ifの中の sayが無引数で呼び出されます。それから say kが呼ばれることで、所望の挙動になります。
この問題の勝者は k. hanazukiさんでした。
Answer 3-1
作問担当は ko1 でした。問題再掲↓
defsay s="Hello", t:'world'"#{ s }#{ t } world"end# Hint: Arguments in Ruby are# difficult. puts say :p
解答↓
defsay 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 でした。問題再掲↓
defsay s, t="Goodbye "# Hint: You can ignore a warning. s = "#{ s }#{ t }" t + "world"end puts say :Hello
解答↓
defsay 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 でした。問題再掲↓
defsay"Hello world"iffalse&& false# Hint: No hint!end puts say
解答↓
defsay"Hello world"if% false&& false# Hint: No hint!end puts say
ifの後に %を書き足します。答えを見ても意味がわからない人のほうが多いのではないでしょうか。
Ruby には %記法というリテラルがあります。%!foo!と書くと、文字列リテラル "foo"と同じです。デリミタ(先の例では !)には、数字とアルファベット以外の任意の文字を使うことができます。上の例は、このデリミタとして改行文字を使っています。わかりやすく、デリミタを改行文字から !に書き換えると、こうなります。
defsay"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.defHello 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 つあります)
defsay s = 'Small' t = 'world' puts "#{s}#{t}"endTracePoint.new(:line){|tp| tp.binding.local_variable_set(:s, 'Hello') tp.binding.local_variable_set(:t, 'Ruby') tp.disable }.enable(target: method(:say)) say