技術部の遠藤(@mametter)です。RubyKaigiお疲れ様でした!
クックパッドはRubyKaigiで、Rubyを使ったパズルを出してました。この記事では、出題者が想定していた解き方を公開します。自力で遊びたい人は解いた後で読んでください。
どんなパズル?
あらかじめ定義された謎の関数の中身を当てるパズルです。適当な引数で呼び出してみて、結果を観察して、中身を想像します。あたりがついたら、同じ関数を定義してみて、テストをパスしたらクリア。
次のURLでブラウザでプレイできます。もう賞品はもらえませんが、解きたい人は今からでも挑戦してみてください。
ruby-puzzles-2022.cookpad.tech
以下、ネタバレで各問題を解説していきます。
1問目
あらかじめヒントが書かれています。
# You can call `func1` p func1(0) #=> 1 p func1(1) #=> 2 p func1(2) #=> 3# Can you tell how `func1` is defined?# Hint: def func1(n) = n + ???# Define `answer1` that works like `func1`defanswer1(n) n end
func1
は引数に1足したものを返しているので、それをanswer1
にそのまま実装するだけです。
defanswer1(n) n + 1end
これで実行ボタンを押したらクリア。
2問目
# Congrats! You've solved the first puzzle!# Next, challenge func2! p func2("Hello") # => ??? (press "Run Ruby" to see output) p func2("world") # => ???defanswer2(str) str end
とりあえず実行した結果。テストが失敗します。
"HELLO" "WORLD" --- testing answer2 answer2: Test failed func2("Bar") != answer2("Bar")
"Hello"
が"HELLO"
になっているということは、大文字に変換すればよさそうですね。「Ruby 大文字 変換」などと検索すればすぐに答えが見つかると思います。次が答え。
defanswer2(str) str.upcase end
3問目
# Next, challenge func3! p func3(1) p func3(2) p func3(3) defanswer3(n) end
とりあえず実行。
[0, 1] [0, 1, 2] [0, 1, 2, 3] --- testing answer3 answer3: Test failed func3(376) != answer3(376)
[0, 1, ..., 引数]
を返せばいいようです。いろいろなやり方がありますが、たとえば次が答え。
defanswer3(n) (0..n).to_a end
RubyKaigi中は、ここまで解いたら賞品としてキッチンクロスが進呈されていました。ここから先は趣味の問題なので、急激に難易度が上がります。
4問目
まずはそのまま呼び出します。
# This call raises an error!# Try to find a correct way to call it. func4
secret2.rb:2:in `func4': no block given (yield) (LocalJumpError) from code.rb:6:in `main'
この例外は、ブロックが渡されていないのにyield
したら起きます。ということは、この関数にはブロックを渡す必要があります。適当なブロックを渡してみましょう。
func4 { nil }
今度はundefined method '+' for nil:NilClass (NoMethodError)
になりました。nil
に何かを足そうとしているようです。ということは、このブロックは整数か文字列あたりを返す必要があると推測できます。
p func4 { 1 } #=> 43
43が帰ってきました。他の値で試してみましょう。
p func4 { 1 } #=> 43 p func4 { 2 } #=> 44 p func4 { 3 } #=> 45
どうやら42を足した値を返しているようです。ということで、解答はこちら。
defanswer4yield + 42end
5問目
p func5(0) #=> 1 p func5(1) #=> 2 p func5(2) #=> 3
1を足すだけに見えるので、まずは試してみます。
defanswer5(n) n + 1end
実行すると、テストがwrong number of arguments (given 2, expected 1) (ArgumentError)
といって失敗しました。引数1つを受け取る関数に引数2つを渡した、と言ってます。つまり、テストはanswer5
に引数を2つ渡すことがあるようです。func5
を2引数で呼んでみましょう。
p func5(0, 0) #=> 0 p func5(1, 1) #=> 2 p func5(2, 2) #=> 4 p func5(3, 3) #=> 6
どうも、第2引数がないときは「第1引数 + 1」を返し、あるときは「第1引数 + 第2引数」を返していると想像できます。ということで答えはこちら。
defanswer5(a, b = 1) a + b end
ちなみにMethod#parameters
を使うと、func5
がどういう引数を受け取るかがわかります。
p method(:func5).parameters #=> [[:req, :augend], [:opt, :addend]]
必須引数augend(足される数)とオプション引数addend(足す数)を受け取るとわかりますね。ヒントの文にあるCheck the "parameters"はそういう意味でした。
6問目
p func6(1) #=> 1 p func6(12) #=> 3 p func6(123) #=> 6
どうやら各桁を足し合わせている(数字和、digit sum)と気づきます。そこで次を試します。
defanswer6(n) n.digits.sum end
しかしテストが通りません。
--- testing answer6 answer6: Test failed func6([**REDACTED**]) != answer6([**REDACTED**])
しかも、どういう引数でテストが失敗したのかが検閲(REDACTED)されています。いじわるですね。
しょうがないので、自分でテストを書いてみましょう。
100.times do |n| if func6(n) != answer6(n) puts "func6(#{n})=#{func6(n)}, answer6(#{n}) = #{answer6(n)}"endend
すると、いろいろ失敗例が見つかります。
func6(19)=1, answer6(19) = 10 func6(28)=1, answer6(28) = 10 func6(29)=2, answer6(29) = 11 func6(37)=1, answer6(37) = 10 func6(38)=2, answer6(38) = 11 func6(39)=3, answer6(39) = 12 ...
これを眺めると、また「各桁を足し合わせている」が見えてきます。つまり、1桁になるまでこれを繰り返すのではと推測できます(数字根、digits rootといいます)。よって次が答え。
defanswer6(n) n = n.digits.sum if n < 10 n else answer6(n) endend
7問目
p func7(0) #=> 1 p func7(1) #=> 2 p func7(2) #=> 3
また1を足すだけに見えます。試してみましょう。
defanswer7(n) n + 1end
しかしこれはよくわからない例外でテスト失敗します。
ヒントに「Try to pass non-Integer!」と書いてあるので、適当に文字列を渡してみましょう。
func7("A") #=> "B"
文字列もひとつ進みました。わかる人はこれでわかると思いますが、わからなかったら更に別のオブジェクト、たとえば浮動小数を渡してみましょう。
func7(1.0) #=> undefined method `succ' for 1.0:Float (NoMethodError)
succ
を呼び出そうとしていることがわかりました。ややマイナーなメソッドですが、Integer#succ
は1足した数を返し、String#succ
はひとつ進めた文字列を返します("A"→"B"→"C"→...→"Y"→"Z"→"AA"→"AB"→...。Excelのカラム番号っぽい)。
ということで答えはこちら。
defanswer7(n) n.succ end
8問目
p func8(0) #=> 0 p func8(1) #=> 1 p func8(2) #=> 1
これではなにもわからないので、サンプルを増やします。
p func8(3) #=> 2 p func8(4) #=> 1 p func8(5) #=> 2 p func8(6) #=> 2 p func8(7) #=> 3 p func8(8) #=> 1 p func8(9) #=> 2 p func8(10) #=> 2 p func8(11) #=> 3 p func8(12) #=> 2 p func8(13) #=> 3 p func8(14) #=> 3 p func8(15) #=> 4 p func8(16) #=> 1
わかる人はここでわかるかもしれません。1、2、4、8、16、……のときに1を返しているのが特徴的です。
ヒントに%b
と書いてあります。これはStringのフォーマット指定子で、2進数表記という意味です。試してみましょう。
p "%b" % 0#=> "0" p "%b" % 1#=> "1" p "%b" % 2#=> "10" p "%b" % 3#=> "11" p "%b" % 4#=> "100" p "%b" % 5#=> "101" p "%b" % 6#=> "110" p "%b" % 7#=> "111" p "%b" % 8#=> "1000"
func8
の返り値と合わせて、グッと見つめてみます。すると、2進数表記したときの1の数だとわかります。わかってください。この問題はもうちょっとヒントあったほうがよかったかもですね。とにかく答えはこちら。
defanswer8(n) ("%b" % n).count("1") end
別解としては、n.to_s(2).count("1")
など。ちなみにこの演算にはpopcount(population count)という名前があります。
9問目
p func9("foo") #=> "foo" p func9("bar") #=> "bar" p func9("baz") #=> "baz"
引数をそのまま返す関数でしょうか? そんなに簡単なわけはありません。
ヒントにPass a spy (or mock) object to func9
と書いてあります。spyやmockとは、主にテストで使われる言葉で、呼ばれたメソッドを記録するダミーオブジェクトを指します。Rubyではとても簡単にspyオブジェクトが定義できます。
classSpydefmethod_missing(name, *args) puts "#{ name } is called with #{ args }"endend
このインスタンスをfunc9
に渡してみましょう。
func9(Spy.new) #=> gsub is called with ["u-g0t-me", "yikes"]
もう何が行われているかわかりましたね。これが答えです。
defanswer9(s) s.gsub("u-g0t-me", "yikes") end
10問目
1回実行すると、次のようになります。
p func10 #=> 1 p func10 #=> 2 p func10 #=> 3
この関数は呼ぶたびに違う値を返しているので、状態を保存していることがわかります。グローバル変数で模倣してみましょう。
$counter = 3defanswer10$counter += 1end
$counter
の初期値をうまく合わせないと、func10 != answer10
というにべもないテスト失敗になります。冪等でない関数はたちが悪いですね。うまくいくと、wrong number of arguments (given 1, expected 0) (ArgumentError)
と出るので、実はfunc10
はオプション引数を受け取ることがわかります。
試行錯誤すると、次のような挙動に気づくと思います。
p func10 #=> 31 p func10 #=> 32 p func10 #=> 33 p func10(true) #=> 0 p func10 #=> 1 p func10 #=> 2 p func10 #=> 3
つまり、引数に真の値が与えられたときは状態を0に戻すようです。ということで次が答え。
func10(true) $counter = 0defanswer10(reset = false) if reset $counter = 0else$counter += 1endend
func10(true)
を呼んで状態をリセットするのがコツです。
別解として、func10
の状態をそのまま返す手があります。p global_variables
を使うと、$__func10_counter
といういかにも怪しいグローバル変数が発見できます。これをそのまま返してもテストはパスします。
defanswer10(*) $__func10_counterend
global_variables
を知ってる人向けの裏技というつもりだったのですが、この方法で解いた人の方が多かったかもしれません。
一旦まとめ
func10
を解くと、"Congraturations! You've completed all our puzzles!"と出ます。お疲れ様でした。
しかし、このパズルにはまだ隠しステージがあります。p methods
を見てみましょう。
p methods
#=> [:func1, :func2, :func3, :func4, :func5, :unlock_all_puzzles, :func6, :func7, :func8, :func9, :func10, :func11, :func12, :func13, :func14, :func15, :func16, :func17, :func18, :func19, :func20, ...]
実はfunc20
まで用意されていることがわかりますね。
挑戦の仕方はいままでと同じです。func11
を適当に呼んで中身を推測し、def answer11; ...; end
を定義して解答です。問題文はありませんので、全部自力になります。なお、10問目まではある程度Rubyを知っていればわかるように配慮しましたが、11問目からはだいぶ理不尽な問題も混ざっています。
この記事はずいぶん長くなってしまったので、func11
以降の解答はまた来週にします(楽しみにしてた人ごめんね)。func11
以降に気づいてなかった人はぜひ今からでも挑戦してみてください。