Cookpad Code Puzzle for RubyKaigi 2022の解説(裏ステージ)

技術部の遠藤(@mametter)です。おまたせしました、RubyKaigi 2022で出題したクックパッドブースの企画、Cookpad Code Puzzle for RubyKaigi 2022の裏ステージの解説です。

このパズル自体の解説は前編の記事をごらんください。

techlife.cookpad.com

さっそく11問目から解説していきます。

11問目

p func11(0)     #=> -510240563
p func11(1)     #=> -171748573
p func11(2)     #=> 405559065
p func11("foo") #=> -62024031

何を与えてもよくわからない整数が帰ってきますね。リロードすると結果が変わることにも気づくかもしれません。つまり、これはハッシュ値であろうと当たりがつきます。ということで答えはこちら。

def answer11(v)
  v.hash
end

11問目からは問題文もないし、知らないと解けない問題が多めになります。

12問目

p func12(0) #=> 1
p func12(1) #=> 1
p func12(2) #=> 1

この辺を見てても1ばかり帰ってきますね。1以外になるところを探してみましょう。

300.times { p [_1, func12(_1)] if func12(_1) != 1 }
[11, 2]
[22, 2]
[33, 2]
[44, 2]
[55, 2]
[66, 2]
[77, 2]
[88, 2]
[99, 2]
[100, 2]
[101, 2]
[110, 2]
[111, 3]
[112, 2]
[113, 2]
...

ゾロ目が目に付きますが、100101も2になることから、同じ数字の出現数が関係しそうです。確認してみましょう。

p func12(1111)   #=> 4
p func12(11111)  #=> 5
p func12(111111) #=> 6

では、違う文字が複数ある場合は?

p func12(1122)     #=> 4
p func12(111222)   #=> 9
p func12(11112222) #=> 16

p func12(11)         #=> 2
p func12(11222)      #=> 6  (= 2 * 3)
p func12(1122233333) #=> 30 (= 2 * 3 * 5)

こういうのとにらめっこすると、文字ごとの出現数の積とあたりがつくのではないでしょうか。なのでこれが答え。

def answer12(n)
  n.to_s.chars.tally.values.inject(&:*)
end

13問目

100.times { p [_1, func13(_1)] }
[0, "AS"]
[1, "UT"]
[2, "US"]
[3, "UT"]
[4, "UT"]
[5, "UT"]
[6, "UT"]
[7, "UT"]
[8, "UT"]
[9, "UT"]
...
[22, "IS"]
[23, "IS"]
[24, "IS"]
[25, "IS"]
[26, "IS"]
[27, "IS"]
[28, "IS"]
[29, "IS"]
[30, "IS"]
[31, "IS"]
[32, "IS"]
[33, "IS"]
[34, "IS"]
[35, "IS"]
[36, "IS"]
...

この問題はもう経験と勘を働かせるしかないです。先頭2文字を取っているのだろうと予想し、"UT"で始まる用語というと、UTCUTFかな?などと考えます。すると、"IS""ISO"と当たりが付き、RubyがサポートしているEncodingの一覧では?と考えついてください。ということで答えです。

def answer13(n)
  s = Encoding.list[n]
  s.name[0, 2] if s
end

func13からfunc15は、正体に気づかないまま、引数と返り値の対応をすべて記憶してテストを通した人が多かったかもしれません。それもよいと思います。

Table13 = {}
1000.times { Table13[_1] = func13(_1) }

def answer13(n)
  Table13[n]
end

14問目

100.times { p [_1, func14(_1)] }
[0, "Z"]
[1, "O"]
[2, "T"]
[3, "T"]
[4, "F"]
[5, "F"]
[6, "S"]
[7, "S"]
[8, "E"]
[9, "N"]
[10, "T"]
[11, "E"]
[12, "T"]
...

これはよくある謎解きです。O→T→T→F→F→S→S→E→? という出題形式が多いかも。

答えを言うと、数字を英語で言ったときの頭文字です。One、Two、Three、Four、Five、……。ということで答え。

def answer14(n)
  raise "The argument should be an integer from 0 to 1000" if n < 0 || n > 1000
  if n <= 20
    "ZOTTFFSSENTETTFFSSENT"[n]
  elsif n < 100
    answer14(n / 10)
  else
    answer14(n / 100)
  end
end

なお、func14(1000)"T" になるのは意図してないバグでした。One thousandなので"O"が正しそう。ハマったひとがいたらごめんなさい。

15問目

100.times { p [_1, func15(_1)] }
[0, "AR"]
[1, "AR"]
[2, "Ar"]
[3, "Ar"]
[4, "Ba"]
[5, "Bi"]
...

この問題も経験と勘です。"RU"で始まるものが多いあたりで"RUBY"と気づけるかどうか。これはトップレベルの定数名の先頭2文字です。ということで答え。

def answer15(n)
  s = Object.constants.sort[n]
  s.to_s[0, 2] if s
end

16問目

p func16  #=> undefined method `call' for nil:NilClass (NoMethodError)

callというあたりから、ブロック引数を受け取っているのでは、と考えます。

p func16 {}        #=> false

falseが帰ってきました。falseを返すからには、trueを返すこともあるはず。ブロックの返り値をいろいろ試してみます。

p func16 { 0 }     #=> false
p func16 { 1 }     #=> false
p func16 { "foo" } #=> false

falseから変わりません。ブロックに引数が渡されているのでしょうか?

func16 {|*a| p a } #=> []

なにも渡されていない……いや、可変長引数で見えない引数がありますね。たとえばselfです。

func16 { p self } #=> main

残念、selfは変わっていませんでした。もうひとつ見えない引数があります。ブロック引数です。

func16 {|&b| p b } #=> #<Proc:0x0141b684 secret2.rb:66>

ビンゴ。ブロックが渡されてるようなので呼んでみましょう。

p func16 {|&b| b.call } #=> true

やった、返り値がtrueに変わりました。ということで答え。

def answer16(&blk)
  flag = false
  blk.call { flag = true }
  flag
end

たぶんこれが一番むずかしい問題だったのではないかと思います。ブロックにブロック引数を渡すこと自体がマイナーだし、気づきにくいですよね。解けた人はすごい。

17問目

func17(0)

実行するとJSのalert("0")が出てきます。この問題はRuby on WasmのJS連携を試してもらいたくていれました。

Ruby on Wasmのドキュメントを頑張って読み始めてもいいですが、適当な関数でテストを走らせてみるとRuby on WasmのJS.evalへのURLが出てきます。

def answer17(n)
end
--- testing answer17
test17.rb:10:in `test17': JS's alert must be called (Hint: https://github.com/ruby/ruby.wasm/blob/194f4a1dfe9036018fef9810d71e23a24cd97bd9/ext/js/js-core.c#L81) (RuntimeError)

ということで答え。

def answer17(s)
  JS.eval("alert(#{ s.to_s.dump })")
end

18問目

func18をいろいろ呼んでもよいですが、この問題は適当なanswer18を定義したほうが早かったかもしれません。

def answer18(s)
end
--- testing answer18
test18.rb:4:in `block in test18': answer18("0") != func18("0") (RuntimeError)
    from test18.rb:2:in `upto'
    from test18.rb:2:in `test18'

テストの通り、"0"を渡してみましょう。

p func18("0") #=> [0]

整数にして配列にする?ということで試します。

def answer18(s)
  [s.to_i]
end
--- testing answer18
test18.rb:10:in `block (2 levels) in test18': answer18("80+41") != func18("80+41") (RuntimeError)
    from test18.rb:8:in `each'
    from test18.rb:8:in `block in test18'
    from test18.rb:6:in `times'
    from test18.rb:6:in `test18'

"80+41"も渡されるようです。

p func18("80+41") #=> [80, "+", 41]

これを繰り返すうちに、文字列を分解すればいいのだとわかります。ということで答え。

def answer18(s)
  s.scan(/\d+|[+\-*\/()]/m).map {|s| s =~ /\d+/ ? s.to_i : s }
end

これは「字句解析」と言われる処理です。

19問目

18問目と同様にテストの失敗を観察していくと、こういうような挙動をすることがわかります。

p func19([2, "*", "(", 3, "+", 4, ")"])
#=> ["*", ["value", 2], ["+", ["value", 3], ["value", 4]]]

これは構文解析ですね。「再帰下降パーサ」で検索するとCでのコード例が見つかります。これを移植したらOK。

def factor(tokens)
  t = tokens.shift
  if t.is_a?(Integer)
    ["value", t]
  elsif t == "("
    r = expr(tokens)
    tokens.shift
    r
  else
    "unknown: #{ t }"
  end
end

def term(tokens)
  r = factor(tokens)
  t = tokens.first
  while t == "*" || t == "/"
    r = [tokens.shift, r, factor(tokens)]
    t = tokens.first
  end
  r
end

def expr(tokens)
  r = term(tokens)
  t = tokens.first
  while t == "+" || t == "-"
    r = [tokens.shift, r, term(tokens)]
    t = tokens.first
  end
  r
end

def answer19(tokens)
  expr(tokens)
end

この問題が一番面倒くさかったのではないかと思います。

20問目

ここまで来た人なら、func20はこういう挙動だとわかるでしょう。

p func20(func19(func18("2*(3+4)"))) #=> 14

ということで、func18func19func20は四則演算の字句解析、構文解析、評価器という構成でした。答えはこんな感じ。

def answer20(e)
  case e[0]
  when "value"
    e[1]
  when "+"
    answer20(e[1]) + answer20(e[2])
  when "-"
    answer20(e[1]) - answer20(e[2])
  when "*"
    answer20(e[1]) * answer20(e[2])
  when "/"
    answer20(e[1]) / answer20(e[2])
  else
    raise "unknown operator: #{ e[0] }"
  end
end

これで全問突破です!おめでとうございます!

まとめ

前後編の長い記事になってしまいましたが、Cookpad Code Puzzle for RubyKaigi 2022の解説でした。

隠された関数の定義を当てるという問題形式 *1 は、いくらでも難しい問題を作れてしまうので、事前に社内でテストプレイをするなどして難易度調整に腐心しました。思ったより多くの人がfunc20まで解いてくれたのでホッとしました。

クレジット:一部の問題は同僚のささださんの発案だったり、@hirekokeさんの発案だったりします。

おまけ:チート対策

Rubyにはこの手のパズルを台無しにするいろんな機能があります。このパズルでは、それらの機能をそこそこ無効にしていました。ただ、潰しきれなかった機能もあります。どのような対策をしたか、それを乗り越えるチート方法などを紹介します。

テスト入力を盗み見る

answerの中で引数を出力させることで、テスト入力を盗み見ることができます。

def answer1(n)
  p n #=> 826
end

7問目以降ではこのチートは禁止してあります。

def answer7(n)
  p n #=> in `write': No writing in stdout during answer :-) (RuntimeError)
end

$stderrを使ってもダメです。

def answer7(n)
  $stderr.puts n.inspect
    #=> in `write': Are you trying me? I've also closed the stderr loophole! (But there is actually a way to see the secret test input. Do you know how to do it? (RuntimeError)
end

$stdout.writeを上書きしていることに気づけば、いくらでも回避方法があります。たとえば、事前に $stdout.method(:write)を取り出しておくのが簡単でしょう。

Write = $stdout.method(:write)
def answer7(n)
  Write.call(n.inspect + "\n")
end

ほかには、IO.for_fd(1)を使って$stdoutを開いたり、見たい文字列をraiseの引数として呼び出したりすれば回避できます。あまりRuby環境を汚さない回避方法としては、JS連携を使ってconsole.logを呼び出すという技もありました。

error_highlightを使う

NoMethodErrorを引き起こすことで、該当行のソースが見えてしまいます。

p func2(1)
secret.rb:6:in `func2': undefined method `upcase' for 1:Integer (NoMethodError)

  s.upcase
   ^^^^^^^
    from code.rb:1:in `main'

これは意図しなくても発動してしまうので、対策として、func4以降ではerror_highlightをわざと止めてあります。error_highlight便利ですね!

answerXからfuncXに移譲する

次のようにすれば、func1の中身を推測しなくてもanswer1は完全に同じ挙動にできてしまいそうです。

def answer1(...)
  func1(...)
end

しかし、これは塞いであります。

--- testing answer1
secret.rb:1:in `func1': Do not use func1 during answer :-) (RuntimeError)
    from code.rb:2:in `answer1'
    from test1.rb:3:in `[]'
    from test1.rb:3:in `block in test1'
    from test1.rb:2:in `each'
    from test1.rb:2:in `test1'

どうしているかというと、func1の先頭に次のようなコードを仕込んでありました。

raise "Do not use func1 during answer :-)" if caller.any? { _1.include?("answer") }

つまり、バックトレース中に"answer"を含むメソッド名があったら例外にしています。

ちなみに後で報告されたことですが、この対策はFiberを使うことで回避できました。なるほどなあ。

def proxy1(...)
  Fiber.new { func1(...) }.resume
end

def answer1(...)
  proxy1(...)
end

チートに使えそうな機能を使う

RubyVM::InstructionSequence.of(method(:func1)).disasm などをすると func1 のバイトコードが覗けてしまうので、このようなメソッドは大体remove_methodしておきました。TracePointは定数を上書きしておきました。

ただ、ObjectSpace.each_objectを対策するのが抜けてました。報告された中で一番豪快なチートは、次のようにすれば正解の定義がすべて抜き出せてしまうというものでした。

ObjectSpace.each_object(String) {|s| puts s if s.start_with?("def func") }

いやー抜けてたなあ。

リバースエンジニアリングへの道(CTFに興味ある人向け)

このパズルはすべてブラウザで動いているので、正解のデータもすべて当然ブラウザ上に入っています。よって、リバースエンジニアリングをすれば理論上はすべてがわかります。

JSのソースコードを見ると/src/app.datというファイルを参照していることがわかります。このファイルは、RubyKaigi 1日目のキーノートでも少し出てきたwasi-vfsを使ってwasmファイルに埋め込んであるので、パズルのコードからでも読めます。

p File.binread("/src/app.dat") #=> "YARB\x03\x00\x00\x00..."

これはRubyのバイトコードをダンプしたデータで、RubyVM::InstructionSequence.load_from_binaryを使ってロードすることができます。 このダンプデータは環境依存なので、WasmのRubyでないとload_from_binaryできません。しかし、パズルのWasmではRubyVM::InstructionSequence.load_from_binaryremove_methodしておいたので、別途wasmtimeなどでwasm32のRubyを動かしてdisasmを見る必要があるでしょう。また、正解のコード部分はAES暗号化されています(パズルではJS連携を使ってWebCrypto APIで復号しています)。腕に覚えがある人は、解読を頑張ってみてください。

*1:この問題形式は、International Conference of Functional Programming(ICFP)という学会で開催れているプログラミングコンテスト(ICFP Programming Contest)の2013年の問題にインスパイアされています。詳しくは自分のICFPc 2013参加体験記などをご覧ください。この問題から理論っぽい要素を抜いて、代わりにRuby知識を前提にするという発想で作りました。

クックパッドは RubyKaigi 2022 に参加&スポンサーしてきました!イベントレポート

RubyKaigi 2022 お疲れさまでした!

クックパッドは RubyKaigi 2022 の Ruby Sponsor です

クックパッド株式会社は Ruby Sponsor として RubyKaigi 2022 を応援させていただきました。

prtimes.jp

クックパッドからは総勢24名が三重は津市に向かい、現地参加しました。一社からの人数としては最大だったのではないでしょうか。白の Cookpad Tシャツを着た人がやたらと目に入ったかもしれませんが、事実大勢いたからなのでした。

また、Ruby Committers’ & Wi-Fi Sponsor として、会場の Wi-Fi ネットワークなどの設営にもメンバーが携わっていました(京大マイコンクラブ (KMC)と共同)。 こちらの舞台裏についても追って記事が公開される予定ですので、お楽しみに。

Cookpad Code Puzzle への挑戦もお待ちしています (裏ステージもあるよ)

スポンサーブースにリンクを掲示していました Cookpad Code Puzzle は挑戦していただけましたか?

ruby-puzzles-2022.cookpad.tech

ちょうど本日、表ステージ (func1 から func10 まで) の解説を公開しましたので、答え合わせチャンスです。

techlife.cookpad.com

ところでこのパズル、実は隠しステージとして func11 から func20 まであるのです。腕に自信のある Rubyist の皆様の挑戦をお待ちしています! (やりかたは上の記事を参照してください)

イベントレポート

さて本題。

Ruby を使い倒している会社として、RubyKaigi に参加することには大きな意義があります。 今回は数年ぶりの物理開催 Kaigi ということもあり、セッションはもちろんのこと、それ以外のところでもクックパッドメンバーが大いに議論・コミュニケーションをしていました。

ここでは「良かったセッション」「印象的だったできごと」「交流・懇親」などのテーマでメンバーが執筆したレポートをお届けします。

@_ko1 (Speaker, Ruby Committer)

  • スピーカー&Rubyコミッターとして参加しました(Making *MaNy* threads on Ruby (発表資料), Ruby Committers vs The World (発表資料) の2つ)。ご参加いただいた皆様に御礼申し上げます。
  • 良かったセッション 今回見ていた発表ですが、どうしても応用的な発表よりもRubyインタプリタ内部に関する発表をよく見ていました。その中でも2つ取り上げます。
    • 1つ目は Datadog でメモリプロファイリングをとるための仕組み検討するHunting Production Memory Leaks with Heap Sampling。二人とも Datadog の人かと思ったら、お一人は ZenDesk で Datadog のカスタマーなんですね。その方が情熱をもって Datadog の人と一緒にメモリプロファイリングの仕組みを作る、という話でした。発表中で出てきた Ruby のインターナル API は私の責任でデザインしたのですが、確かにこういう用途だと色々足りないなぁ、というのがわかるので、なんとかしたいところです。
    • 2つ目は MJIT のオリジナルの作者である vlad の新作 A Faster CRuby interpreter with dynamically specialized IR が興味深かったです。彼は、もともと Ruby を Register based virtual machine にした上で MJIT の手法で JIT コンパイラを作りたかったのですが、どうしてもわかりづらい部分ができてくるんで(わかりづらさを正当化する性能が出るかどうか不明だったので)採用しなかったのですが、その手法、そしてそれをさらに勧めた手法(x86 のようにオペランドの種類を色々増やしたりする)で大きな性能向上を収めた、というものでした。正直、あれであんなに速くなる、という理由がイマイチわからないので、勉強してみたいと思います。
  • 印象的なできごと ウナギと松坂牛を食べたのですが、どちらもとてもおいしくて衝撃でした。ただ、この歳になるとサーロインのステーキは無理だということがわかりました。
  • 交流
    • いつもあまりブース回れないのですが、今年はRuby biz グランプリ の営業で回りました。あんまり知られていないので、来年はご応募をご検討いただけますと幸いです。
    • いろんな発表で、「過去に実装した人」という意味で名前を呼ばれるのですが、実際に会場で声をかけられることはほぼありません。まぁ、内部実装する人に話すことはそんなないですよね...。使ってる人の話とか聞きたいんですが。

@mametter (Speaker, Ruby Committer)

  • TRICK 2022 (Returns), Ruby Committers vs The World (発表資料), error_highlight: user-friendly error diagnostics (発表資料)
  • 良かったセッション
    • Ruby meets WebAssembly (発表資料): クックパッドにインターンに来てくれたkateinoigakukunさんが無事Ruby界隈に華々しくデビューできたのでよかった
    • Matz Keynote: 内容が新作だったのでよかった
  • 印象的なできごと
    • ひさびさの物理イベントがおもったより刺激的だった。はじめましての人や数年ぶりの人にいっぱい会えて単純に楽しかった。
    • ブースを回ってみて、各社の工夫に感心した。手前味噌だけどコードパズルもそれなりに好評だったようでほっとした。
  • Shopifyの人たちの多くと初めて物理で会えた。今後も仲良くしていきたい。

@asonas

asonasが参加したセッションの中で印象的だったのは @nay3 の「The Better RuboCop World to enjoy Ruby (発表資料)」でした。

チームでRubyを書いていく上で、常に自分たちが意識しなくてはならない、所謂「かたい」設定と状況によって柔軟に参考にする「やわらかい」設定に分けるという提案がささりました。 特に聞いていてよかったのは、これらの設定は私たち人間によって考える必要があり、ディジタルに2値で決めうることではなく、人間らしく柔軟にプロジェクトのスタイルガイドを決めればよい、ということでした。

熟練者がいるチームではRuboCopとすぐにうまく波に乗れることができるかもしれません。しかし、世の中の開発チームはそういったものばかりではないので、この資料を読みどのようにしてRuboCopと共にうまく開発をしていくかの指針になると思いました。 また、RuboCopの設定とうまく付き合うために必要な心構えとして、RuboCop vs 私たち(またはルールで揉める、私たち vs 私たち)ではなく、私たち(とRuboCop) vs 問題ということにちゃんと向き合うことだな、と受け止めました。

懇親方面では、今年はオフィシャルパーティがなく各位が自由に懇親する形式でした。僕も友人たちとこじんまりと懇親をしていました。GMOペパボや、SmartHR、Fusic、pixiv、マネーフォワードの友人たちを中心に若手のコミュニケーションをうまくしたいというのを初日にお話していました。2度のオンライン開催となり、新卒の方や新卒2年目の方、この3年の間で転職してきた方など、RubyKaigiを知らない方々が増えており、かつてのコミュニティの繋がりがなくなっていました。 そこで、各社の若手を募りグループをつくり津駅の周辺に放つのはどうだろうか?という話があり各社で募ってみると総勢で25名ほどの若手が集まりました。 2日目の終わりにスポンサーブースに集合させてその場で5人1グループを作って津駅の方面に放ちました。この記事でも感想がありますが、この仕組みは結構よさそうな手応えがありました。来年もオフライン開催が見込まれるようだったら同じように若手同士でグループを組んで懇親をさせてみたいと思いました。

@hfm

技術部 @hfm です。印象的だったセッションはいくつかあるのですが、特に気になったのは @peterzhu2118 さんの Automatically Find Memory Leaks in Native Gems でした。このセッションで紹介された https://github.com/Shopify/ruby_memcheck は minitest や RSpec といい感じに統合することができて、導入の簡単さも含めてとても便利そうでした。また、こういったかゆいところに手が届くツールを含めて様々なソフトウェアを世に送り出している Shopify の勢いも感じました。

また RubyKaigi に参加するのは 2019 年福岡以来で、およそ 3 年ぶりに会う Rubyist たちもたくさんいました。特に 前職の GMO ペパボの皆さんや ANDPAD の id:shiba_yu36 さんなど、懐かしい面々と会えて良かったです。お互いの近況を話だすだけであっという間に時間が過ぎてしまいました。また夜は Helpfeel の人たちにお誘いしてもらって居酒屋に繰り出し、新しい交流ができてとても楽しかったです。

@s4ichi

  • 良かったセッション
    • Making *MaNy* threads on Ruby (発表資料)
      • MaNy の夢、これから Ruby がどういう方向でマルチコアに適合していくのかが計測ベースで紹介されていて夢が持てました。
      • Web サーバーは特にコアを使い切ることでパフォーマンスやコスト面に大きく寄与するので、社内で活用できるのが楽しみです。
    • Stories from developing YJIT
      • 処理系のコアの話が好きなので楽しみにしていたんですが、思ったよりもレイヤの低い話で付いていくのがやっとでした。
      • 要所要所で出てくるワードを掻い摘みつつ持ち帰って知的欲求が満たせたので良い発表でした。
    • Fast data processing with Ruby and Apache Arrow
      • ニッチな話だな、と思いきや、業務でよく使われているデータフォーマットの話の延長だったり、拡張して考えられるきっかけにもなったので興味が持てました。
      • 紹介されていたプロダクトを触るチャンスを伺っていきます。
  • 印象的な出来事 オフラインのカンファレンスっていいですよね。見て終わりではないし、熱量がオンラインと段違いでした。
  • 交流
    • 社内のメンバーとも、業界の人々とも、直接でないにせよ Ruby を支えるコミュニティの人とも交流できました。
    • 2年間、こうした規模のイベントも無かったので社外の方と関わる機会があって新鮮でした。世界は広い。

@terfno (Network Operations)

  • 良かったセッション Method-based JIT compilation by transpiling to Julia
    • 概要
      • Ruby でデータ処理したいけど、やっぱ早くなってほしい
      • Ruby はいつでも method の再定義ができて、その動的性(?)を守り続ける以上速度の限界がある
      • データ処理の文脈において、この動的性を守り続ける意味は薄い
      • そこで、Ruby から Ruby AST の次に静的型付けのされた中間表現に変換してその後 LLVM の中間表現、そしてようやく Native Code に…というルートをたどって動的性を捨てると早くなりそうと思ったが、「静的型付け中間表現にして…Native Code になる」が難しいので、これを Julia にしてもらう(すでにしていた)というモチベ
      • つまり、Ruby->Ruby AST->Julia-なんやかんや->Native Code とすることで、Ruby で書きつつも早いデータ処理を実現できるのでは…?という実験をしていた
      • 実際いくつかの例で早くなっていてすごかった
      • Ruby から Julia へのトランスパイルでは、言語仕様の差がある部分について、Julia で使える Ruby と同じ仕様の関数を呼べるものを作っていた
    • なんで好きか
      • トランスパイルそのものへの興味を再び持つきっかけになった
      • Ruby から Julia へのマッピングも実験に必要な範囲から手でマッピングしているらしく、そこに情熱と狂気を感じてよかった
      • はじめて社外の Ruby committer と喋った気がする。みんな優しい
  • 印象的な出来事
    • ネットワークの準備を手伝った
      • ケーブルの引いて回ったり、AP を運んだりした
      • スケールしなそうな手作業を、スケールするように工夫したりするの地味に楽しい
        • 雑なアイディアをとりあえず一緒にやってくれたおしょうゆさんや、KMC のお二人に感謝。
    • 初めての RubyKaigi に前日の準備から会場に居れたのはラッキーだった
      • 「初回参加だし必要なら抜けてセッション行ってね〜」って気を使ってもらって、日中というかセッション中はセッションを聞くことができた。
      • そんな至れり尽くせりなことある?ってぐらい楽しかった
      • ありがとうございます
    • それとは別に、熱量とか、どれくらいの人がどうやって連携しているとか、そういった感覚を素手で触れて嬉しかった(語彙力消えた)
  • 交流

@osyoyu (Helper Staff, Network Operations)

  • 良かったセッション Making *MaNy* threads on Ruby (発表資料)
    • ISUCONに参戦するたび、Goroutineの圧倒的なコア使い切り力をうらやましく思っていたのですが、Rubyでも近い性能を出せる可能性に胸が躍りました。Rubyの圧倒的に柔軟なデータ構造操作力とMaNyの力が組み合わさる世界を早く見たい。
  • 印象的なできごと
    • 今回は当日Helperスタッフ (https://twitter.com/rubykaigi/status/1554059852963807232) およびネットワークチームの一員としても参加していました。イーサネットケーブルや光ファイバーを抱えて広大な会場を駆け回ったり、舞台裏(物理)に入って活動したり、多くのスタッフでKaigiを作り上げていくシーンの一員になれて(たらいいな)、忘れられない思い出になりました。
    • 今までに参加したことがあるカンファレンスの中で最も知り合いが多かったこともあり、「交流の場」としての機能をしっかり堪能できました。クックパッドの同僚にコミュニティの人を紹介してもらう、ということもたくさん起きたのも良かったかも。エッジが多い知り合いがいると世界が広がりやすい。
  • Rubyに熱い気持ちをもっている人が世界にたくさんいることを肌で感じることができました。いい世界。
  • どこのお店に入ってもRubyistがいる街、という非日常空間も刺激的でした。お店でたまたま隣にいたグループと「RubyKaigi参加者ですよね」から話が弾んだりする、なんてこともありましたが、これも(東京ではなく)地方で開催されるRubyKaigiの良さなのかな、とも思ったりです。

@funwarioisii

  • 良かったセッション Method-based JIT compilation by transpiling to Julia
    • Ruby の動的なメソッド呼び出しのコストを抑制するために、Julia に変換して実行させるのとその仕組みを整えていたのがよかった
    • 一見この手の仕事?は大掛かりな作業をしていそうなのに、そういったコストの低い検証をして進めていたのが良かった
  • あそなすさんが企画してくれた若者交流会で別の会社のRubyistと話すことができてよかった

@SpicyCoffee66

  • 良かったセッション
    • error_highlight: user-friendly error diagnostics
      • 入った変更自体の便利さに加えて、その対応に必要だったポイントを知ることができてとてもおもしろかったです。エコシステムも含めると結構いろんなユースケースがあって、網羅するのが大変という現実の話がとてもよかった。
    • Create my own search engine.
      • 好きなものをつくるっていいなぁという気持ちになれるセッションでした。個人的にカードゲームが好きなのもあってとても楽しかった。
  • 印象的な出来事
    • 久々の物理カンファレンスはやっぱり “空気” がよかったです。昔やっていた勉強会の知り合いにばったり再会したりして、懐かしい気持ちなれた。
    • あと、スポンサーブースに各社の工夫が感じられてとても楽しかったです。ノベルティもいいやつがたくさんあってホクホクした。ブース起点で会話がたくさん生まれましたが、うちが出していたスクラムとかマイクロサービスな話は結構興味持ってもらえてたようでよかった。各社工夫とか苦労してる点が結構あることがわかって安心しました。
  • 初日の夜に他社のエンジニアと飲みながらマイクロサービスの話とかしたのが新鮮でした。もともとあんまりそういうのやらないんですけど、場のパワーってすごい。そういう場でちゃんと自分の意見として話せるようにもっと勉強が必要だなぁとも思いまして、いい刺激をもらいました。

@miquito

  • 良かったセッション
    • スポンサーブース担当なのでセッションへの参加なし
  • 印象的な出来事
    • オフラインカンファレンスの良さを再認識
      • あまりにも普通な感想なんだけど、オンラインカンファレンスでは味わえないであろう時間や空気が確かにあって、それがとてもよかった。物理カンファレンスでよかった!
    • Ruby Quiz が大好評
      • 英語が得意というわけでもないどちらかというと寡黙な方が海外の Rubyist と Ruby Quiz を楽しそうに一緒に解いている姿や、Twitter でたくさんの方が Ruby Quiz に時間を使って取り組んでいた様子
    • 三重ごはん
      • 用意していただいたお弁当が美味しかった!毎日、松阪牛が食べるなんてことは、この後の人生でなさそう
      • あと、ラーメン屋で食べた津ぎょうざが大きかった、美味しかった
  • 交流
    • クックパッドの仲間といっしょに旅行してる気分になって楽しかった
    • OB もたくさんいて、同窓会だった。たくさんの Rubyist たちがクックパッドのコードを書いてきたんだなと改めて思った
    • ブース出展企業の方ともたくさん話した。その後の iOSDC にも同じくでている方がいて、各社のやっていきを感じた
    • 町を歩いていて声をかけられた初めて話す海外の方と呑みに行く、みたいなあまり普段はない(しないであろう)体験も楽しめた

@uasi

  • 良かったセッション
    • Ruby meets WebAssembly
      • button.addEventListener 'click' do … end が書けるのは自然でよい
      • Ruby のソースファイルも含めて1バイナリに収めるために仮想ファイルシステムまで作るのがすごいね
    • Types teaches success, what will we do?
      • gem_rbs_collection には意外と気軽にコントリビュートしていいということが分かって良かった。最初から網羅的に書く必要はなく、(自分が)よく使う箇所に型をつけるだけでいいとのこと
  • 印象的な出来事 ANDPAD ブースの2進数足し算タイムアタックで matz が2位に食い込んでいた
  • 夜に飯屋を探していたら TableCheck の CTO とエンジニアに声を掛けられて一緒に呑んだ。2人とも英語メインだったから軽い世間話くらいになった。技術的な話も英語でできるようになりて〜

@ukstudio

  • 良かったセッション
    • RBS generation framework using Rack architecture
    • Let's collect type info during Ruby running and automaticall
      • 開発してるアプリケーションへのRBSの導入は自分の中でも興味のある話題なので、自動生成の取り組みやその手段についてとても興味深かった
    • The Better RuboCop World to enjoy Ruby
      • 自分の中でもRuboCopはなかなかしっくりこないことがあったけど、それをうまく言語化されていてとても良かった
      • 問題についての向き合い方についても色々と考えさせられるセッションだった
    • 言語処理系や低レイヤの話は大体「わからん〜〜〜」ってなるけど、これこそRubyKaigiという感じがしてどのセッションも良かった
  • 印象的なできごと
    • Promvizのダッシュボードに人がとても集まってきてくれて、そこから色々と話することができた。みんなマイクロサービスに興味があるんだなと思った
    • スポンサーブースでgRPCの導入について聞かれて、gRPCを採用してる企業として認識してくれてる人もいるんだなと思った。もっと情報発信していきたい
    • ランチのローストビーフ弁当がめちゃくちゃおいしかった
  • 交流
    • 夜に他社のエンジニアとご飯に行ったけど、お互いの技術スタックなどについて話ができて自分の知らない技術スタックの話も聞けてとてもよかった
    • 参加してないけど、弊社と他社との若者の集まりが開催されてたのがよかった
      • 最近は横のつながりというかコミュニティの参加機会が減ってきているとおもうので、きっかけができてたのはとても素晴しい

@9toon

  • 良かったセッション
    • error_highlight: user-friendly error diagnostics
      • いかに互換性を保ちながら新しい便利機能を導入するかという工夫が見て取れたのがとてもよかったです。また、「言語側で便利機能を用意しましたよ」に留まらず、Ruby on Rails 等にも改善を取り込んで Ruby のエコシステム全体に価値を波及させようとしているのもとても印象的でした
    • Towards Ruby 4 JIT
    • Making *MaNy* threads on Ruby
  • 印象的なできごと
    • 低レイヤーの話になると「全くわからん...」になってしまったのですが、その「分からん」にも前向きな気持ちで向き合えるのがオフラインイベントならではだったかもなと思いました。
    • オンラインだったらきっと途中で気持ちが切れてしまっただろうし、みんなで分からん分からん言いながら「こういうこと?」って話せたのがよかったです。
  • 元クックパッドな人たちとたくさんお話できてよかったです。各位それぞれの場所で活躍しているようで頼もしかったです。

ということで、クックパッドからの Kaigi 参加者(の一部)によるイベントレポートでした! 現場の熱気、伝わりましたでしょうか。 来年は松本でお会いしましょう!

Cookpad Code Puzzle for RubyKaigi 2022の解説(表ステージ)

技術部の遠藤(@mametter)です。RubyKaigiお疲れ様でした!

クックパッドはRubyKaigiで、Rubyを使ったパズルを出してました。この記事では、出題者が想定していた解き方を公開します。自力で遊びたい人は解いた後で読んでください。

Cookpad Code Puzzle for RubyKaigi 2022

どんなパズル?

あらかじめ定義された謎の関数の中身を当てるパズルです。適当な引数で呼び出してみて、結果を観察して、中身を想像します。あたりがついたら、同じ関数を定義してみて、テストをパスしたらクリア。

次の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`
def answer1(n)
  n
end

func1は引数に1足したものを返しているので、それをanswer1にそのまま実装するだけです。

def answer1(n)
  n + 1
end

これで実行ボタンを押したらクリア。

2問目

# Congrats! You've solved the first puzzle!
# Next, challenge func2!

p func2("Hello") # => ??? (press "Run Ruby" to see output)
p func2("world") # => ???

def answer2(str)
  str
end

とりあえず実行した結果。テストが失敗します。

"HELLO"
"WORLD"

--- testing answer2
answer2: Test failed

    func2("Bar") != answer2("Bar")

"Hello""HELLO"になっているということは、大文字に変換すればよさそうですね。「Ruby 大文字 変換」などと検索すればすぐに答えが見つかると思います。次が答え。

def answer2(str)
  str.upcase
end

3問目

# Next, challenge func3!

p func3(1)
p func3(2)
p func3(3)

def answer3(n)
end

とりあえず実行。

[0, 1]
[0, 1, 2]
[0, 1, 2, 3]

--- testing answer3
answer3: Test failed

    func3(376) != answer3(376)

[0, 1, ..., 引数] を返せばいいようです。いろいろなやり方がありますが、たとえば次が答え。

def answer3(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を足した値を返しているようです。ということで、解答はこちら。

def answer4
  yield + 42
end

5問目

p func5(0) #=> 1
p func5(1) #=> 2
p func5(2) #=> 3

1を足すだけに見えるので、まずは試してみます。

def answer5(n)
  n + 1
end

実行すると、テストが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引数」を返していると想像できます。ということで答えはこちら。

def answer5(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)と気づきます。そこで次を試します。

def answer6(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)}"
  end
end

すると、いろいろ失敗例が見つかります。

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といいます)。よって次が答え。

def answer6(n)
  n = n.digits.sum
  if n < 10
    n
  else
    answer6(n)
  end
end

7問目

p func7(0) #=> 1
p func7(1) #=> 2
p func7(2) #=> 3

また1を足すだけに見えます。試してみましょう。

def answer7(n)
  n + 1
end

しかしこれはよくわからない例外でテスト失敗します。

ヒントに「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のカラム番号っぽい)。

ということで答えはこちら。

def answer7(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の数だとわかります。わかってください。この問題はもうちょっとヒントあったほうがよかったかもですね。とにかく答えはこちら。

def answer8(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オブジェクトが定義できます。

class Spy
  def method_missing(name, *args)
    puts "#{ name } is called with #{ args }"
  end
end

このインスタンスをfunc9に渡してみましょう。

func9(Spy.new)
#=> gsub is called with ["u-g0t-me", "yikes"]

もう何が行われているかわかりましたね。これが答えです。

def answer9(s)
  s.gsub("u-g0t-me", "yikes")
end

10問目

1回実行すると、次のようになります。

p func10  #=> 1
p func10  #=> 2
p func10  #=> 3

この関数は呼ぶたびに違う値を返しているので、状態を保存していることがわかります。グローバル変数で模倣してみましょう。

$counter = 3
def answer10
  $counter += 1
end

$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 = 0
def answer10(reset = false)
  if reset
    $counter = 0
  else
    $counter += 1
  end
end

func10(true)を呼んで状態をリセットするのがコツです。

別解として、func10の状態をそのまま返す手があります。p global_variablesを使うと、$__func10_counterといういかにも怪しいグローバル変数が発見できます。これをそのまま返してもテストはパスします。

def answer10(*)
  $__func10_counter
end

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以降に気づいてなかった人はぜひ今からでも挑戦してみてください。