RubyKaigi 2019 Cookpad Daily Ruby Puzzles の正解と解説

Ruby 開発チームの遠藤です。RubyKaigi 2019 が無事に終わりました。すばらしい会議に関わったすべてのみなさんに感謝します。

開催前に記事を書いたとおり、クックパッドからはのべ 7 件くらいの発表を行い、一部メンバは会議運営にもオーガナイザとして貢献しました。クックパッドブースでは、様々な展示に加え、エンジニアリングマネージャとトークをする権利の配布やクックパッドからの発表者と質疑をする "Ask the speaker" など、いろいろな企画をやりました。

クックパッドブースの企画の 1 つとして、今年は、"Cookpad Daily Ruby Puzzles" というのをやってみました。Ruby で書かれた不完全な Hello world プログラムを 1 日 3 つ(合計 9 問)配布するので、なるべく少ない文字を追加して完成させてください、というものでした。作問担当はクックパッドのフルタイム Ruby コミッタである ko1 と mame です。

RubyKaigi の休憩時間を利用して正解発表してました↓

問題と解答を公開します。今からでも自力で挑戦したい人のために、まず問題だけ掲載します。(会議中に 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 を入れました。

これはちょっと知識問題で、e^{i\pi} = -1 という公式を使います。この公式は「オイラーの公式」と呼ばれ、ヒントにあるように「最も美しい等式」などと言われることもあります。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#yieldProc#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

コメントの中の essentiallyFiber. の間に改行文字を追加しました。コメントの中にある 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 文字解答はたくさんあります。3535-8 に変えたり、say jsay j*2 に変えたり、or putsor 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 ksay(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 :psay t:p に書き換えています。これにより、シンボルの :p を渡していたところから、キーワード t のキーワード引数として p を渡すように変わります。pKernel#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 の仕様の重箱の隅をつつくような問題ばかりでしたが、「クックパッドのパズルがおもしろかった」という声も結構いただきました。まだやっていないかたは、今からでも(上の解説を見ずに)楽しんでいただければ幸いです。

こういうパズルが入社試験として出るわけではありませんが、このパズルをきっかけにクックパッドに興味を持ってくれた人は、↓からぜひ応募してください。

cookpad.jobs

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