Quantcast
Channel: クックパッド開発者ブログ
Viewing all 802 articles
Browse latest View live

【RubyKaigi発表予告】error_highlight: user-friendly error diagnostics

$
0
0

技術部の遠藤です。2日連続の投稿です。

今年のRubyKaigi 2022ではTRICKの発表をしますが、もうひとつ真面目な発表もします。Ruby 3.1の目玉機能であったerror_highlightについてです。

この発表内容について、あらすじを紹介したいと思います。

Ruby 3.1のerror_highlightとは

Ruby 3.1でNoMethodErrorが発生すると、次のようなエラーが表示されます。

$ ruby test.rb
test.rb:1:in `<main>': undefined method `time' for 42:Integer (NoMethodError)

42.time { print "Hello" }
  ^^^^^
Did you mean?  times

この42.time { print "Hello" }とその下線を出しているのがerror_highlightという機能です。

当日は実装をかんたんに発表しますが、この内容はすでに記事を書いているので、興味があれば読んでください。

techlife.cookpad.com

Ruby 3.2で何が変わるか

ざっくり言えば2つです。

  • ArgumentErrorやTypeErrorでも下線が出るようになる
$ ruby test.rb
test.rb:1:in `+': nil can't be coerced into Integer (TypeError)

1 + nil
    ^^^
  • Railsのエラーページでも下線が出るようになる

Railsのエラー画面のerror_highlight

これだけなんですが、これらを実現するためにRubyインタプリタのエラー処理まわりをいろいろと整理する必要がありました。

何がむずかしかったか

一言で言えば、「エラーメッセージの変更は非互換である」ということでした。

Ruby 3.1のerror_messageはException#messageをオーバーロードしてerror_highlightのメッセージを出すので、要するにエラーメッセージを書き換えていました。しかしエラーメッセージはターミナルなどに表示されるだけではなく、テストで参照されたり、ログファイルに書かれたりもするので、むやみに変えると非互換が問題になるのでした。

Ruby 3.1では比較的影響の少なそうなNameError/NoMethodErrorにとどめたのですが、それでもいくつか困りごとが報告されていました。いきなりArgumentErrorやTypeErrorに対象を広げると更に大きな問題になるので、非互換問題を先に解決する必要がありました。

Ruby 3.2の新機能、Exception#detailed_message

どうすればよいか。本来のException#messageを書き換えることなく、ターミナルなどに表示するメッセージだけ書き換えればよいです。

そこで、Exception#detailed_messageという新規メソッドをRuby 3.2に導入しました。

#messageは従来どおりのシンプルなメッセージを返します。

puts $!.message
#=> undefined method `time' for 1:Integer

#detailed_messageはerror_highlightやdid_you_meanの情報が付加されたメッセージを返します。#detailed_messageは内部的に#messageを呼んでます。

puts $!.detailed_message
#=> undefined method `time' for 1:Integer (NoMethodError)
#
#     1.time
#      ^^^^^
#   Did you mean?  times

テストやログでは従来どおり#messageを使い、ユーザにエラーを表示したいときは#detailed_messageを使う、という風に使い分けることになります。Rubyインタプリタのエラー表示も#detailed_messageを使うようにしました。

これで、#messageの内容を変えずにエラー表示を拡充出来るようになったので、非互換の問題なくArgumentErrorやTypeErrorでも下線を表示できるようになりました。

エコシステムへの対応

この#detailed_messageの変更は、Rubyのエラーを表示するフレームワークにちょっと対応をしてもらう必要があります。たとえばRackの開発モードのエラー画面では、did_you_meanやerror_highlightの情報も出してほしいでしょう。そのためには、#messageではなく#detailed_messageを使うようにする必要があります。

Rackについてはプルリクエストを送ってマージしてもらいました。シンプルなので参考になると思います。

github.com

Railsについては、単純に#detailed_messageを使うのではなく、error_highlightのAPIを直接使って専用の表示をするようにしました。

github.com

SentryとDataDogは、Exception#detailed_messageを提案したチケットの中で、#detailed_messageを使うようにすることを約束してくれました。彼らの同意が得られたから#detailed_messageが導入できたという背景もあります。

しかし、エラー表示をしたいフレームワークは他にもたくさんあるので、地道に対応していく必要があるでしょう

まとめ

Ruby 3.2のerror_highlightでは、

  • ArgumentErrorやTypeErrorでも下線が出るようになります。
  • Railsのエラーページでも下線が出るようにもなります。

この2点を実現するために、Rubyのエラー処理まわりを整理しました。

という話を、RubyKaigiでします *1。3日目の一番最初のトークです。興味あればぜひ。

また、お使いのフレームワークに#detailed_messageを対応させる必要があるのかもしれません。Ruby 3.2にしたときに「did_you_meanやerror_highlightが表示されてないな」と思ったら、この内容を思い出してフレームワークにプルリクを作るなどしてもらえるとうれしいです。

*1:記事では省略しましたが、インタプリタが余計なことをするせいで下線位置がずれてしまう!というバグ修正の話なども。


【RubyKaigi 2022】津駅から行ける三重グルメ! 予約必須の美味い店はここにある

$
0
0

こんにちはCTO室の緑川です。 今月9月8日〜10日にかけて開催されるRubyKaigi 2022ですが、クックパッドはRuby Committers'& Wi-Fiスポンサーとして協賛します。クックパッドからはRubyコミッターとして活動する2名のエンジニアが登壇する予定です。また、オフライン会場でブースを出展するほか、パネルディスカッション「Ruby committers vs the World」を主催します。

techlife.cookpad.com

techlife.cookpad.com

さて、今回のRubyKaigi 2022の会場は三重県津市です。初めて訪れる方もいらっしゃるかと思いますが、そうなると必要になるのは現地のグルメ情報ですよね? 三重県は名物が多く、松阪肉や伊勢海老などの豪華な食材から津餃子やカレー焼きなどの手軽に楽しめるグルメまで多く揃っています。RubyKaigi 2022の開催期間は津駅周辺に宿泊される方が多いと思いますので、津駅を中心にオススメの飲食店をまとめてみました。是非お食事の参考にしてください。

目次

三重と言えば、世界的なブランドである松阪肉が有名です。松阪肉は高級食材ということもあり、お住まいの地域によってはなかなか目にすることができません。津市では松阪肉を食べられるお店が多いので、この機会に是非一度立ち寄ってみてください。

まる良 津駅から徒歩3分

駅から近く、手頃なお値段で松阪肉が食べられるのがこの「まる良」です。松阪肉のロースやカルビなどを食べられるほか、松阪肉の黄金盛りといったメニューもあります。普段から予約が取りにくいお店とのことですが、RubyKaigi 2022中はさらに混み合うことが予測されるので、焼肉をされる予定の方はぜひご予約ください。

tabelog.com

松重 津駅から徒歩2分

松阪肉でしゃぶしゃぶやすき焼きを楽しめる「松重(まつじゅう)」です。松阪肉を専門としている卸業者の朝日屋が直営するお店で、上質の松阪肉を食べることができます。完全個室の部屋もあるので、落ち着いた雰囲気で食事を楽しめます。

tabelog.com

松阪肉の定番中の定番

松阪肉の本場は津市の隣の松阪市です。かなり有名なお店もあるので、もし時間的に余裕がありましたら松阪市にも足を伸ばしてみてください。

和田金 松阪駅から徒歩10分

定番中の定番ですが、明治11年創業の老舗である「和田金」です。こちらのお店では、目の前でお肉を焼いてお皿に入れてくれます。一番良いタイミングで松阪肉のすき焼きを楽しむことができます。

tabelog.com

牛銀本店 松阪駅から徒歩13分

同じく松阪肉の老舗「牛銀本店」です。こちらのお店では一般には流通しない品質の松阪肉を味わうことができます。また、松阪市には松坂城跡があり、牛銀本店は武家屋敷の長屋内にあります。観光と合わせてオススメです。

tabelog.com

一升びん 宮町店 松阪駅から徒歩10分

回転焼肉の先駆けとなった「一升びん 宮町店」です。こちらは回転寿司のように松阪肉がレーン上を流れており、好きなお肉をとって、自分で焼くスタイルとなっています。他のお店よりコスパ良く松阪肉を食べることができます。

tabelog.com

うなぎ

津市のうなぎといえば、市民一人当たりの年間消費金額が全国1位になったことがある市民に親しまれている食材です。そのため、市中には多くのうなぎ店があります。どのお店も他の地域と比べ、値段が安く、ボリュームがあって美味しく食べることができます。

うなぎ料理 はし家 津駅から徒歩26分

津市の中でもオススメなのが昭和28年から続く名店の「はし家」です。はし家では肉厚でボリュームがあるうなぎを手頃な値段で食べることができます。津駅から2kmほどあり、歩いていくには遠いと思いますので、タクシーを利用するのが良さそうです。なお、席の予約はできないのでご注意ください!

tabelog.com

大観亭支店 栄町本店 津駅から徒歩4分

津駅からも近く、オススメのうなぎ屋さんがこちらの「大観亭支店 栄町本店」です。こちらもボリュームがあるうなぎ丼を手軽な値段で食べることができます。もちろん炭火で焼いており香ばしい匂いがしており、甘く濃厚なタレが絡んでいることから、ご飯の量の多さを感じさせない一品となっています。

tabelog.com

新玉亭 津駅から徒歩22分

こちらもボリュームにインパクトがある「新玉亭」です。量に圧倒されがちですが、うなぎは炭で焼かれ香ばしく、甘めのタレがご飯に絡み、何杯でも食べれる美味しさです。ただ初回だと中盛りまでしか注文ができないので、もし大盛りに挑戦したい場合にはRubyKaigi 2022中に何度か足を運ぶと良いかもしれません。

tabelog.com

伊勢海老

いせもん本店 津駅から徒歩2分

三重県は伊勢海老の漁獲量が全国1位で、伊勢海老は三重県を代表する高級食材として知られています。津駅から近く、本格的な伊勢海老を楽しめるのが「いせもん本店」です。地産地消をコンセプトとした和食屋さんなので、伊勢海老だけでなく、松阪肉、海鮮、伊勢うどんなどあらゆる山海の幸を楽しめるお店です。

tabelog.com

ラーメン・津餃子

麦(ばく) 一等兵 津駅前店 津駅から徒歩2分

津市のご当地グルメとして有名な「津餃子」を食べることができるラーメン屋が「麦 一等兵 津駅前店」です。津餃子とは直径15センチの皮を使った大きな揚げぎょうざで、1985年ごろに給食用に作られたご当地メニューです。ラーメンは鶏がらスープを使った醤油らーめんや塩ラーメン、豚骨ラーメンと揃っています。

tabelog.com

まるかん 三重県総合文化センターから徒歩2分

RubyKaigi 2022の会場となる総合文化センターから一番近くのラーメン屋が「まるかん」です。YouTubeチャンネルの「ナイツ塙の自由時間内」で『【営業メシ】三重県津市の超贅沢ラーメン』と紹介されたお店です。醤油ラーメンが有名で、特製らーめん白や麺が見えないほど多く盛られたチャーシュー麺が食べ応えがありおすすめです。

tabelog.com

居酒屋

空麦 津駅から徒歩3分

創作料理とうどんを楽しめるのが、こちらのうどん居酒屋の「空麦」です。〆のうどんが有名ですが、もちろん居酒屋なのでお酒とおつまみがメイン料理で、日本酒は三重県内の地酒だけじゃなく全国の銘酒が揃っています。こちらのお店では香川県産のうどん専用小麦「さぬきの夢2009」を使用した讃岐うどんが提供されています。

tabelog.com

鳥さわ 津駅から徒歩2分

こちらも「松重」同様に松阪肉専門の卸である朝日屋さんが直営するお店です。とり肉だけでなく、牛串やかわり串を食べることができます。飲み放題付きのコースもありますので、打ち上げに活用できるお店です。〆の鍋は前日までに予約が必要なので、予約をして食べに行くのが良いと思います。

tabelog.com

炙り屋 kamakura 津駅から徒歩4分

お店の見た目にインパクトがある「炙り屋 kamakura」は、伊勢赤どりを使った鶏料理を中心とした居酒屋です。鶏を使ったお造りや串物の他、三重県の名物である松阪肉や伊勢うどんを食べることができます。また、梅酒は日本中から集めており数が豊富にあります。

tabelog.com

一福 津駅から徒歩23分

津駅でどうしてもお店に入れなかった際には、少し離れた大門という飲食店がいくつかある商店街に出向くのがオススメです。大門の中でもオススメなのが居酒屋の「一福」です。こちらは串や鳥料理が有名で、中でも手羽先が絶品です。 一品料理の種類も豊富なのでどなたでも食事が楽しめるお店です。

tabelog.com

海鮮系居酒屋

居酒屋 徳ちゃん 津駅から徒歩2分

「居酒屋 徳ちゃん」は刺身や貝の種類が豊富な海鮮居酒屋です。もちろん揚げ物やお浸しもあり、旬な料理を食べることができます。日本酒や焼酎も数多く揃っており、日本の飲み比べセットもありますので多くのお酒を楽しめます。

tabelog.com

旬彩酒房 あうん 津駅から徒歩2分

「旬彩酒房 あうん」は旬の素材を活かした和食居酒屋のお店です。揚げ物や海鮮などの創作料理が食べられ、その日ごとのメニューも数多くあります。日本酒も三重の地酒が揃っており、海鮮では特にオススメの居酒屋さんです。

tabelog.com

丸五水産 津駅から徒歩1分

「丸五水産」は津駅の上にある海鮮のビアガーデンです。時間制ですが真牡蠣やはまぐりを食べることができます。メインは海鮮ですが、おつまみやバーベキュー用の食材もありますので、海鮮が苦手な人でも楽しめます。ビアガーデンなので席数も多く、大人数で行くのに重宝するお店です。

tabelog.com

その他

東洋軒 本店 津駅から徒歩25分

津駅からやや離れていますが明治22年に創業された名店の「東洋軒」もオススメです。西洋料理店ということもあり、さまざまなコース料理が選べますが、中でも特製ブラックカレーライスが有名で、コースによっては追加料金でブラックカレーライスを食べることができます。

tabelog.com

津 みやび 津駅から徒歩1分

津駅から離れる際にお弁当として購入していただきたいのが、津駅の前のホテルグリーンパーク津の2Fにある「津 みやび」のお弁当です。上質な松阪肉を使っており、松阪牛ステーキ弁当は見た目もインパクトがあります。注文から提供まで時間がかかりますので、事前に予約しておくとよさそうです。

tabelog.com

カレー焼店さかえや 津駅から徒歩5分

津市にあるご当地おやつとして有名なのが「さかえやのカレー焼き」です。カレー焼きはカリッと焼かれた生地にカレーが詰まっているおやつです。カレーは野菜がたっぷり入っており、生地との相性も抜群です。カレー味のほか、クリーム、あんこと合計で3種類の味があります。

tabelog.com

蜂蜜まん 津駅から徒歩1分

「蜂蜜まん」も津市のご当地おやつとして有名です。サイズはやや小振りですが、1個あたりの値段が安く食べられるおやつです。生地には名前の通り蜂蜜が練り込まれており、中はこしあんとなっています。津駅東口構内でも購入することができますので、お土産としても良さそうです。

tabelog.com

まとめ

三重県は名物が多くありますが、そのほとんどが津駅の周辺で味わうことができます!RubyKaigi 2022にご参加の方はぜひお立ち寄りください。

最後に宣伝にはなりますが、クックパッドで働きたいエンジニアの方を募集しております。ぜひRubyKaigi 2022のブースで声をかけていただくか、クックパッドのウェブサイトからご連絡ください!

info.cookpad.com

iOSDC Japan 2022に社員5名が登壇 &スポンサー企画のご案内

$
0
0

こんにちは!クックパッドでモバイルアプリ開発エンジニアをしているあつや (@n_atmark) です。スプラトゥーン3の発売日がいよいよ今月になり、非常に待ち遠しい日々を過ごしています。

さて、iOSと周辺技術を題材としたカンファレンス、iOSDC Japan 2022 が9/10(土)〜9/12(月)に開催されますね!

クックパッドは、プラチナスポンサーをさせていただいており、オフライン会場でブース出展いたします。 オフライン会場でクックパッド社員をお見かけの際には、お声がけいただけますと嬉しいです。

オフライン会場参加予定社員一覧

@mirakui@kalupas226@yujif_@aomathwift@tk108gabalian@ren_lit_335@enomotok_@n_atmark

トークのご紹介

今回クックパッドからは5名のトークを採択いただき、登壇することになりました! ここで紹介させてください。

Day 0

9/10(土) 16:40〜

Track B(20分)

  • 登壇者: あつや / @n_atmark
  • タイトル: 施策基盤としてのディープリンク 〜なめらかにアプリが開く体験のために〜

fortee.jp

9/10(土) 17:15〜

Track D(40分)

  • 登壇者: アイカワ / @kalupas226
  • タイトル: SwiftUI Navigation のすべて

fortee.jp

Day 1

9/11(日) 13:00〜

Track B(20分)

  • 登壇者: ainame / @ainame
  • タイトル: Swift 5.7で変わる正規表現を試してみよう

fortee.jp

Day 2

9/12(月) 10:50〜

Track B(20分)

  • 登壇者: あおい / @aomathwift
  • タイトル: 即時通知を導入する際に考えるべきこと

fortee.jp

9/12(月) 13:55〜

Track B(20分)

  • 登壇者: Yuji Fujisaka / @yujif_
  • タイトル: モバイルアプリの行動ログの「仕込み」を快適にする

fortee.jp

ノベルティの紹介

iOSDCのノベルティBOXにクックパッドのノベルティとしてLGTMうちわを封入させていただきました!

うちわに「LGTM」の文字を印刷していて、セッションを応援したり、セッションの登壇者に感謝の気持ちを伝えるのに使ってもらえたら嬉しいです! もちろん、まだしばらく残暑が続きそうなのでうちわをパタパタさせて涼むのにもご活用ください。

そして、うちわをデコレーションするためのシールも封入いたしました。ぜひうちわをデコって、自分だけのLGTMうちわを作ってみてください!

綺麗にデコレーションできたら #クックパッドのLGTMうちわでツイートして他の参加者に自慢しちゃいましょう!

ブースの紹介

今年のクックパッドブースでは、3つの企画を行います。

iOSDC Japan 2019 の様子

クックパッドでのアプリ開発の様子をデモ紹介

クックパッドブースにて「クックパッドでのアプリ開発の様子」をデモ形式で紹介します!

  • 9/11(日) 12:30〜12:45
    • サンドボックスビルドを行いミニアプリ上で画面表示できるようにするまでの流れを紹介
  • 9/11(日) 13:50〜14:05
    • ログの定義から埋め込み用コードの自動生成、ログ実装までの流れを紹介

クックパッドでのサービス開発を体験してみたい方はぜひお越しください!

CTO座談会

9/11(日)、9/12(月)はクックパッドCTOの @mirakuiもiOSDCに参加します!

そこで、CTOと当日参加するクックパッドのエンジニアがパネルディスカッション形式で座談会を行います!

  • 開催日: 9/12 (月) 12:40〜13:00
  • 場所: クックパッドブース (参加人数によってはフリースペースに場所を移す可能性があります)

クックパッドで働くエンジニアの仕事内容や、エンジニア組織についてなど話しますので、お気軽に参加してください!

クックパッドのコード全部見せます大質問会 2022

9/12(月) 14:20〜14:50 に フリースペースにて「iOS版クックパッドアプリのコード全部見せます大質問会 2022」を開催します!

2019で大盛況だったiOS版クックパッドアプリのコード全部見せます会をもう一度実施します。

2019年に実施したiOS版クックパッドアプリのコード全部見せます大質問会の様子

実際のプロダクトコードを見せながら、クックパッドのiOS開発について紹介します。社員が交代で以下のようなトピックを予定です

  • 2019年から新しく増えた機能の実装紹介
  • Sandboxアプリのコード
  • ログ自動生成周りの実装
  • 社内ドキュメント
  • … etc

その他、当日会場でも見てみたいコードを参加者の方から募る予定です。 クックパッドのアプリ開発に興味がある方、2019年からの差分について知りたい方など、ぜひご参加いただければと思います。

After Party iOSDC Japan 2022 を開催します!

iOSDC Japan 2022の開催後「After Party iOSDC Japan 2022」というイベントをクックパッド主催で9/28(水) 19:00からオンラインにて開催します!

cookpad.connpass.com

※このイベントは「iOSDC Japan 2022実行委員会」が運営するものではありません。

4名のクックパッドエンジニアがカンファレンスでは惜しくも採択されなかったトーク、発表には入りきらなかった話などをお話しします!

クックパッドのiOS開発ってどうなっているんだろう?どんな社員が働いているんだろう?という疑問のある方、クックパッドに興味のある方、なんかイベントがあるならとりあえず行くぜ!という方がいましたら、ぜひこちらのイベントにもご参加ください。お待ちしております。

また、iOSDC Japan 2022本編のクックパッドブースでAfter Party iOSDC Japan 2022の参加申し込みページを見せてくださった方に先着で「オリジナルキッチンクロス」をプレゼントをお渡しします!

クックパッドのサービスで使われているカスタムシンボルをあしらったキッチンクロスになっています。とても可愛いのでぜひ貰いに来てください!

おわりに

今年のiOSDCはオフライン+オンラインのハイブリット開催ということもあり、3年ぶりのオフライン会場にわくわくしています。ブース企画も用意しておりますので、クックパッドに少しでもご興味をお持ちの方は、お気軽にブースまでお越しください!みなさまにお会いできることを楽しみにしております。

おまけ: iOSDCチャレンジ!

import RegexBuilder

letiosdc="2022/09/10(Sat)-9/12(Mon) iOSDC Japan 2022"letresult= iosdc
    .replacing(Regex {
        Regex {
            OneOrMore {
                ChoiceOf {
                    .whitespace
                    ("A"..."Z")
                    ("0"..."9")
                    "/""-""("")"
                }
            }
        }
    }, with:"")
    .replacing(/a[^a]+a/, with:"#")

print("iOSDC Token is", result)

AWS GameDay を社内開催しました

$
0
0

技術部 SRE グループの奥村 (@hfm) です。クックパッドでは Hackarade というエンジニアの技術力の底上げを目的とした社内ハッカソンを不定期に開催しています。テーマは毎回異なり、2022 年 8 月 3 日に開催された Hackarade では AWS GameDay を題材としました。本記事ではそのイベントをレポートします。

AWS GameDay と開催経緯

AWS GameDay は ISUCONのようにスコア計測があるトレーニングプログラムで、テーマに沿って自分で考えながらシステムを構築しつつ、時に障害に対処したり、スキルアップを図る実践的な内容になっています。AWS re:Invent などで開催されるものに任意参加するのが一般的ですが、今回は社内向けに開催してもらいました。GameDay という名の通り、AWS に関するクイズだけにとどまらないゲーム要素もあり、全くの AWS 初心者でもルールの範囲内である程度戦いようがあるように設計されています。

クックパッドは AWS をフル活用しており、SRE グループは社内でもとりわけ使い込んでいます。一方他の部署では、Solution Architect の Associate や Professional の資格を持っているエンジニアも何人かいますし、AWS に馴染みのあるエンジニアもいる一方で、例えば「なんとなく S3 などを使ってはいるけど、マニュアルやドキュメントをちゃんと読んで機能を把握した上で使いこなせているわけではない」といった人たちもいます。仕事の関係上、AWS に触れる機会が少ないエンジニアの方々にも AWS への理解を深めてもらうことで、クラウドを使い倒すスキルを向上に貢献したいという思いが開催の一番大きな理由でした。

AWS GameDay はチーム戦となっており、今回は合計 65 名 15 チームで競い合いました。チーム編成は参加者に任せて、適当に用意したリポジトリの Issue でそれぞれメンバーを募ってもらいました。チームごとのスキル差があまりに激しいようであれば手を入れようかと思っていましたが、特に何もせずともそれなりにバラけていました。

チームメンバーを募集している様子

AWS GameDay の仕様的には個人参加も可能みたいでしたが、チームで相談しつつ取り組んでほしかったこともあって 3〜4 人のチームを組んでもらうことにしました。

また、AWS GameDay 自体が初めての人も多かったので、事前にもらっていた AWS GameDay のシナリオ(簡単なゲームルールだったり、求められる AWS サービスの知識が書かれたスライド)の予習会を開きました。シナリオ中に登場した AWS サービスのドキュメントを読みにいったり、またクックパッドには AWS サービスを自由に試せる本番用は別の開発用 AWS アカウントがあるのですが、そこでサービスを試しに使ってみたりしたエンジニアもいました。

当日の形式や様子

スケジュールは 13 時から 18 時までの 5 時間で、シナリオについての説明が行われたあと競技が始まり、競技後は表彰を行うといった流れになります。今回は Unicorn Polo League (UPL)*1というシナリオでした。あまり中身に触れることは出来ないのですが、シナリオごとに異なる世界観があり、UPL ではユニコーンレンタル市場という架空の市場におけるスタートアップ企業として、経営課題を解決するために競技に挑むという形式になっています。競技では英語で出題される複数のクエストを解きつつ、ユニコーンを他チームと戦わせるといったゲーム的な要素もありました。

SRE グループに対する特別ルール

AWS GameDay を社内開催するにあたって SRE グループのメンバーをどうするかに小さな課題がありました。というのも、SRE は通常業務でもっとも AWS に触れているグループであり、他のチームに混ざるとチームごとの戦力差が出すぎてしまわないかを懸念していました。

そこで、SRE は SRE だけでチームを組むことにし、さらに特別ルールとしてイベント中にいつでも他チームからの質問を受け付けるという、環境デバフというかバランス調整を行ってみました。他と比べてチーム人数がやや少ないこともあり (2 人 1 組、3 人 1 組の計 2 チーム)、人数差はそれなりにいいバランス調整になりましたが、結果的に他チームから一切質問が来なかったのでもう少し工夫の余地はあったかもしれません。

バーチャル会場

また、イベントを盛り上げるための工夫として Gatherを使ったバーチャル会場を用意しました。オフィスに集まって参加しているチームもいましたが、コロナ禍で全員がオフィスに集まることも難しく、しかし多くのエンジニアが参加するイベントでワイワイ感を少しでも演出するために Gather を試してみました。Gather を利用するのが初めての人も多数いましたが、マップを移動出来れば十分なのでそこまでバーチャル会場特有の困難は無かったように思います。

また、AWS の人にも Gather スペースに常駐してもらい、AWS GameDay で分からないことがあったときに質問しにいくことが出来るようにブースを設けました。実際競技中に何度か質問に向かっている人たちもいるようでした。

こうした社内イベントに Gather を利用するのは初めてでしたが、最低限の空間(チームごとに集まれるエリアや質問ブース、全体アナウンス用の集合場所など)を用意しつつ、あとは参加者全員に Builder 権限というマップの編集権限を渡してみたところ、みんなが良い感じにマップを改変してくれました。

AWS GameDay を開催した Gather バーチャル会場(マップは既に改変されている)

競技効率を考えればチームごとに Slack チャンネルと Zoom を用意する方が集中しやすくていいと思うのですが、画面上のキャラクターによって他者の存在を感じやすい点はお祭りイベントと相性がいいように思います。

終わりに

ゲーム後の社内アンケートでは、歯ごたえのあるゲームルールのおかげでチームごとの戦略の違いによる差が出て面白かった点や、今まで触れたことのない AWS のサービスや機能に触れる機会が生まれた点、普段業務で AWS を触っている人にとっても既視感のあるシチュエーションを模した問題があって楽しめた点などが好評でした。

冒頭にも書いたとおり、今回の AWS GameDay は Hackarade という社内の枠組みの中で開催しました。Hackarade はエンジニアの技術力を底上げするための取り組みで、チームで競い合う以外にも様々なやり方があり、たとえばクックパッドのデータを使ったワークショップみたいな企画も今後やっていきたいと考えています。また、過去には段階グランプリという名前で、より ISUCON に近い形式の社内イベントも開催してきましたが、この手の競技性のあるゲームはまたいずれやりたいと思っています。

AWS GameDay 終了後の Gather の様子

OpenAPI SpecificationsからiOSプロジェクトのネットワーク層を自動生成する

$
0
0

こんにちは。iOSエンジニアの河邉です。 今回は海外出向研修プログラムで出向した海外版Cookpadを開発するブリストルオフィスで取り組んだ、OpenAPI SpecificationsからiOSプロジェクトのネットワーク層を自動生成する話をしたいと思います。

海外出向研修プログラムについての体験談はこちらをご覧ください。

techlife.cookpad.com

本題に入る前にまず海外版Cookpadについて簡単に紹介します。 海外版Cookpadは、日本のクックパッドレシピサービスとはコードベースも開発チームも会社も分かれていて、イギリスのブリストルという都市にあるオフィスをベースとするチームによって開発されています。 現在は世界74の国や地域で、32言語をサポートし、各地域のコミュニティチームと連携しながら世界中の毎日の料理を楽しみにするためにレシピサービスを展開しています。

海外版CookpadとOpenAPI

海外版Cookpadでは2019年頃からOpenAPIを用いたスキーマ駆動開発の導入を開始しました。 Androidプロジェクトではすでに2021年の段階でOpenAPIのスキーマからDTO(Data Transfer Object)と呼んでいるネットワーク層に用いるオブジェクトや、APIエンドポイントの定義の自動生成をしていました。

英語の記事ではあるのですが、iOSに先行して実施したAndroidプロジェクトへの導入について、以下の記事で紹介しています。 Let OpenAPI generate your Android network layer by leveraging Retrofit, Moshi, and Coroutines

しかし、iOSプロジェクトは2022年になってもOpenAPI SpecificationsからDTOやエンドポイントの定義を生成することができていませんでした。

Androidと比較してiOSのプロジェクトが遅れてしまっていたひとつの要因は、もちろんマンパワー的な問題もありますが、Androidプロジェクトには導入以前からすでに手書きのDTOが存在したためOpenAPIから生成するものと置き換えることができましたが、iOSプロジェクトにはDTOが存在しなかったため単に置き換えることができず、新しくDTO層を追加する必要があったからです。

DTO層が存在しなかったので、1つのオブジェクトをAPIレスポンスのデコードからUIロジックまで使っていて責務の切り分けができない・肥大化する・変更しづらいといった問題と、自動生成していないことによって単調なコードをAPIの変更のたびに手書きする必要がある・記述ミスのリスクが排除できないなどの問題を認識していました。

OpenAPIからiOSプロジェクトのネットワーク層を自動生成する

DTOの無かったiOSプロジェクトにOpenAPIから生成したDTOを導入した実際の手順について紹介します。

DTOを生成する

まず初めに取り組んだのはDTOの自動生成でした。自動生成に用いたライブラリはCreateAPI/CreateAPIという非常に新しいライブラリです。

// Generated by CreateAPI
public final class UserDTO: Codable, DTO {
    public let type: `Type`
    public let id: Int
    public let name: String
    public let avatarURL: URL
}

海外版Cookpadアプリはバイナリサイズにシビアな地域でも多くのユーザーに使われているので、アプリのバイナリサイズにはより細心の注意を払いました。 海外版Cookpadは既に400以上のComponentsが定義されていて、決して小さいアプリとは言えず、全てのComponentsをDTOとして一気に生成してしまうとバイナリサイズを不必要に大きくしてしまう懸念がありました。

今回のCreateAPIを使ったDTOの自動生成は当初は実験的に開始したため、CreateAPIの includeというパラメータを用い、アプリのバイナリサイズを肥大化させないために少しずつDTOの生成を進めていきました。

また、CreateAPIにあるisGeneratingStructsというパラメータを用いて、DTOをStructではなくClassとして生成しました。我々のケースではStructで生成するのと比較して1.7MB縮小することができました。

Mapperを記述する

enum UserMapper {
    static func toEntity(from dto: UserDTO) -> User {
        User(
            id: dto.id,
            name: dto.name,
            avatarURL: dto.avatarURL
        )
    }
}

CreateAPIによって生成したDTOを既存のオブジェクトにマッピングする関数を記述しました。 これによって既存のオブジェクトには変更を加えずに、DTOとMapperを用いたネットワーク層を新しく追加することができます。

DTOとMapperのテスト

DTOとMapperが既存のオブジェクトと同じようにAPIのレスポンスをシリアライズしているかを確認するために、実際のレスポンスのJSONと同じ形式のダミーデータを用意し、DTOとMapperの組み合わせと既存のシリアライズ方法で全く同じ結果を得られるかを確かめるテストを用意しました。

final class UserMapperTest: XCTestCase {
    func test_user() throws {
        // Given User response json
        let response: Data = try Bundle.module.data(forResource: "User") // JSONファイルを読み込む関数を用意
    
        // when parse the response to User with legacy parser
        let legacyParser = DefaultModelParser<User>()
        let old = legacyParser.parse(response).value // 従来の方法でシリアライズ

        // and when parse and map the response to User with DTO parser
        let parser = DTOResponseParser<UserDTO>()
        let new = UserMapper.toEntity(from: parser.parser(response).get().result) // 新しい方法でシリアライズ

        // then
        XCTAssertEqual(new, old)
    }
}

この過程で既存の実装にAPIの定義とは異なる記述が見つかりましたが、ここではそれらのリファクタリングは後回しにして、とにかく現状の実装と一致する結果が得られるようにMapperやテストを記述しました。 タスクのスコープをネットワーク層の導入以外に広げないようにしたことが、導入をやり切る上でとても重要だったと思います。

これらのテストは移行期間中のみの一時的なもので移行が完了したら一緒に削除していきます。

DTOとエンドポイントはモジュールに分割して閉じ込める

DTOとエンドポイントはAPIKitという新しくつくったモジュール内に生成しています。 ApplicationモジュールはAppBaseモジュールを通じてのみAPIKitを参照するので、ApplicationモジュールはAPIKit内のネットワーク層の実装を意識しなくても良いようになっています。

便利にするための小技

今回ネットワーク層を自動生成する過程で用いた便利な小技をいくつかご紹介します。

Sourceryでpublic initを自動生成する

Mapperを記述する過程で既存のオブジェクトにpublicなinit関数が必要になりました。 これを毎度手書きしているとなかなか大きな手間になるので、Sourceryで自動生成する仕組みを導入しました。

// sourcery: autoPublicInit

StructやClassの定義の一行上に上記のように記述すると下記以下のようなpublic initを生成してくれます。これによって面倒な記述を減らすだけでなく、抜け漏れなども起きなくなりました。

// sourcery:inline:auto:Recipe.autoPublicInit
    public init(
        id: Int,
        title: String
    ) {
        self.id = id
        self.title = title
    }
// sourcery:end

GitHub ActionsでスキーマをWeb・Android・iOSにデプロイする

OpenAPIのスキーマは独立したレポジトリで管理されていて、これによって生成されたスキーマファイルはWeb・Android・iOSプロジェクトの各レポジトリ内でそれぞれに保持しています。 これらの同期を簡単にするため、OpenAPIスキーマのレポジトリからGitHub Actionsでそれぞれのレポジトリにデプロイし、プルリクエストを立てる仕組みを用意しました。

まとめと今後

今回ネットワーク層を自動生成したことによって、iOSプロジェクトもより確実に最新のAPIの最新の定義に追従できるようになりました。また、ネットワーク層を自動生成するようになったことで、よりアプリケーション層の実装に注力できるようになったと考えています。

さらにDTOをOpenAPIのスキーマから自動生成する過程で、より良いスキーマ定義について考える機会となったということも今回のプロジェクトの副産物でした。

現時点では全てのDTOとエンドポイントを利用できているわけではなく、一部APIのスキーマを改善しながら継続して既存の仕組みとの置き換えを進めてていく必要があります。 導入にあたっては私の所属していたGlobal Mobile Platform(モバイル基盤)チームで主導しましたが、現在ではProductチームのiOSエンジニアにもそれぞれの担当している機能に導入をお願いするなど、全iOSエンジニアで進めていけるように継続的にコミュニケーションを取っています。

海外版Cookpadを開発するブリストルオフィスのチームには機能の開発をするプロダクトチームと、今回のOpenAPI Specificationsのような基盤の改善をするプラットフォームチームに分かれています。 両チーム共にiOSエンジニアを募集しているので興味のあるかたはぜひご連絡ください。

今回私はHorizonという新卒向け研修プログラムでブリストルオフィスに出向しました。Horizonについての詳細は以下の記事をご覧いただければと思います。

英国派遣プログラム「Horizon」 責任者に聞く制度への思い(前編)

最後までご覧いただきありがとうございました。

新卒向け社内研修制度Horizonを利用してイギリスに出向してきました

$
0
0

こんにちは。エンジニアの河邉です。

クックパッドにはHorizonという新卒3年目までの若手エンジニア・デザイナーにグローバルな環境で働く機会を与えて成長の後押しをしてくれる海外出向研修プログラムがあります。

詳細は英国派遣プログラム「Horizon」 責任者に聞く制度への思い(前編)を御覧ください。

2021年4月にクックパッドに新卒入社した私は、この制度を利用して2021年末にイギリスに渡航し、2022年1月から6月までの半年間ロンドンから電車で2時間ほどのブリストルにあるグローバル本社でiOSエンジニアとして働きました。

今回はその体験談をお届けします。

グローバルクックパッドについて

クックパッドは現在74の国や地域に32言語でレシピサービスを提供しています。 日本以外の地域で提供しているサービスはイギリスのブリストルにあるオフィスで開発されています。

Horizon参加の流れ

2021年の夏、Horizonの実施が社内ブログで告知されました。 私が参加を決めた理由は大きく分けて2つあります。

1つめは、グローバルクックパッドのサービス自体に興味があったからです。 私はクックパッドに入社する前からコミュニティサービスの開発に興味を持っていました。 もちろん日本のクックパッドや私がプログラム参加前まで開発を担当していたcookpadLiveにもユーザー同士のコミュニケーションは存在するのですが、グローバルクックパッドは現状いわゆるSNSチックなサービスの設計がされていて、ユーザーのコミュニケーションがより重視されたサービスだなと思っていたので、開発に携わってみたいと思いました。

2つめは、異なる文化や言語を持った多様なメンバーと一緒に働いてみたかったからです。これまでグローバルな組織でエンジニアとして働いた経験がなかったので人生経験として一度は挑戦したいと思っていました。

仕事内容

クックパッドグローバルのエンジニア組織は機能開発をする「プロダクト」、基盤の開発をする「プラットフォーム」と大きく2つに分かれています。

私はまずアプリの全体像を把握するためにグローバルモバイルプラットフォームチームという基盤の開発をするチームに入りました。 そこで最初に拾ったタスクがiOSプロジェクトへのOpenAPIの導入でした。

具体的な業務内容は別ブログでご紹介していますのでよかったらご覧ください。

techlife.cookpad.com

当初の予定では初めの3か月プラットフォームチームで働いてその後プロダクトチームで働きたいとバディ*と話していたのですが、結局このプロジェクトが楽しくなってしまい5か月以上プラットフォームチームで働いていました。

*入社してから数か月は同じ領域のエンジニアがバディとして付いてくれる。

6か月目にプロダクトチームのiOSエンジニアの退職に伴い、まだまだプラットフォームでやりたいこともありましたが、良いタイミングだったのでプロダクトチームに異動させてもらいました。

プロダクトチームはタイムラインを担当するチームや検索を担当するチームなど、基本的に機能ごとに1つのチームが編成されています。 私は検索機能の開発を担当するチームでiOSアプリの実装を担当することになりました。 私のチームでは週に1度イテレーションプランニングというミーティングがあり、ここで1週間のチームのタスクを決め、毎朝軽い進捗共有のミーティングをしています。

プロダクトチームに移ってから面白いなと感じているのは、主に2つあります。

1つは、ある機能のA/Bテストを行う場合にどの地域で実施するのかという議論があることです。この国では、〇〇な使われ方をしているから、〇〇のデータを持っているから、既に〇〇をリリースしているからなど、それぞれの国でのサービスのステージの違いや宗教的な行事の有無などによって機能の出しわけをするのは、多様な地域にサービスを提供している実感を持てて興奮します。

2つめはチームに専属のデータエンジニアがいることです。これは日本とグローバルの違いというよりは組織の違いでしかないのですが、グローバルクックパッドでは基本的に1つのプロダクトチームに1人のデータエンジニアがいて、A/Bテストをはじめとしたさまざまな施策の進捗を客観的な数値として報告してくれます。

日本にいたときは数値の測定も自分でする場合が多く、調べきれない部分もあったのですが、データエンジニアが付いてくれていることで自分達がやっていることに対する納得感が数値としてより正確に得られるのは、異なる母国語や文化を持つメンバーで構成されているチームでは特に重要なのではないかと感じています。

成長

まず技術面では、私は基盤的な開発をした経験がこれまでになかったので、長期間に渡って1つの大きな改善に取り組めたのは自信に繋がりました。 その過程でCreateAPIというOSSの改善に初めて加わることもできました。 プロダクトチームに異動してからはチームのiOSエンジニアとして工数見積もりの精度や実装スピードを週を追うごとに少しずつ向上できている実感があります。

これらの成長実感は日本とグローバルでの違いではなく仕事内容の変化によるものですが、6か月という限られた期間を別の組織に出向して、それまでと異なる働き方やプロダクトの開発に関わることのできるのはHorizonの魅力と言えると思います。新卒で入社してからの1年間で、複数の組織で複数のプロダクト開発に関われたことはとてもありがたいことでした。

グローバルで働くということならではの成長としては、日本にいるときよりもより簡潔に議論を展開できるようになったことだと思っています。もともと喋りながら考えてしまうタイプでまとまりのない主張をしてしまいがちだったのですが、英語力の問題もあり、より結論ベースで簡潔な主張をするようになりました。

また今回1歩海外へ踏み出したことによって将来の自分のキャリアへの展望が大きく広がったと強く実感します。海外で英語を使って働くことができるという自信と、同僚の多種多様な来歴を聞いて、今後も海外で働くなど、日本にいたときには考えられないような将来の選択肢を考え始めるようになりました。

私はHorizonプログラムの終了後、グローバルクックパッドの選考を受け、転籍することにしました。クックパッドをもっとたくさんの人に世界中で使われるサービスにしていきたいですし、個人としても今後も日本だけじゃなくていろんな国で活躍できるようになっていきたいです。

生活

まとめの前に生活面の経験についても少しご紹介させていただきます。

現地に到着してまず取り組んだのは友達づくりでした。思い切って参加した家の近くにあるダンス教室で友達をつくることができました。これまでダンスをしたことはなかったし、かなり勇気のいることでしたが飛び込んでみてよかったと思います。会社外でも友達をつくることで仕事から完全に離れてリフレッシュする時間を楽しめました。

ブリストルでは美味しいクラフトビールが楽しめます

またイギリスからヨーロッパへは飛行機で安く行けるので、スイス、フランス、オーストリア、スロヴァキア、スペインに週末や有給をつかって観光に行きました。あまり遠く離れていないにも関わらず、それぞれの地域で言語や食などの文化の違いを大きく感じられるのは、ヨーロッパに住む大きなメリットだと感じました。

フランスのモンブラン近くでスノーボードをしました

まとめ

グローバルクックパッドを開発したい、海外で働いてみたいという2つの思いを持って参加したプログラムでしたが、1つめのサービス開発という面では挑戦しきれなかったというのが正直なところです。ほとんどの時間を基盤系の仕事に使ったことも理由ですが、プロダクトチームに移ってからもまだ毎週のリリースサイクルに付いていくのが精一杯です。これからもっとスピードをあげてiOSエンジニア領域外の仕事にも手を出して行きたいと思っています。

次に2つめの目的だった海外でグローバルな組織で働くということについては、本当に経験してよかったと思っています。同僚との何気ない会話の中からカルチャーショックをもらったり、仕事終わりのパブでいろいろな人生観を知ったり、そういった刺激がとても心地よくて同僚と会話することを楽しみに会社に行っています。

ただ「毎日の料理を楽しみにする」という共通の目標を持っている人間同士、同じである部分も多く感じられました。 日本にいてメディアなどを通じて伝わってくる「違い」だけではなく、「同じ」であることも多く実感できたことは、これから先グローバルに働いて生きていく上でとても重要な感覚を得られたなと思っています。

Horizonプログラムはクックパッドに新卒で入社する最大の魅力の1つだと思います。また中途入社でイギリスで活躍している方もいらっしゃいます。ご興味があればぜひご連絡ください。

最後までご覧いただきありがとうございました。

iOSDC Japan 2022 ありがとうございました!#iwillblog

$
0
0

クックパッドでモバイルエンジニアをしているあつや(@n_atmark)です!iOSDCに参加されたみなさまお疲れ様でした!

さて、クックパッドは先日の iOSDC Japan 2022 でプラチナスポンサー & スポンサーブースを務めました。 スポンサー特典の一つにオープニングでの社名読み上げがあるのですが、今年もミサトさん(三石 琴乃さん)に社名を読み上げていただいて「これこれ〜!」とテンションが上がってしまいました。

会期中には、クックパッドに所属する あつや(@n_atmark)、アイカワ(@kalupas226)、ainame(@ainame)、あおい(@aomathwift)、yujif(@yujif_)がスピーカーとして登壇しました。

イベント参加のご報告として、当社所属メンバーの発表やブースの紹介をいたします!

発表

施策基盤としてのディープリンク 〜なめらかにアプリが開く体験のために〜 / あつや

https://fortee.jp/iosdc-japan-2022/proposal/6c4615f5-b471-4f78-9044-4ae3f8dd75d4

あつや(@n_atmark)からはディープリンクに関する発表を行いました!間違えやすいディープリンク用語の整理や施策の効果を発揮するために活用できる仕組みを紹介しています。

speakerdeck.com

個人ブログでウラ話も紹介しているのでよければご覧ください!

SwiftUI Navigation のすべて / アイカワ

https://fortee.jp/iosdc-japan-2022/proposal/0b6f453a-68f0-4300-9ab2-cb1e3457eb53

アイカワ(@kalupas226)からは”SwiftUI Navigationのすべて” という題で発表がありました!SwiftUIのNavigation APIを俯瞰した説明や、既存のNavigation APIの課題と課題に対するアプローチを紹介しています。

speakerdeck.com

9/28に開催されるAfter Party iOSDC Japan 2022では本編トークに入り切らなかった話もされるそうですよ!

cookpad.connpass.com

Swift 5.7で変わる正規表現を試してみよう / ainame

https://fortee.jp/iosdc-japan-2022/proposal/6ce89b3f-8f08-47ba-a78d-deaee335c215

Swift5.7で強力になった正規表現周りのアップデートに関してainameから発表がありました!これまでのSwiftの正規表現と比較して、どのようなアップデートがあったのかを紹介しています。

speakerdeck.com

Swift5.7でのRegexの実装についてさらに理解を深めたい方は、ainameがわいわいswiftcで発表した内容もぜひご覧ください!

www.youtube.com

即時通知を導入する際に考えるべきこと / あおい

https://fortee.jp/iosdc-japan-2022/proposal/dc963bce-52bd-429c-86ed-9a7b314063ce

あおいからは即時通知に関する発表が行われました!即時通知の紹介と、どの通知を即時通知にすべきかの判断をチーム内でどのように運用していくのかプラクティスを踏まえて紹介しています。

speakerdeck.com

即時通知の機能紹介をあおいがWEB+DB Pressに寄稿しています!バックナンバーになりますが、こちらもよろしければご覧ください。

aomathwift.hatenablog.com

モバイルアプリの行動ログの「仕込み」を快適にする / Yuji Fujisaka

https://fortee.jp/iosdc-japan-2022/proposal/ad544d2d-0e37-48f7-836a-3d46abe4ad2f

yujifからは”モバイルアプリの行動ログの「仕込み」を快適にする”という発表が行われました!ログ収集における悩みの種のうちログ実装の「仕込み」に対して、仕組み化によって解決を図っている事例を紹介しています。

speakerdeck.com

「Markdown定義からログの実装コードを自動生成する仕組み」に関してはクックパッド開発者ブログに載っています。

ブースや企画の様子

Cookpad TechConf 2019で元クックパッドのgiginetさんが発表したマルチモジュール図(通称: 親の顔より見た図)のパネルと、2022年現在のマルチモジュール図をパネルにして用意しました。

新旧見比べることで2019年からモジュール構成に差分があったり、モジュール分離が進んでいる様子を知ることができ参加者の方からも好評でした!

また、9/11(日) day1ではクックパッドアプリの開発の様子を体験できるデモをブースで実施しました。 「テンプレートを用いたVIPERシーンの自動生成を行い、シーンに対してサンドボックスビルドを生成するデモ」と、「ログ定義Markdownからログ実装用コードを自動生成し、仕込まれたログを疎通確認ツールで検証するようなデモ」の2つを実施しました。

デモが成功すると「おおー!」と歓声があがって盛り上がりました!

9/12(月) day2にはアンカンファレンスブースを利用して、CTO座談会とios-cookpadのコード見せる会を実施しました!

CTO座談会では、あつや(@n_atmark)[クックパッド事業本部 買物サービス開発部所属 *1 ]・あおい(@aomathwift)[買物事業本部 買物プロダクト開発部所属 *2 ]・CTOの成田(@mirakui)の3人で"買い物領域"について話しました! クックパッドはレシピサービスをずっとやっている印象をもたれることも多いのですが、クックパッドが力を入れている “買い物領域” について知ってもらえるきっかけになりました。

ios-cookpadのコード見せる会では、2019年に盛況だったios-cookpadのコード見せる会からのアップデート部分を主に、クックパッドエンジニアが順番に実装コードの紹介を行いました!多くの方にアンカンファレンスブースを覗きに来ていただけました。「クックパッドアプリで使われているカスタムナビゲーションの実装*3を見せて欲しい」とリクエストをいただけたりしました。

おわりに

改めて、iOSDC Japan 2022ありがとうございました!

次は10月5日から開催される DroidKaigi 2022 に参加する予定です。みなさまにお会いできることを楽しみにしています🤗

droidkaigi.jp

宣伝

まだまだiOSDC熱が収まらない皆さま!9/28(水) 19:00よりYouTube Liveにてクックパッド主催のiOSDCアフターイベント「After Party iOSDC Japan 2022」を実施します!

cookpad.connpass.com

  • enum で Key Paths のような機能を実現する Case Paths / アイカワ(@kalupas226)
  • Maintainability Indexを計測することでiOSプロジェクトのコードの保守性を改善した話 / toya108(@tk108gabalian)
  • DocC Documentation Archiveをアプリ開発で活用してみよう / あおい(@aomathwift)
  • OpenAPIのクライアント自動生成を現場に導入していくためのノウハウ / imajin(@mrimjn)

4つのトークタイトルを発表予定です。ぜひお越しください!

*1:クックパッド事業本部 買物サービス開発部: レシピサービスクックパッドに買い物体験を取り入れることで、食卓におけるレシピ決定をより豊かにすることを目標としている部署

*2:買物事業本部 買物プロダクト開発部: クックパッドマートにおける注文、出品、マーケグロース等、作り手と買い手のための仕組みづくりの設計開発を行う部署

*3:UINavigationControllerをカスタマイズ 〜OSの影響を受けづらいカスタムナビゲーションの実装〜

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

$
0
0

技術部の遠藤(@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`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以降に気づいてなかった人はぜひ今からでも挑戦してみてください。


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

$
0
0

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の解説(裏ステージ)

$
0
0

技術部の遠藤(@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

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

defanswer11(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)

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

defanswer12(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の一覧では?と考えついてください。ということで答えです。

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

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

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

defanswer13(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、……。ということで答え。

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

なお、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文字です。ということで答え。

defanswer15(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に変わりました。ということで答え。

defanswer16(&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が出てきます。

defanswer17(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)

ということで答え。

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

18問目

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

defanswer18(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]

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

defanswer18(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]

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

defanswer18(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。

deffactor(tokens)
  t = tokens.shift
  if t.is_a?(Integer)
    ["value", t]
  elsif t == "("
    r = expr(tokens)
    tokens.shift
    r
  else"unknown: #{ t }"endenddefterm(tokens)
  r = factor(tokens)
  t = tokens.first
  while t == "*" || t == "/"
    r = [tokens.shift, r, factor(tokens)]
    t = tokens.first
  end
  r
enddefexpr(tokens)
  r = term(tokens)
  t = tokens.first
  while t == "+" || t == "-"
    r = [tokens.shift, r, term(tokens)]
    t = tokens.first
  end
  r
enddefanswer19(tokens)
  expr(tokens)
end

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

20問目

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

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

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

defanswer20(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])
  elseraise"unknown operator: #{ e[0] }"endend

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

まとめ

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

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

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

おまけ:チート対策

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

テスト入力を盗み見る

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

defanswer1(n)
  p n #=> 826end

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

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

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

defanswer7(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)
defanswer7(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は完全に同じ挙動にできてしまいそうです。

defanswer1(...)
  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 :-)"ifcaller.any? { _1.include?("answer") }

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

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

defproxy1(...)
  Fiber.new { func1(...) }.resume
enddefanswer1(...)
  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知識を前提にするという発想で作りました。

DroidKaigi 2022に弊社の社員が登壇 &スポンサー企画のご案内

$
0
0

こんにちは!クックパッドCTO室の緑川です。今週の10月5日からいよいよDroidKaigi 2022ですね。先月のiOSDC Japan 2022でも思いましたが、カンファレンスがリアル開催となると多くの方と交流ができるので嬉しく思います。

クックパッドは、DroidKaigi 2022ではゴールドスポンサーとして協賛させていただいており、会場ではブース出展をします。数名のAndroidエンジニアと私でお待ちしていますので、気軽にブースまでお越しいただきお声がけいただけますと幸いです。

この記事ではクックパッドのエンジニアが登壇するトークの紹介とブースの案内をさせていただきます。

トークの紹介

今回クックパッドからは1名のエンジニアが登壇することになりました! Glass EE2をテーマとしており、Glass EE2向けの開発環境やデザインガイドライン、アプリ開発例を紹介します。 スマートグラスに関心がある方はぜひご参加ください。

Day1 10/5(水) 14:20〜14:45 CARDS(25分) 登壇者: shanonim タイトル: 実践 Glass EE2 向けアプリ開発

droidkaigi.jp

協賛企業選出のライトニングトーク

また17:45-19:00から行われる「協賛企業選出のライトニングトーク」でもLTを行う予定です。 クックパッドのイベントとプレゼントのご連絡がありますので、よろしければこちらもお聞きいただければ幸いです。

droidkaigi.jp

ブースの紹介

クックパッドブースでは、Androidアプリの展示を行います。 またブースにお越しいただいた方には先着でキッチンタオルをプレゼントさせていただきます。

また、弊社のエンジニアと交流をしていただいた方には、先着でパスタの1人前と2人前の量を測れるパスタメジャーをプレゼントさせていただきます。

両方とも数量限定なので、予告終了する可能性がございます。

After Party DroidKaigi 2022を開催

上記にも記述させていただきましたが、DroidKaigi 2022開催後の10月19日(水)19:00 〜 21:00にオンライン上でクックパッド主催イベント「After Party DroidKaigi 2022」を開催いたします。※このイベントは「DroidKaigi 実行委員会」が運営するものではありません。

cookpad.connpass.com

こちらのイベントではクックパッドで働く4名のエンジニアが登壇する予定です。

  • こやまカニ大好き:「マルチモジュールアプリの画面遷移処理実装」
  • ハギー:「実はクックパッドでもFlutter使ってるんですよ〜」
  • koguchi:「Jetpack Compose の Preview を効果的に使って開発する」
  • monchan:「チームの開発効率を上げる取り組み」

Androidアプリ開発をはじめとしたモバイルアプリ開発についてお話しします! クックパッドのモバイルアプリ開発や社内の様子について興味のある方はぜひご参加ください。

おわりに

先月から多くのカンファレンスでリアル開催が増えてきており、多くのエンジニアの方とお話ができる機会がありとても嬉しく思います。 DroidKaigiでもブースには常にクックパッドの社員がいるようにしますので、お気軽にブースまでお越しください!みなさまにお会いできることを楽しみにしております。

最後に宣伝にはなりますが、クックパッドで働きたいエンジニアの方を募集しております。ぜひブースでお声がけいただくか、クックパッドのウェブサイトからご連絡ください!

info.cookpad.com

Androidアプリ開発を効率的に行うための仕組み

$
0
0

こんにちは、サーバーサイドエンジニアをしつつ、最近はAndroid開発初学者のhyogaです。 入門してからそろそろ1年経とうとしているので、もう初学者と言えないかもしれないですね。

クックパッドでは、開発を効率的に行うために、様々な仕組みやツールが導入されています。 この記事ではクックパッドでのAndroidアプリの開発中からアプリのリリースまでに使われる以下のような仕組みやツールについて広くお伝えできればと思います。

Flipper

Flipper

Meta社の作っているFlipperというツールです。 クライアント側でセットアップし、使いたいPluginごとの対応が必要ですが 対応さえ済んでしまえばFlipperを開くだけで、以下の内容などが見れるようになります。

ネットワークViewerでは、 APIリクエストをmockして特定のAPIリクエストでエラーを起こしたり、レスポンスを任意の値に置き換えたりすることができます。 APIリクエストエラー時の挙動が簡単に確認できたりして、とても便利ですね。

APIレスポンスを書き換えている様子 モックされたAPIレスポンスにより部分的にエラーになっている様子

他にもShared Preferencesの内容を確認して、書き換えたりもできます。 クックパッドのAndroidエンジニアの中では必須ツールになっています。

Shared Preferencesの中身を書き換えている様子

開発者用のデバッグツール

デバッグビルド版にのみサイドメニューに導線が現れる

社内で開発されている開発用のデバッグツールです。 クックパッドAndroidアプリは マルチモジュール化されています。 このデバッグツールもその中の1つのモジュールとして提供されており、 デバッグビルド版でのみ使うことができます。

この開発者ツールでは、以下のような事ができます

  • APIの接続先(ステージング/プロダクション)の切り替え
  • ログインしているユーザの行動ログの閲覧
  • ログインしているユーザの簡単な切り替え
  • A/Bテスト用のツールのA/Bパターンの切り替え
  • などなど...

これにより、エンジニア以外でも特殊なツールなしにデバッグや動作確認が容易になっており、とても便利です。

また、この開発者ツールはリリース版ビルドには含まれないので、実験的に新しい機能や ライブラリを導入して試すような使われ方もしています。

Android StudioのFile and Code Templates

クックパッドAndroidアプリでは、VIPERっぽい*1アーキテクチャを用いて実装されています。 VIPERアーキテクチャでは、1つの画面を実装するために InteractorRoutingなどといったたくさんのファイルを用意する必要があり、あたらしい画面を実装するたびに、同じ似たようなコードを書くのはなかなか手間がかかります。

クックパッドAndroidアプリのVIPERベースのアーキテクチャ*2

そこでクックパッドでは、 Android StudioのFile and Code Templates機能を使い、必要なファイル群をガガっと生成することができるようにしています。 基本的に新しい画面を実装する時は、このファイル群を編集していく形になります。

生成されたファイル群

少し細かい工夫ポイントは、Templatesを使った時点でビルドが可能な状態になっていることです。

生成されたContractファイル

これによって、Interactor、 ViewModelといったデータの取得部分から実装する必要がなくなり、Viewから実装する選択肢を取れるようになりました。 Jetpack ComposeだとPreviewで簡単にViewを確認できるということもあり、個人的にはとても嬉しいです。

ちなみにTemplatesは .idea/fileTemplates/以下に配置しており、クックパッドAndroid アプリを開発するエンジニアが共通して同じものを使えるようにGitリポジトリに含めています。 *3

.idea/fileTemplates/

haneda: 開発中のアプリを簡単にインストールできる仕組み

クックパッドでは、GitHub Enterpriseを使っておりPR上でコードレビューなどをします。 クックパッドAndroidアプリのリポジトリでは、 PRを出すとCIによってビルドされたアプリがhaneda(社内のアプリ配信サービス)から配信され、 社員は手元の端末にPRの変更が適用されたアプリをインストールすることができます。

最新のビルドだけでなく過去のビルドなどもインストールできたり、クックパッドAndroidアプリだけでなく、クックパッドで開発されている全てのアプリがhaneda上で配信されているので、クックパッドのメンバーが社内の開発中アプリにアクセスしやすくなっています。

ブラウザで開くとQRコードが表示されて便利

社内の誰でもQRコードから開発中のアプリをインストールできることはとても便利です。 エンジニア以外の人も開発中のアプリをインストールして動作確認したりドッグフーディングすることができます。

おわりに

どうでしたでしょうか? この他にも

  • アプリにbeta機能として特定の機能を追加できるFeature Flag
  • マルチモジュール構成になってるアプリの特定のモジュールだけを別アプリとして切り出すことのできる仕組み*4
  • Markdownで管理された行動ログのログ定義から実装コードを自動生成できる仕組み*5

など日々エンジニア主体でAndroidアプリ開発を効率的に行う仕組みの改善や導入がおこなわれています。

クックパッドは、DroidKaigi 2022にゴールドスポンサーとして協賛しています!会期中は会場でブース出展しますので、興味を持っていただけた方はぜひブースにお越しください。 オフラインでもオンラインでも他の話も聞いてみたい!などあれば気軽にご連絡お待ちしてます!!

info.cookpad.comtwitter.com

クックパッドマートにおける item-to-item レコメンデーションの変遷

$
0
0

こんにちは。研究開発部の深澤(@fufufukakaka)です。

本記事ではクックパッドマートにおける item-to-item レコメンデーションについて、その概要とアルゴリズムの変遷についてお話したいと思います。

item-to-item レコメンデーションとは

レコメンデーションにはいくつかタスクが存在しますが、今回はその中でも item-to-item レコメンデーションについてお話します。

item-to-item レコメンデーションでは、「ある商品について、その商品を軸におすすめできるアイテム」を表出します。表現の仕方はサービスによって様々ですが、よく この商品を買っている人にはこちらもおすすめです , この商品に関連する商品などと表現されています。

さて、その item-to-item レコメンデーションの中にも実は更に種類があります。それは商品間のスコア(距離,類似度,etc) をどの軸で表現するか、です。大きく分けて以下の2種類になるかと思います。

  • ある商品について、その商品に関する行動データ(商品a を買った後によく商品b が買われている、など) を利用してスコアを計算する行動データを使った方式。 この商品を買っている人にはこちらもおすすめですといったレコメンドを実現する際に選択されます
    • 行動データを使って似ている商品を出すことを目的としたモデルもあります
  • 商品名や商品画像を使って商品間の類似度を使うコンテンツデータを使った方式。この方式でおすすめ商品を表出している場合は この商品に関連する商品に適したレコメンドになります

今回は item-to-item レコメンデーションかつ行動データを使ったモデルの話になります。

クックパッドマートにおける item-to-item レコメンデーション

クックパッドマートでは早くから item-to-item レコメンデーションが取り入れられています。

レコメンドモデルによって算出されている「よく一緒に購入されている商品」

各商品の詳細ページ下部に、この商品を買っている人におすすめといったセクションがあります。ここで出ている 6 商品は機械学習モデルによって毎日更新されています。この機能は2020年に追加され、いくつかモデルの変遷を経て現在、こちらの機能はクックパッドマートの売上においてある程度の貢献を果たすに至っています。

ここからは実際にどのような変更があったのかをお話します。

アルゴリズムの変遷

Item2Vec 時代

初めは Item2Vec でレコメンドを実装していました。Item2Vec は https://arxiv.org/abs/1603.04259で提案された手法です。僕は前職の先輩方が使っていたのを見て使いはじめました。

Item2Vec は単語の意味ベクトルを獲得する Word2Vec をレコメンドに応用したもので、Word2Vec が単語を対象にするのに対して、Item2Vec では商品を対象にし、商品ごとの意味ベクトルを獲得します。これによって商品Aと商品Bの距離が計算でき、「商品Aに距離が近い商品群」が取得できます。

"距離"の意味合いは学習のさせ方によって異なります。当初は1回の注文で一緒に購入された商品を軸に学習させました。具体的には order を context、その order に含まれる item_id を word とみなして Word2Vec の文脈で扱います。

Item2Vec の魅力はとても簡単なコードでレコメンドが実装できる点にあると思います。gensim を駆使することで数行でレコメンドの大枠が整います。例えば user_id, order_id, item_id, cv_datetimeという dataframe から学習させたい場合には

from gensim.models import word2vec

# df.columns = ["user_id", "order_id", "item_id", "event_timestamp"] のようになっていると仮定します
order_product_dict = (
  df.groupby(["order_id"])["item_id"]
  .apply(lambda x: [str(v) for v in x.tolist()])
  .to_dict()
)

# word2vec_params というパラメータを集約した yaml or json を事前に読み込んでいると仮定します
item2vec = word2vec.Word2Vec(
  order_product_dict.values(),
  vector_size=word2vec_params["vector_size"],
  window=word2vec_params["window"],
  alpha=word2vec_params["alpha"],
  sample=word2vec_params["sample"],
  epochs=word2vec_params["epochs"],
  ns_exponent=word2vec_params["ns_exponent"],
  min_alpha=word2vec_params["min_alpha"],
  hs=0,
  negative=1,
  sg=1,
  seed=42,
  min_count=1,
  workers=4,
)

このようなコードでレコメンドモデルの学習自体は完了します。 推薦したいときは most_similar 関数でコサイン類似度に基づいて、クエリとなる商品と "距離"の近い商品を指定した数だけ取得できます。

item2vec.wv.most_similar(target_product_id, topn=10)

このように Item2Vec による推薦は gensim に乗っかることでかなり簡単に実装できます。このモデルを使ったレコメンドが 2020年9月頃からアプリに載り始めました。その後、以下のような変更・施策を試してきました。

  • 編集距離を用いた名寄せ
    • 当初はキャンペーン用に商品が複製+prefix に SALE など文字列が付いた、実体は同じだけど違う id の商品がたくさんありました。これを編集距離ベースで名寄せしていました。現在は複製されずにセールの運用が可能になったため、あわせてこちらも退役させています
  • 直近出品された商品の中から推薦する枠を混ぜる
    • 学習データを1年分としているのですが、直近出品された商品をフィーチャーする目的として枠を用意していました。具体的には6商品を普通に推薦し、3商品を直近枠として混ぜていました。現在、RecVAE に変わったタイミングで6商品のみとなり、このタイミングで外しました(まだ実装自体は残っています)
  • 特定のカテゴリの商品には予め決めたレコメンドを出す
    • これは Item2Vec に限った話ではないですが、例えばワイン系の商品のレコメンドではワインによく合う(だろうとこちらが考えて決めた)おつまみリストを出すようにする、という施策を試しました。相談を受けて試してみたところ、有意な変化が見られず、そのまま元に戻しました

Item2Vec からの転換

Item2Vec での運用は非常に手軽だったため、アルゴリズム本体以外の細やかな変更を色々試せました。が、ずっと思っていた心残りがありました。オフラインテストでの精度指標があまり高くなかったのです。

Metrics Value
Hit@10 0.149
MRR@10 0.0549
NDCG@10 0.0604
Precision@10 0.0176
Recall@10 0.1012

これはある時点でのデータを抽出して Item2Vec での学習を実施した際の結果です。NDCG@10 が 0.06 となっていますが、これは感覚的にはとても良い数字とは言えないものでした。

Item2Vec を倒すために Matrix Factorization・特徴量をいくつか混ぜた Factorization Machine 系のものをニューラルベースで実装し試したのですが、精度指標が多少上回っても推薦の偏りが強くなったり(Coverage が下がってしまった) となかなか満足いくアルゴリズムが出せずにいました。

RecBole による実験

そんなことを考えつつ過ごしていたときに、以下のブログでも紹介した RecBole を知り、これを PoC だけでなく実践で試してみようと考えました。

techlife.cookpad.com

RecBole についての紹介は前述した記事で記載した紹介を引用します。

RecBole は中国人民大学・北京大学の研究室が共同で始めたプロジェクトのようで、去年の11月に arxiv に登場しました。今年の8月に提供しているモジュールがv1を迎えて、本格的に色々な人が利用するようになったようです。 RecBole 最大の魅力は、上述してきた再現性の難しいレコメンドモデルを統一したインタフェースで実装し、比較を容易にしているところにあります。そして実装されているモデル、適用できるデータセットの数が凄まじいです。モデルは現時点で70以上(モデルリストがすごい )、データセットは20以上のものについて即座に試せます。どれくらい即座に試せるかと言うと

pip install recbole
python run_recbole.py --model=<your favorite model> --dataset_name ml-100k

これだけで、レコメンド界隈の中で最も有名なベンチマークである MovieLens-100k データセットに対して70以上のモデルを即座に(追加の設定が必要なやつもありますが)試せます。これだけのモデル・データを試すことができる環境はそうないと思われます。また70以上の収録されているモデルたちは全て PyTorch ベースで丁寧に再実装が行われており信頼性は非常に高いです。predict 関数などの基本的なインタフェースは統一されており、実験のし易い環境が整えられています。

RecBole は user に対する item への推薦を仮定しているため、item-to-item のレコメンドで RecBole を適用するためにはログを変形する必要があります。 具体的には user1: item1,item2というログが得られたらこれを変形して item1→item2, item2→item1というように関連して購入されたと思われるセッション内でペアをつくり、各アイテムを user とみなしたデータを作ります。 実際には「あるユーザがその日購入した商品履歴」を軸にしてペアを作っています。

こうしたデータを作り、RecBole で実験を行ったところ、以下の結果を得ました。

Name hit@10 mrr@10 ndcg@10 precision@10 recall@10
RecVAE 0.3264 0.1368 0.1358 0.0617 0.1864
MacridVAE 0.3111 0.1537 0.1352 0.0661 0.1612
EASE 0.2598 0.1352 0.1123 0.0657 0.1298
NeuMF 0.2884 0.1102 0.1115 0.0546 0.1554
NNCF 0.2598 0.1072 0.0995 0.0528 0.1277
ItemKNN 0.2339 0.1039 0.0954 0.0461 0.123
NGCF 0.2116 0.1017 0.0786 0.0436 0.0914
BPR 0.2069 0.1014 0.0765 0.0458 0.0847
MultiDAE 0.2095 0.0949 0.0755 0.0427 0.0907
DGCF 0.18 0.0909 0.0619 0.0386 0.0653
Item2Vec 0.149 0.0549 0.0604 0.0176 0.1012
SLIMElastic 0.1672 0.0861 0.0597 0.0419 0.054
LightGCN 0.1734 0.0881 0.0593 0.037 0.0604
DMF 0.1732 0.0843 0.0586 0.0392 0.0585
CDAE 0.1524 0.083 0.0517 0.0336 0.0462
SpectralCF 0.1543 0.0804 0.0502 0.0336 0.0446
LINE 0.1331 0.0732 0.0419 0.0315 0.0282
Pop 0.1174 0.0424 0.0315 0.0234 0.0332
ENMF 0.073 0.0355 0.0211 0.0184 0.0111
GCMC 0.003 0.0011 0.0015 0.0003 0.003

30モデル弱を実験し、RecVAE など VAE 系が NDCG@10 で 0.13 程度を達成しました。 これは Item2Vec の成績と比較するとかなりよかったので、RecVAE と Item2Vec をオンラインで比較してみることにしました。

RecVAE について

RecVAE はユーザとアイテムのヒストリーを行列にした上で、 Variational Auto-Encoder というニューラルネットワークで圧縮・復元の学習を行い、ユーザとアイテムのヒストリー行列を正確に復元できるように学習したモデルです。 Multi-VAE の構造を元にしつつ composite-prior の導入や Encoder 層の改良を行っています。

Paper-With-Code のレコメンドに関するリーダーボードで NDCG による評価が行われている Movielens 20M (https://paperswithcode.com/sota/collaborative-filtering-on-movielens-20m) では、RecVAE は 2019 年に発表されたモデルでありながらまだ 3位の位置を保っており、かなり強いモデルです。

先程のブログでも紹介した、社内のデータを使った実験でも殆どのケースで RecVAE は上位に入っていました。

インターリービングによるオンラインテスト

オンラインでの比較にはABテストが実施されることが一般的ですが、今回は実験的な意味合いも含めてインターリービングを実施しました。

インターリービングとは推薦システムなどランキングを出力するタイプのアルゴリズムに関して適用できる手法で、比較したい2つのランキングアルゴリズムの出力を混ぜて一つのランキングを出力します。その混ぜたランキングについてユーザが何らかのアイテム(アイテムα)にコンバージョンした際、どちらのアルゴリズムによって出力されたアイテムなのかという観点から各アルゴリズムにポイントを割り振り、アルゴリズムA・Bのどちらが一方より優れていたか、をクエリ(アイテムα)ごとに出します。

インターリービングが ABテストと最も異なっている点は、群を分けることなく同じユーザ群を使った実験が可能で、群差を気にする必要がない点です。また、同じユーザに直接アルゴリズムA, Bを提示して暗黙的な直接比較を要求するので、ABテストよりもはっきり結果が出やすい点が特徴的です。CVR など性能を定量的に測れない(相対的にしかわからない)などデメリットも多いですが、前述したメリットに着目して実施してみることにしました。

今回は Pairwise Preference multileaving(PPM) を使ってランキングを生成してみました。こちらのライブラリ(mpkato/interleaving) を利用すると以下のように実装できます。

In [1]: import interleaving

In [2]: a = [1, 2, 3, 4, 5]

In [3]: b = [4, 3, 5, 1, 2]

In [4]: method = interleaving.PairwisePreference([a, b])

In [5]: interleaved_ranking = method.interleave()

In [6]: type(interleaved_ranking)
Out[6]: interleaving.ranking.PairwisePreferenceRanking

In [7]: interleaved_ranking
Out[7]: [4, 1, 2, 3, 5]

In [8]: interleaved_ranking = method.interleave()

In [9]: interleaved_ranking
Out[9]: [4, 1, 3, 2, 5]

評価するときは以下のようになります。

In [10]: my_ranking = interleaving.PairwisePreferenceRanking([a, b], 
                 contents=interleaved_ranking)  # ランキングクラスを復元する

In [11]: interleaving.PairwisePreference.evaluate(my_ranking, [0, 1])  # アイテム4と1がクリックされたとする
Out[11]: [(0, 1)]  # ランキングAが勝ったことを示す

2022年2月~3月で Item2Vec・RecVAE のランキングを混ぜてアプリに表出し、そのデータを集めました。結果は以下のようになりました。

Name Value
RecVAE 勝利数 1295
Item2Vec 勝利数 1149
引き分け回数 899

これについて Binomial sign test (有意水準 5%) を行い、RecVAE が有意に Item2Vec より優れていることがわかりました。

RecVAE と Item2Vec の比較(定性評価)

2022年 3月頃のレコメンド結果からいくつかのケースを抽出して考察してみます。現時点(2022/10/05)でクローズされていない商品にはリンクをつけております。

ケース1. アトランティックサーモン

クックパッドマートの中でも非常に人気な商品であるアトランティックサーモン (https://cookpad-mart.com/products/1592) について、RecVAE と Item2Vec のレコメンドをそれぞれ見てみました。

Item2Vec は比較的同一系統の商品を提案していますが、RecVAE はサーモンと併せて使えそう・ついでにこちらも、といった観点で食材を提案している様子が見て取れます。

ケース2. 有機じゃがいも

続いて、【有機】じゃがいも400g(https://cookpad-mart.com/products/7520) という商品のレコメンドを確認します。

Item2Vec はこちらでも同系統(といってもじゃがいもばかり出しているのではなく、有機という特徴を捉えている)の商品を出しており、RecVAE はそれとは対照的な商品推薦を行っています。

ケース3. ささみ

最後に お徳用 ささみ 大袋 約500g (https://cookpad-mart.com/products/1605) の推薦結果を確認します。

やはりここでも、同様の傾向が見られました。Item2Vec は行動データを使ったモデルではありますが、クエリとなる商品に対して置き換え可能な商品候補を上位に出す傾向が強そうなことがわかります。

Item2Vec は Word2Vec の仕組みを転用しているため、周辺の商品から出現しそうな商品を予測することで獲得した重みを使った推薦を行っています。「私はカレーが好きです」という文章があったら、カレーの代わりにシチューなどが単語として入り得ますが、そうしたときにカレー・シチューが単語として近いベクトルを持つようになるといったイメージです。

Item2Vec ではこれがどうなるかというと、「ブロッコリー・ささみ・ピーマン」という商品履歴があった際に、これが「ブロッコリー・鶏むね肉・ピーマン」となってもタンパク質を重視したヘルシーな商品の並びという意図は変化しなさそうです。このようなときに、ささみと鶏むね肉は近いベクトルを持つことになります。こうしたことが起きていると考えると、Item2Vec の推薦傾向が、行動データを通じて似ているアイテムを出す、というものであることが納得できるかと思います。

RecVAE の内部でどのような学習がなされているか、Item2Vec よりも学習の仕組みが複雑であるため詳細に考察を述べることは難しいのですが、VAE によって商品履歴を復元する過程で「この商品にフラグが立っていたらこちらにもフラグが立つだろう」という推論がうまく行えているのだろうと思います。

レコメンドのアーキテクチャ

この結果を受けて現在レコメンドモデルは RecVAE (in RecBole) となっています。アーキテクチャは以下のとおりです。

レコメンドのアーキテクチャ図

  • レコメンドは日次バッチで更新されます。一連の流れは Kuroko2という社内ジョブ管理システムによってキックされます
  • まず Redshift から 3ヶ月分のカート追加イベントを取得します (Queueryを使用して取得します)
  • Hakoによって定義された ECS 環境が立ち上がり、カート追加イベントを受け取って RecBole を使った RecVAE の学習が行われます
    • およそ 3時間程度で学習が完了します
  • 全アイテムに対するレコメンドリストを出力し、S3 に格納します
    • このとき、オフラインテストのメトリクス群も取得し、S3 に保存します
    • オフラインテストのメトリクスは社内ツールの Metrics Tracerというメトリクス監視ツールに取り込まれます。このツールを使って機械学習モデルに関する簡単な監視をしています
  • レコメンドリストをクックパッドマートの DB に取り込みます
  • 最終的に Backend サービスから API でレコメンドが配信され、アプリに表示されます

RecVAE (in RecBole) に変えてみて

オフライン・オンラインでの評価でいずれも良い成績を見せた RecVAE ですが、実際のコンバージョンにも大きく寄与しています。

変更後、それまでの水準から比較するとおよそ4,5倍ほどレコメンド経由でのカート追加数が増加しました。 その分学習にかかる時間や計算リソース、学習コードの複雑さは若干増しましたが、それを上回るメリットが得られたなと感じています。

今回のレコメンドでは期待されるものが Item2Vec ではなく RecVAE という結果になりましたが、「この商品に関連している商品もどうですか」などといった関連商品を表示する文言とともに表示される枠であれば Item2Vec もシチュエーションに適合して良い成績を残したと考えられます。

RecBole で実装したおかげで追加されるモデルとの比較実験を行う環境が簡単に用意できるので、今後もオフラインテストのメトリクスを監視しつつ新しいモデルをどんどん試していこうと思います。

まとめ

本記事ではクックパッドマートにおける item-to-item レコメンデーションの変遷の概要をお伝えさせていただきました。 様々な試行錯誤を経てクックパッドマートにおけるレコメンドの重要性は強くなっています。また、レコメンド以外にもクックパッドマートには様々な機械学習の技術が用いられており、日々進化しています。

この記事を読んでいただきありがとうございました。 機械学習の技術をプロダクトで活かしたい方がいらっしゃいましたら、ぜひ新卒・中途採用にご応募ください。

info.cookpad.com

VOICE 22 イベントレポート

$
0
0

こんにちは、ボイスサービス部の ymd (@y_am_a_da) です。 今回は私と、 Cookpad inc (UK) の Global CTO である miles (@tapster) が VOICE 22 に登壇をしてきたのでそのレポートです。

VOICE 22 とは

2018 年から開催されている世界的な対話型 AI *1に関するカンファレンスです。2021 年までで世界で 10 万人以上が参加しており、この界隈の中では非常に大きなカンファレンスであると言えます。 世界的な情勢もありここ数年はオンラインのみでの開催となっていましたが、今年は久しぶりにオンラインとオフライン混合での開催となっていました。

対話型 AI に関わるベンチャーの CEO や、音声ユーザーインターフェースのデザインに関わるデザイナー、対話型 AIを活用するサービスを開発するエンジニアなどが様々な観点から発表をしていました。

www.voicesummit.ai

クックパッドは海外展開にも注力しており、世界 74 ヶ国 32 言語で展開をしています。私の所属するボイスサービス部でも複数の国でサービスを展開しており、今回のイベントではそのサービスを運営して得られた知見をもとに音声対話型 AI やスマートスピーカーが料理のシーンをどのように変えられるのか、その可能性について "Introduce VUI to Make Everyday Cooking Fun"というタイトルで発表をいたしました。

ボイスサービス部の ymdCookpad inc (UK) の Global CTO である miles

登壇資料はこちらになります。

参加者について

今回登壇やブースを出展していた企業は大まかに 2 種類に分類することができました。

  • 対話型 AI を簡単に導入できるサービス
  • 音声コンテンツを生成するサービス

また、登壇者以外の来場者の層は、私達と同じようにスマートスピーカーや対話型 AI 向けにサービスを提供している開発者であったり、他にも対話型 AI のプラットフォーマー、対話的 AI の導入を検討している企業など様々でした。 全体的に、いわゆる開発者向けのカンファレンスではなく、もう少し幅広い対象を意識したイベントとなっていました。

登壇やブース出展していた企業を上記の分類ごとにもう少し詳しく紹介していきたいと思います。

対話型 AI を簡単に導入できるサービス

いわゆる Alexa や Google Assistant のような対話型 AI を導入できるサービスです。ここでの導入先は例えばコールセンターのオペレーター、レストランなどの予約受付システム、銀行の窓口業務などがあたります。 すなわち、人間が仕事でやっているコミュニケーションのうち、機械に任せられるところは機械に任せようというのを目標としたサービスです。

コミュニケーションにかかる人件費の削減や効率性を主に押し出していますが、それだけではなく、ユーザーとのコミュニケーションをデータとして可視化、分析できるようなダッシュボードを用意し、ユーザーとのコミュニケーションを改善するためのツールを提供しているようなところも多々ありました。 また、エンジニア向けの API の提供はしているものの、全体的に作り込まれたノーコードでの制作ツールを提供していることが多く、そのことからもエンジニアを抱えていない企業を強くターゲットとして意識している印象でした。

音声コンテンツを生成するサービス

いわゆる Text-to-Speech と呼ばれる技術を用いてテキストから発話データを生成するサービスです。ただ読み上げデータを作るだけではなく、多言語翻訳であったり声質の変換だったりにも対応しているサービスもありました。また、テキストだけではなく発話を入力にすることも可能なサービスもありました。

主な利用シーンとしては、 Podcast やスポーツの実況、動画コンテンツなどにつける注釈のようなものを想定しているようでした。例えば、ブログを文章で書けばそれをそのまま Podcast のコンテンツにも流用できる。というような具合です。 日本でも以前から類似のサービスはありますが、例えば Podcast のコンテンツを生成した音声で制作する発想はなかったのでそういったユースケースの話には新鮮さを感じました。

終わりに

今回登壇していた企業を見るとほとんどがいわゆるモバイルアプリケーションなどの延長線上ではなく、労働力の置き換え手段として音声アシスタントやその周辺技術を捉えている雰囲気を感じました。おそらくマーケットの大きさやマネタイズポイントのわかりやすさからそういったサービスが現状では大きく成長をしているのだと考えられます。自分自身はアプリケーションの延長線上として捉えている部分が大きかったため良い刺激になりました。

また、登壇者ではなく来場者の方には私達と同じように音声アシスタントプラットフォーム上に向けたサービスを開発している方も多く、ネットワーキングの時間で色々と情報を交換できてそちらも非常に有意義な時間となりました。

今回まとめた中には含まれませんでしたが、他にもいくつか面白い発表があったのでここでまとめて紹介します。

  • コロナ禍により対話型 AI の利用者数はかなり伸びたという話
    • 感染症予防の対策により非接触型のニーズが高まり、窓口に行って話をするのではなく、モバイルデバイスからチャットボットや音声アシスタントとやり取りをすることを好むユーザーが増えたとのこと
  • 車内でのインタラクティブな音声広告の話
    • いわゆる Voice Advertising と呼ばれるインタラクティブな音声広告がここ数年で広まりつつありますが、それを車載の音声アシスタントに搭載した。という内容でした。今どのあたりを走行しているのか認識することができるので、例えば近所のスーパーで起きているセールを提案する。などが事例として紹介されていました。まだまだ成長途中のマーケットですが、既存の広告媒体と比較して非常に高いエンゲージメントを誇っており、説明では既存のデジタル広告の平均 CTR は 0.6 % であるものの、自社の広告はエンゲージメントが平均 12% であると発表していました。 *2
  • Alexa を使った新たなデバイスなどの話
    • 主に Alexa Inovators と呼ばれる方々の開発している製品についていくつか紹介がされていました。特に Labrador という製品が面白かったです。 Alexa で操作ができる自律走行可能なワゴンカートなのですが、専用のトレイを利用して簡単な荷物の積み下ろしに対応しており、日本の家屋にマッチするサイズ感かはさておき、日々の家事が大変な人にはとても便利そうに見えました。 *3

弊社ではこのように色々な技術スタックを持ったエンジニアが数多く在籍しております。絶賛エンジニア募集しておりますのでご興味ありましたらぜひこちらのサイトをご覧ください。

info.cookpad.com

*1:音声アシスタントやチャットボットのようにテキストや発話での自然言語を使ってインタラクションが可能な AI を指しています。

*2:過去の記事ですが以下の記事にも書かれています techcrunch.com

*3:こちらに詳しいですwww.youtube.com

Swift Concurrencyでセマフォを作る

$
0
0

こんにちは、レシピサービス開発部と技術部兼務のヴァンサン(@vincentisambart)です。

Swift Concurrencyに関する中級の記事がまだ多くない気がしていたので、そういう記事を書くことにしました。

Swift Concurrencyの理解を深めたい人にはWWDC21の「Swift concurrency: Behind the scenes」がおすすめです。そのプレゼンの中でDispatchSemaphoreをSwift Concurrencyで使うべきではないと述べられました。

Preserving the runtime contract - Forward progress

Swift Concurrencyに提供されているツールを見ると、セマフォがありません。でも提供されたものでセマフォを作れないでしょうか?セマフォを使いたい場面が多いわけではありませんが、良い勉強になると思います。

どういうツールが標準で提供されているのでしょうか?safe(安全に使えるもの)はactorTaskTaskGroupasync letAsyncStreamCheckedContinuation、くらいですかね。

セマフォ自体の説明をすると長くなるので、下記はセマフォをある程度知っている前提で書いています。セマフォのメソッド名はセマフォの説明でたまに使われる分かりにくいPVではなく、DispatchSemaphoreも使っているwait()(待つ)とsignal()(合図を送る)を使います。

セマフォの値が0以下だったらwait()が次のsignal()まで待つ必要があるので、待たせる仕組み、もう待たなくて良いという合図を送る仕組み、が必要です。Swift Concurrencyのそれぞれのツールで実装できないか検討してみましょう。

actor

actor自体でできることを色々見ても、何かを待たせるすべはなさそうです(ビジーウェイトは論外)。

とはいえ、セマフォの状態を正しく保つには良いかもしれません。別のツールと合わせてなら役に立てるかもしれません。

TaskGroupasync let

async letのevolution proposalを見ると、TaskGroupと比較して紹介されていて、2つのユースケースが似ています:処理をいくつかの子タスクに分けて、最後に親タスクが結果をまとめます。async letは子タスクの数を動的に変えられないけどもっと使いやすい感じですかね。

特定のセマフォのwait()signal()を呼んでいるタスクが親子や兄弟であると限らないので、TaskGroupasync letをもっと詳しく調べなくても2つとも向いていなそうです。

AsyncStream

AsyncStreamの紹介事例は基本的に既存のSwift Concurrencyを使わないコードをSwift Concurrencyの世界に持ってきます。ゼロからSwift Concurrencyを使ってセマフォを作ろうとしているので、AsyncStreamは向いていないのでは?と思うかもしれませんが、もう少し見てみましょう。

  • AsyncStreamAsyncSequenceなので、非同期に値を順番に生み出します。次の値を入手するにはawaitを使う必要があるので、セマフォのwait()に近いかもしれません
  • AsyncStreamはクロージャーを渡して作成します:AsyncStream { continuation in ... }。クロージャーに渡されるAsyncStream.Continuationyield())がsignal()に少し似ているかもしれません

wait()signal()の実装に使えそうなものを見つけたので、本当に実装できるのかまずもう少しドキュメントを見てみましょう。

signal()に使えそうなAsyncSequence.Continution.yield()は並行で複数のタスクから呼んでも問題ないようです(この場合、値が取り出される順序が保証されませんが)。AsyncStream.init(_:bufferingPolicy:_:)のドキュメント)から抜粋:

The AsyncStream.Continuation received by the build closure is appropriate for use in concurrent contexts. It is thread safe to send and finish; all calls to the continuation are serialized. However, calling this from multiple concurrent contexts could result in out-of-order delivery.

ですが、残念ながら、wait()の実装はできないようです。並行で複数のタスクからAsyncStreamの次の値をawaitすることはできません。AsyncStream.Iteratorのドキュメントから抜粋:

This type doesn’t conform to Sendable. Don’t use it from multiple concurrent contexts. It is a programmer error to invoke next() from a concurrent context that constends with another such call, which results in a call to fatalError().

上記に書いてある時点で実際のコードで動いたとしても使うべきではありませんが、遊び感覚で試してみました。IteratorSendableでないためタスク間で共有できないが、並行で複数のタスクからvar iterator = stream.makeAsyncIterator(); await iterator.next()をしてみたら、記述の通りfatalError()が起きました。

_Concurrency/AsyncStreamBuffer.swift:253: Fatal error: attempt to await next() on more than one task

このため、今回のユースケースには向きません。とはいえ、AsyncStreamが既存のコードをSwift Concurrencyの世界に持っていくためだけのツールではないことを見られたと思います。

CheckedContinuation

CheckedContinuationとは

AsyncStream同様、CheckedContinuationは既存のコードをSwift Concurrencyの世界に持っていくためのツールとしてよく紹介されています。コールバックを使っているメソッドをawaitできるようにするためのツールですが、今回のユースケースで使えないでしょうか?

CheckedContinuationwithCheckedContinuation(function:_:))(エラーが発生することがあればwithCheckedThrowingContinuation(function:_:)))を使って作られます。

一番シンプルなユースケースは以下のようにhogeWithCallbackawaitできるようにすることです。

// `hogeWithCallback(_:)`を`hoge(_:)`と命名しても大丈夫です。funchogeWithCallback(_ callback: ()->Void) { ... }
funchoge() async {
    await withCheckedContinuation { (continuation:CheckedContinuation<Void, Never>) in
        hogeWithCallback {
            continuation.resume()
        }
    }
}

非同期のタスクがawait withCheckedContinuationに止まって、continuationを渡されたクロージャーが実行されて、hogeWithCallbackが呼ばれます。CheckedContinuationresumeメソッドが呼ばれたらawait withCheckedContinuationに止まっていたタスクが再開します。

セマフォはwait()側でawait withCheckedContinuationをして、signal()側でcontinuation.resume()を呼べたら実装できるかもしれません。同時に複数のタスクが待つ可能性があるので、1つのCheckedContinuationでは足りませんが、CheckedContinuationの配列を使えば良いでしょう。並行でさまざまなタスクからアクセスされても、セマフォの内部状態である配列と値の正当性を保つにはactorが向いていそうです。

セマフォの内部状態

では実装してみましょう。状態はとりあえず上記の説明にあった待機中のCheckedContinuationの配列とセマフォの値が必要です。

actor ContinuationSemaphore {
    // 待機中のタスクの`CheckedContinuation`です。// この`Void`はこの`CheckedContinuation`が値を返さないことを示します。// この`Never`はこの`CheckedContinuation`ではエラーが発生しないことを示します。privatevarwaiters:[CheckedContinuation<Void, Never>]= []
    // セマフォの値です。// `value`が負であれば、`waiters.count == -value`が保証されます。privatevarvalue:Intinit(value:Int) {
        // `DispatchSemaphore`同様、初期値が負であってはいけません。
        assert(value >=0)
        self.value = value
    }

    // セマフォの正当性が保たれているのを確認するメソッドです。privatefuncensureValidState() {
        // セマフォの値が正であれば、待つタスクがあるべきではありません。// セマフォの値が負であれば、待つタスクの数が`-value`個であるべきです。
        assert((value >=0&& waiters.isEmpty) || (waiters.count ==-value))
    }

wait()

一番複雑なのがwait()の実装です。

コード自体が長いわけでもなく、シンプルにも見えますが、特別なことが起きています。

funcwait() async {
        value -=1// 引き算後に値が0以上だったら待つ必要がありません。if value <0 {
            await withCheckedContinuation { continuation in
                waiters.append(continuation)
                ensureValidState()
            }
        }
        ensureValidState()
    }

この実装は本当に大丈夫でしょうか?

awaitを使うたびに制御フローが別のタスクに移る可能性があります。wait()が呼ばれる時にvalue0で、waitersが空っぽだったのを仮定します。1を引かれたvalue-1になって、await withCheckedContinuationが呼ばれます。ここで制御フローが別のタスクに移るとしたら、別のタスクがセマフォを使えば、value-1なのにwaitersが空っぽなので不正状態ですよね…

また、withCheckedContinuationasync関数なのに、渡されるクロージャーが直接actorのプロパティwaitersを変更できているのは僕にとって少し不思議でした。

actor上でのクロージャーの制限を洗い出してみましょう。

actor上でのクロージャーの制限

特定のactorにとって、メソッドや関数はそのactorのisolatedな(孤立した)環境に属するかどうかで区別されます。このactorのisolatedな環境に属するメソッドや関数は以下の通りです:

  • actorの一部として実装されていて、nonisolatedでないもの swift actor MyActor { func anyMethod() {} }
  • actor外で実装されているが、actorを引数で受け取って、この引数にisolatedが明記されているもの swift actor MyActor {} func someFunction(myActor: isolated MyActor) {}

逆にactorのisolatedな環境に属さないメソッドや関数は以下の通りです:

  • actorの一部として実装されているがnonisolatedであるもの swift actor MyActor { nonisolated func anyMethod() {} }
  • actor外で実装されているが、このactorをisolated引数で受け取っていないもの swift actor MyActor {} func someFunction() {}

今回気になっているwithCheckedContinuationが特定のactorのisolatedな環境にも属しませんし、このwithCheckedContinuationがactorのisolatedな環境に属するメソッドから呼ばれてクロージャーを渡されるので、このユースケースに焦点を当てましょう。

現状SwiftのConcurrencyチェックがデフォルトで緩いので、Strict Concurrency Checkingを一番厳しい設定「Complete」にして色々試してみましょう。

Swift Compiler - Language - Strict Concurrency Checking

ひとまずは一番シンプルなケース、呼ばれる関数が普通である(asyncでない)場合を見てみましょう。

funcnormalFunctionTakingClosure(block: ()->Void) {}
actor MyActor {
    varvalue:Int=0funcanyMethod() async {
        normalFunctionTakingClosure { value =1 }
    }
}

上記のコードでself.を明記せずにactorのプロパティをそのままアクセスしても何の警告が出ません。呼ばれる関数がasyncでないので、actorのisolatedな環境で実行されるので問題が起きる心配はありません。

async関数で試してみましょう。

funcasyncFunctionTakingClosure(block: ()->Void) async {}
actor MyActor {
    varvalue:Int=0funcanyMethod() async {
        await asyncFunctionTakingClosure { value =1 }
    }
}

上記のコードをビルドすると以下の警告が出ます。actorのisolatedな環境に属さないasyncメソッドや関数は別の環境で実行されます。() -> Voidは環境の境界線を渡れないと。(blockの型を() async -> Voidにしたとして同じです)

Non-sendable type () -> Void exiting actor-isolated context in call to non-isolated global function asyncFunctionTakingClosure(block:) cannot cross actor boundary

実行環境間、タスク間にクロージャーを送りたい場合、@Sendableをつける必要があるので試してみましょう。

funcasyncFunctionTakingClosure(block:@Sendable ()->Void) async {}
actor MyActor {
    varvalue:Int=0funcanyMethod() async {
        await asyncFunctionTakingClosure { value =1 }
    }
}

上記のコードをビルドしたら以下のエラーになってしまいました。

Actor-isolated property value can not be mutated from a Sendable closure

actorはisolatedな環境で実行されるものなので、プロパティを別の環境からアクセスできたらisolatedでなくなります。

withCheckedContinuationは上記のasyncFunctionTakingClosureに似ていそうなのに、警告なくクロージャーからactorのプロパティにアクセスできます。

withCheckedContinuationの定義を見てみると、クロージャーの型に@Sendableがついていませんし、不思議な@_unsafeInheritExecutorというのがついています。

この@_unsafeInheritExecutorの影響でwithCheckedContinuationが特別な振る舞いをします。async関数ではあるものの、actorから呼んでも実行環境(executor)が継承されます。渡されるクロージャーが@Sendableでもなく@escapingでもなく同じ実行環境のまま実行されます。もっと詳しい説明はこちらをご覧ください

注意:unsafeのついたものはリリースされるコードで使う場合、注意が必要です。UnsafeContinuationを利用することはできますが、安全なCheckedContinuationを使うべきです。@_unsafeInheritExecutorが分かりにくく、特別な振る舞いをするので一般的な開発において使う必要が出ることはないと思います。

好奇心を満たすためだけに試してみたら予想通り以下のコードで何の警告も出ません。

@_unsafeInheritExecutorfunc asyncFunctionInheritExecutorTakingNonEscapingClosure(block: ()->Void) async {}
actor MyActor {
    varvalue:Int=0funcanyMethod() async {
        await asyncFunctionInheritExecutorTakingNonEscapingClosure { value =1 }
    }
}

wait()再び

余談はここまでにして、ContinuationSemaphorewait()に戻りましょう。

funcwait() async {
        value -=1// 引き算後に値が0以上だったら待つ必要がありません。if value <0 {
            await withCheckedContinuation { continuation in
                waiters.append(continuation)
                ensureValidState()
            }
        }
        ensureValidState()
    }

「Strict Concurrency Checking」の厳しさを最大のCompleteにしても、上記のコードで警告が出ません。

(正確にはXcode 14.0.1でmacOS用にビルドすると出ますが、iOS用だと出ないし、Xcode 14.1だとmacOS用でも出ません。警告が出るのはバグで間違いなさそうです)。

改めてwait()が呼ばれる時にvalue0で、waitersが空っぽだったのを仮定して実行順を追ってみます。

  1. 実行環境がactorの環境に切り替わります。
  2. actorの実行環境上でvalue -= 1if value < 0 {が実行されます。
  3. withCheckedContinuationがactorに属さないとはいえ、@_unsafeInheritExecutorがついているので実行環境(executor)が継承され、withCheckedContinuationが実行されます。
  4. withCheckedContinuationの内部の動きが複雑ですが、簡単にまとめてみると、continuationが作成されて、actorの実行環境のままクロージャーが呼ばれるようです。
  5. value -= 1waiters.append(continuation)の間actorの実行環境を離れていないので、正当性が保たれるはずです。

signal()

signal()は割とシンプルです。待っているものがあれば、一番前から待っていたcontinuationを取り出してresume()を呼びます。

funcsignal() {
        value +=1if value <=0 {
            // 配列が空っぽの場合、`removeFirst()`によって異常終了されるが、// この`if`の条件が満たされていて`signal()`が呼ばれた時点ではvalueの値が-1以下のはずなので、// 待機中のタスクが1つ以上だったはずです。letwaiter= waiters.removeFirst()
            // `waiters`から1つの`CheckedContinuation`を取り出して、正しい状態が保たれるはずです。
            ensureValidState()
            // 待っていたタスクが続行できます。
            waiter.resume()
        }
        ensureValidState()
    }
}

全体のコード

上記にContinuationSemaphoreの全てのコードを3つに分けて載せましたが、念のためまとめて載せます。意外と短いですね(正当性の確認を消すとなおさら)。

actor ContinuationSemaphore {
    privatevarwaiters:[CheckedContinuation<Void, Never>]= []
    privatevarvalue:Intinit(value:Int) {
        assert(value >=0)
        self.value = value
    }

    privatefuncensureValidState() {
        assert((value >=0&& waiters.isEmpty) || (waiters.count ==-value))
    }

    funcwait() async {
        value -=1if value <0 {
            await withCheckedContinuation { continuation in
                waiters.append(continuation)
                ensureValidState()
            }
        }
        ensureValidState()
    }

    funcsignal() {
        value +=1if value <=0 {
            letwaiter= waiters.removeFirst()
            ensureValidState()
            waiter.resume()
        }
        ensureValidState()
    }
}

注意事項

上記のコードは少し試したし、理解を深めるには良い例だと思いますが、そのままプロダクションで使うのをおすすめできません。あまりテストされていないことを除いても、以下の問題が未解決状態です。

  • ContinuationSemaphoreが解放される時、waitersが残っていれば何をすべきでしょうか?
    • DispatchSemaphoreは解放時のvalueが作成時に渡されたvalueより低かったら強制終了です。
  • タスクのキャンセルをどう扱うべきでしょうか?
  • 提供されている機能が限られています。wait()にタイムアウトを指定できるようにするとか、signal()に数字を渡せるようにするとか、多くのセマフォの実装に入っているけどここにはないさまざまな機能があります。

キャンセルを扱う場合、注意が必要です。wait()を呼んでいるコードがキャンセルを意識しなければ、キャンセルの影響でwait()が終わるとき、セマフォに守られているリソースが使えるのを勘違いしていれば困ります。Taskがキャンセルされるときにwait()throwするようにした方が良いかもしれません。

Task(ボーナスコンテンツ)

さまざまなツールを見てみましたが、Taskの話はまだしていませんでしたね。Taskは特定なタスクが終わるのを待つことはできます(await task.value)が、そのTask自体を止める方法がなさそうです(ビジーウェイトはもちろん論外)。

元々Taskに関してはこの数行だけで留まるつもりでしたが、Taskのドキュメントを改めて見てよく考えたら、ビジーウェイトには少し似ている邪道な方法を思いつきました。実行されたTaskをキャンセルできる機能とsleepさせる機能を利用すれば…

上記のContinuationSemaphoreCheckedContinuationの使い方が想定されたものだと思います。以下のTaskSemaphoreがそうでもありませんし、僕の気づいていない問題が隠れているかもしれません。こういう使い方をおすすめできないとはいえ、勉強になり得るので、とりあえず興味がある方のために載せてみます。全体の構成がCheckedContinuationを使ったバージョンに近いので、説明は少なめです。

actor TaskSemaphore {
    privatevarvalue:Intprivatevartasks:[Task<Void, Never>]= []

    init(value:Int) {
        assert(value >=0)
        self.value = value
    }

    privatefuncensureValidState() {
        assert((value >=0&& tasks.isEmpty) || (tasks.count ==-value))
    }

    funcwait() async {
        value -=1if value <0 {
            lettask= Task {
                // キャンセルされない限り永遠にsleepさせますwhile!Task.isCancelled {
                    // タスクがキャンセルされたら、途中だったsleepがすぐ終わるはずです。try? await Task.sleep(nanoseconds:100_000_000_000/* 適当に100秒 */)
                }
            }
            tasks.append(task)
            ensureValidState()
            await task.value
        }
    }

    funcsignal() {
        value +=1if value <=0 {
            lettask= tasks.removeFirst()
            ensureValidState()
            task.cancel() // 永遠にsleepさせているタスクをキャンセルすることで起こします。
        }
        ensureValidState()
    }
}

最後に

この記事からひとつだけ覚えるとしたら、既存のコードをSwift Concurrencyの世界から使えるためにあると紹介されているツールは他のユースケースでも利用できる場合があるということでしょう。CheckedContinuationAsyncStreamも最初からSwift Concurrencyを使っているコードでも役に立つ場面があります。

また、すべてのツールを洗い出して、セマフォを実装できましたが、現在存在する標準ライブラリのツールだけで実装できないものもあると思います。

この記事を読んでくれた皆さんがSwift Concurrencyに少しでも詳しくなっていたら嬉しく思います。


Cookpad TechConf 2022 をパシフィコ横浜ノースで物理開催します!

$
0
0

こんにちは、CTO室の緑川です。 公式サイトでも連絡させていただきましたが、ちょうど1ヶ月後の11月25日(金)に技術カンファレンス『Cookpad TechConf 2022』をパシフィコ横浜ノースで開催します。カンファレンスではクックパッドのエンジニアやデザイナーをはじめ、多くの社員が日頃の業務と研究の末築き上げた技術的知見や経験を発表する予定です。このエントリーでは当日の見どころやトーク以外の企画について紹介いたします。

Cookpad TechConf 2022の概要

  • 名称:Cookpad TechConf 2022
  • 日時:2022年11月25日(金)13:00 - 18:00
  • 定員:200名(完全招待制)
  • 会場:パシフィコ横浜ノース 4F (〒220-0012 神奈川県横浜市西区みなとみらい1-1-2
  • トーク数:全10本(基調講演2本 + メイントーク8本)
  • 公式サイト:https://techconf.cookpad.com/2022/
  • ハッシュタグ:#CookpadTechConf

Cookpad TechConfとは

クックパッドでは「毎日の料理を楽しみにする」というミッションを掲げ、世界中における食と料理の課題をテクノロジーで解決するためにさまざまなプロジェクトに挑戦しています。 Cookpad TechConf はクックパッドの社員が課題に取り組む中で、どのようにサービスを生み出し、どのようにシステム開発をしているのか等、日頃の挑戦の中から得た技術的知見について発表します。

また、今回のCookpad TechConfでは新型コロナウイルス感染症への対策を万全にするために、完全招待制のカンファレンスとさせていただいています。

トークの紹介

トーク数はJapan CEOやCTOとデザイナー統括マネージャーによる基調講演2本とメイントーク8本の合計10本を予定しています。 レシピサービスの改善や機能の追加、新規事業クックパッドマートの取り組みや物流の最適化など、さまざまなトピックについて触れていきます。

また、今回のCookpad TechConfでは『ペア登壇』という仕組みを取り入れました。これはエンジニアとエンジニア、デザイナーとデザイナーというペアに加え、エンジニアとデザイナーといった職種を超えたペアも登壇予定です。1つのプロジェクトに対してエンジニアの視点とデザイナーの視点の両方から知見を得ることができます。

レシピサービスのプロダクト開発プロセスについて

登壇者:大石 英介 & 島 朋代
時間:13:30 - 13:50

巨大なレシピサービスのアーキテクチャを最高にしたい

登壇者:宮崎 広夢
時間:13:55 - 14:15

デザインシステムを使ってプロダクトのデザイン負債を解消する

登壇者:村山 賢太 & 見上 香桜里
時間:14:30 - 14:50

クックパッドマートが実現する新しい生鮮食品流通プラットフォーム

登壇者:勝間 亮 & 米田 哲丈
時間:14:55 - 15:15

生鮮食品をユーザーに届ける流通の仕組みと技術

登壇者:長 俊祐
時間:15:20 - 15:40

新規事業クックパッドマートを支える機械学習の技術

登壇者:深澤 祐援 & 山口 泰弘
時間:15:55 - 16:15

クックパッドが挑戦する「レシピ」と「かいもの」をつなぐ新しいサービス作り ~ 役割にとらわれず新しい価値に向き合い続けるチーム ~

登壇者:谷口 浩司 & 新妻 里夏
時間:16:20 - 16:40

Go Global Search 2

登壇者:Orgil Davaajargal
時間:16:45 - 17:05

その他の企画

Cookpad TechConf 2022ではメイントーク以外にもさまざまな企画を実施する予定です。簡単に紹介させていただきます。

Ask The Speaker

会場ではトーク後に登壇者へ質問できるAsk The Speakerを設けます。登壇者と物理開催ならではのコミュニケーションが行えますので、この機会に講演内容だけでなくさまざまなことをご質問ください。

ポスター展示

クックパッド内で使用している技術や事業に関するポスターを展示します。ポスター展示を行う社員からの概要としては以下の通りです。

  • 料理という事業ドメインならではの検索課題や、それらの課題を解決するシステムの全体像
  • Komerco のアーキテクチャや利用技術についての解説(Firebase の使い方など)
  • おりょうりえほんのサービス紹介ポスター
  • クックパッドマートで買える食材を、社内のメンバーがどのように楽しんでいるかを紹介します!
  • 意外と失敗する BtoBtoC プラットフォーム開発

LT(ライトニングトーク)

メイントークの後は約5分ほどのLTをメイン会場で行います。メイントークでは話せなかった奥が深い知見や特別な経験などLTならではの発表を行う予定です。

ライブコーディング

メイン会場とは別の会場で、エンジニアやデザイナーによるライブコーディングやライブデザインを行います。クックパッド社員による迫力満点の技術を体験できる貴重なイベントです。

懇親会

ご参加いただいた皆様と是非懇親会を行えればと思います。クロージングの基調講演終了後、会場からバスを手配し、野毛というエリアで懇親会を行う予定です。

Discordへの招待

11月に今回のイベントに向けたDiscordをオープンします。このDiscordでは社員とコミュニケーションができるほか、クックパッドが行う部活動の紹介や公開ミーティングなどのさまざまなイベントを行う予定です。クックパッドの文化や雰囲気を一緒に楽しんでいたければと思います。

まとめ

Cookpad TechConfは2019年以来の開催となります。交流会や勉強会など多くのイベントがオンライン開催となっている中での久しぶりの物理開催です。この2年半以上の時間で、クックパッドは組織全体で技術力の向上や新規プロジェクトへ果敢に取り組んできました。この成果をぜひ皆さまと共有できる場にできればと思います。


安全に開催できることを第一とし、その上で皆さまと楽しめるカンファレンスにできるよう万全な体制でお待ちしております!

最後に宣伝にはなりますが、クックパッドで働きたいエンジニアの方を募集しております。ぜひクックパッドのウェブサイトからご連絡ください!

info.cookpad.com

Android クックパッドアプリの画面遷移実装

$
0
0

Androidエンジニアのこやまカニ大好きです。

10/19 に弊社で開催した After Party DroidKaigi 2022というイベントで、クックパッドアプリの画面遷移について発表しました。 当日のセッションでは時間が限られていたりスライドでのコード表示の制約から実装面の説明をかなり省略していたので、この記事で補足しつつ説明しようと思います。

クックパッドアプリの構成

実装の内容に入る前に、前提としてクックパッドアプリのプロジェクト構造を説明していきます。

モジュール構成

これまでの技術ブログでも何度か説明してきた通り、クックパッドアプリは2018年ごろからマルチモジュールプロジェクトになっています。

  • 画面実装は View(Compose)と関連ロジックを Scene という単位で構成
  • 関連のある複数 Scene で機能モジュール(feature module) を構成
  • 共通ロジックやドメイン層は役割ごとにモジュール分割(library module)

全体的なモジュール構造は以下のようになっています。 feature module から library module への参照はありますが、feature module 同士に参照はありません。 (この図は過去の技術記事から持ってきたため、 legacy モジュールが重要そうな感じで見えていますが今回の内容には関係ありません)

画面遷移実装

クックパッドアプリでは以下の理由から Navigation Component を利用していません。

  • Navigation Component が出る前からマルチモジュール化されていて登場当時導入が難しかった
  • クックパッドアプリの画面遷移が複雑で Navigation Component を利用できる箇所が限られていた
  • 特にボトムタブに Multiple Back Stacks 相当の実装を自前で入れていたので乗り換えが難しい状況だった

今回紹介するような実装の工夫によってクックパッドアプリの画面遷移処理が簡単になったこと、Navigation Component 自体が進歩してきていることから今後はクックパッドアプリでもNavigation Component の採用を検討できる状態になってきましたが、今回の発表時点では Navigation Component 関連の内容はありません。

クックパッドアプリの画面遷移実装

モジュール間画面遷移の基本

例として、 A module の画面A から B module の画面Bに遷移する場合を考えます。 この時、素直に 画面A から 画面B の Fragment を生成しようとすると A module から B module を参照する形になります。 もし、画面Aと画面Bが相互に行き来できる場合はどうでしょうか。 A module と B module は相互参照になってしまうため、モジュール間の依存関係を設定できなくなります。

クックパッドアプリでは、別モジュールの画面を生成するために navigation module に AppDestinationFactoryというインターフェイスを定義し、全ての画面は AppDestinationFactoryを経由して遷移先の画面インスタンス(Fragment,Intent)を生成するようにしています。 この構造だと各 feature module は navigation module への参照を持つだけで画面遷移を実装できます。 AppDestinationFactoryの実体は全てのモジュールへの参照を持つ、 application module で定義しています。

この構造はクックパッドアプリ特有というわけではなく、マルチモジュールプロジェクトで画面遷移を navigation component を使わずに実装しているアプリはほとんどが類似の方法で画面遷移を実装していると思います。

ボトムタブごとに画面遷移の履歴(Back Stacks) を切り替えられるようにする仕組み

クックパッドアプリはボトムタブで大まかな機能を切り替える設計になっています。 このボトムタブを切り替えた際、タブごとに画面遷移の履歴(Back Stacks)を保存して切り替える実装を入れています。 最近の FragmentManager、 Navigation Component では Multiple Back Stacksという名前で類似の実装が含まれていますが、クックパッドアプリでは PrimaryNavigationFragmentという仕組みを使い、数年前から独自に実装しています。

PrimaryNavigationFragmentの説明はかなり難しいのですが、大体以下のような仕組みだと考えてください。

  • Activity と画面 Fragment の間に存在する FragmentManager制御のための Fragment
    • NavHostFragmentのようなもの…というか NavHostFragmentPrimaryNavigationFragmentを使って実装されている
  • ActivitysupportFragmentManagerを使う代わりに PrimaryNavigationFragmentchildFragmentManagerを利用して画面遷移を実装することで back などの挙動はそのままに backstack の管理ができるレイヤーを作れる
  • タブ切り替え時に PrimaryNavigationFragmentを attach/detach して切り替えることで PrimaryNavigationFragmentchildFragmentManagerが持つ backstack ごとタブ表示を切り替えられる

全然わかりませんね。 ものすごく大雑把にいうと、 requireActivity().supportFragmentManagerの代わりに requireActivity().supportFragmentManager.let { it.primaryNavigationFragment?.childFragmentManager ?: it }を使って画面遷移するといい感じにできるということです。 この実装を作った時に個人ブログに説明記事を書いたので、もし興味がある人がいれば参考にしてみてください。

画面遷移の処理で毎回 requireActivity().supportFragmentManager.let { it.primaryNavigationFragment?.childFragmentManager ?: it }という記述を書いていられないので、 FragmentManagerを適切に選択してくれる NavigationControllerという仕組みを導入しました。

NavigationControllerはアプリ内の画面遷移における共通処理を吸収するレイヤーで、以下のような機能を持っています。

  • 処理対象の FragmentManagerの選択
  • replace 先の containerViewId 指定
  • backstack の管理
class NavigationController internalconstructor(  
    val context: Context,  
    val fragmentManager: FragmentManager,  
    val childFragmentManager: FragmentManager,  
    privateval activityResultCaller: ActivityResultCaller,  
) : ActivityResultCaller by activityResultCaller,  
    FragmentResultOwner by fragmentManager {
    ...

    @JvmOverloadsfun navigateFragment(  
        fragment: Fragment,  
        fragmentTransition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN
    ) {  
        fragmentManager.commit {  
            replace(DEFAULT_CONTAINER_ID, fragment, null)  
            setTransition(fragmentTransition)  
            addToBackStack(null)  
        }  
    }

    ...
}

privateval FragmentManager.primaryNavigationFragmentManager: FragmentManager  
    get() {  
        val fragment = primaryNavigationFragment  
        returnwhen (fragment?.isAdded) {  
            true-> fragment.childFragmentManager  
            else->this  
        }  
    }  
  
privateval FragmentActivity.primaryNavigationFragmentManager: FragmentManager  
    get() = supportFragmentManager.primaryNavigationFragmentManager  
  
val Fragment.navigationController: NavigationController  
    get() {
        return NavigationController(  
            context = requireContext(),  
            fragmentManager = requireActivity().primaryNavigationFragmentManager,  
            childFragmentManager = childFragmentManager,  
            activityResultCaller = this,
        )  
    }  
val FragmentActivity.navigationController: NavigationController  
    get() {  
        return NavigationController(  
            context = this,  
            fragmentManager = primaryNavigationFragmentManager,  
            childFragmentManager = primaryNavigationFragmentManager,  
            activityResultCaller = this,
        )  
    }

NavigationController を利用する前の画面処理は大体以下のような実装でした。

val destinationFragment = destinationFactory.createFragment()
val fragmentManager = fragment.requireActivity().supportFragmentManager.let { it.primaryNavigationFragment?.childFragmentManager ?: it }  
fragmentManager.commit {  
    replace(containerId, fragment, null)  
}

NavigationController を利用することで以下のようにシンプルな処理にできます。

val destinationFragment = fragmentFactory.createFragment()
fragment.navigationController.navigateFragment(destinationFragment)

NavigationController により、画面遷移がよりシンプルに実装できるようになったことがわかると思います。

条件によって遷移先が Fragment だったり Activity だったりする画面遷移の実装

クックパッドアプリにはユーザー状態などによって遷移先が変わるアクションが存在します。 極端な例ではログイン済みの場合は目的の画面(Fragment)、未ログインの場合はログイン画面(Activity)という複雑なパターンもあります。 この遷移先切り替え問題の解決のため、 Destinationという仕組みを導入しました。

Destinationは遷移先の実体を隠蔽するための仕組みで、以下のような特徴を持ちます。

  • sealed interface である DestinationFragment, Intentをラップする実体クラスという構成
  • NavigationControllerDestinationへの 画面遷移をサポートすることで各画面では遷移先が Fragmentなのか Activityなのか気にせず実装できる
    • Result APIなどで処理結果を受け取るケースでは考慮する必要がある

Destination の中身

sealedclass Destination {  
    class FragmentDestination internalconstructor(val fragment: Fragment) : Destination()  
    class DialogFragmentDestination internalconstructor(val dialogFragment: DialogFragment) : Destination()  
    class ActivityDestination internalconstructor(val intent: Intent) : Destination()  
}  
  
fun Fragment.toDestination(): Destination =  
    when (this) {  
        is DialogFragment -> Destination.DialogFragmentDestination(this)  
        else-> Destination.FragmentDestination(this)  
    }  
  
fun Intent.toDestination(): Destination = Destination.ActivityDestination(this)

NavigationController では Destination の実装クラスごとに navigation 処理の呼び分けをしているだけです。

class NavigationController internalconstructor(
    ...
    @JvmOverloadsfun navigate(destination: Destination) {  
        when (destination) {  
            is Destination.FragmentDestination -> navigateFragment(destination.fragment)  
            is Destination.DialogFragmentDestination -> navigateDialogFragment(destination.dialogFragment)  
            is Destination.ActivityDestination -> navigateActivity(destination.intent)  
        }  
    }
    ...
}

このように、 NavigationControllerDestinationの導入により各画面は遷移先の画面が Fragmentなのか Activityなのか気にせず画面遷移処理を書けるように成りました。

URL から画面遷移する処理を実装する

URL から画面遷移する処理というのは、いわゆるディープリンク処理と呼ばれているものです。 URLによって遷移先は Fragmentだったり Activityだったりする他、外部からのアプリ起動、WebView内での遷移、APIレスポンスからのアクションなど様々な箇所で同じ挙動を実現したいという要求があります。 この要求を解決するため、 DestinationParamsという仕組みを導入しました。

  • DestinaionParamsDestinationを生成するための情報をまとめた sealed interface
  • URLから遷移先(Destination)の種別と必要なパラメータを抽出し、パラメータとして保持する
  • Destination実装は sealed interface なため、when による分岐処理でパターン網羅しやすい

DestinationParams の実装

DestinationParamsの定義は基本となる DestinationParams sealed interface と、それを実装する各 data class 、 object から成ります。

sealedinterface DestinationParams : Parcelable {
    @Parcelizedataclass Web(val url: String) : DestinationParams
    
    @Parcelizedataclass RecipeDetail(val recipeId: Long) : DestinationParams

    @Parcelizeobject MyKitchen : DestinationParams

    ...(似た実装がいっぱいある)
}

DestinationParams の利用方法

Uri.toDestinationParams()という拡張関数を定義し、その中でURLを解析して適切な DestinationParams (または null)に変換する処理を書いています。 この仕組みにより、ほとんどのケースで以下のような実装だけでディープリンクが動作するようになりました。

// URI から DestinationParams を生成val destinationParams = uri.toDestinationParams(serverSettings) ?:return// DestinationParams から Destination を生成val destination = appDestinationFactory.createDestination(context, destinationParams) ?:return// 画面遷移処理
fragment.navigationController.navigate(destination)

URI ->DestinationParamsの変換、 DestinationParams ->Destinationの変換が別処理になっているため、それぞれのレイヤーで必要なテストが書けるようになっているのが特徴です。 この仕組みの導入により、クックパッドアプリでディープリンクとして扱うURLは URL 〜 遷移対象の画面の生成まですべてテストが書かれている状態にできました。

DestinationParams の機能ごとのタグづけ

Kotlin ではひとつのクラス/オブジェクトが複数の sealed interface を継承することができます。 この仕組みを使い、各 DestinationParamsの実装クラスがサポートする機能に対応する sealed interface を作って継承するようにしています。

以下の例では、 RecipeDetailという DestinationParamsが外部からのディープリンクによるアプリ起動とアプリ内でのディープリンクによる画面遷移、WebView内でのディープリンクによる画面遷移をサポートしていることがわかります。

このサブ sealed interface によるタグ付の導入により、アプリの起動処理やURIから DestinationParamsの変換処理で何もしない when 分岐の数が減り、処理やテストが書きやすくなりました。

/**   * アプリ起動をサポートしている DestinationParams */sealedinterface AppLaunchSupportedDestinationParams : DestinationParams  
  
/**   * DeepLink からの変換をサポートしている DestinationParams */sealedinterface DeepLinkSupportedDestinationParams : DestinationParams  
 
/**   * WebView 内でのハンドリングをサポートしている DestinationParams */sealedinterface WebViewSupportedDestinationParams : DestinationParams  
  
sealedinterface DestinationParams : Parcelable {
    @Parcelizedataclass RecipeDetail(val recipeId: Long) :  
        DestinationParams,  
        AppLaunchSupportedDestinationParams,  
        DeepLinkSupportedDestinationParams,
        WebViewSupportedDestinationParams

...
}

DestinationParams による WebView の改善

2021年の DroidKaigi で発表した時点では DestinationParamsは存在していませんでした。 そのため、 各WebView画面ではディープリンクからネイティブ画面の遷移は遷移先画面ごとに navigateXXX()のようなメソッドを個別に実装していました。 今年に入って DestinationParamsが導入されたことにより、各 WebView 画面は WebViewSupportedDestinationParamsを処理するメソッドだけ実装すれば良いようになりました。 以下の画像は WebView のディープリンク処理に WebViewSupportedDestinationParamsを導入した時のPRの一部です。 大量のメソッドが消え、 WebViewSupportedDestinationParamsを処理するメソッドに集約されているのがわかると思います。 WebViewSupportedDestinationParamsも sealed interface なので、 when する処理を各 WebView 画面に書いておくだけで WebViewSupportedDestinationParamsのパターンが増えた場合でも自動的にビルドエラーになってくれます。賢いですね。

まとめ

クックパッドアプリでは画面遷移処理を自作しており、画面遷移の高度な要求に対応するために様々な工夫を凝らしてきました。 最近の変更で画面遷移処理の抽象化が進み、アプリ起動時の処理やWebViewが圧倒的に開発しやすくなりました。 一方、今後の課題としては以下のような項目があると考えています。

  • Jetpack Navigation Component を利用していないこと
  • Navigation Component 事態は必須ではないと感じているが、長期的なメンテナンスコストを考えると公式が推している仕組みに近づけた方が良さそう
  • Navigation Component のマルチモジュールサポートはまだ改善が必要だと考えているので、しばらくは自作した仕組みを Navigation Component に寄せやすくする改良をしていくかも

クックパッドではユーザー体験を最高にするため、モバイルアプリの画面遷移を最高の状態にしていくために引き続き改善を続けていく予定です。 最高の画面遷移処理や最高の WebView に興味がある人はぜひお気軽にご連絡ください。

info.cookpad.com

Kaigi on Rails 2022 にて『森羅万象に「いいね」するためのデータ構造』の発表をしました

$
0
0

メディアプロダクト開発部で Rails を書いているなどやま ( @pndcat ) です。業務では、広告基盤の開発、運用から新規サービス開発など、マーケティングソリューション領域に関する開発をしています。趣味は、スプラトゥーンと推し活動をがんばっています。

Kaigi on Rails 2022にて、森羅万象に「いいね」するためのデータ構造というタイトルで発表をしました。この記事では、Twitter などで寄せられた質問に回答したいと思います。発表中にあった「いいね」のリファクタリングについてのさらなる詳細については、後日公開する予定です。

After Kaigi on Rails イベントの開催のお知らせ

本日 (10/31) の19時から Cookpad Lounge #15 After Kaigi on Rails を開催します!イベントでは、Kaigi on Rails に登壇した3人の振り返りや、発表では話せなかったことにも触れます。また、惜しくもプロポーザルが落ちてしまった社員による発表もありますので、ぜひ遊びに来てください!(アーカイブもあるのでよろしくお願いします)

cookpad.connpass.com

資料

speakerdeck.com

動画

www.youtube.com

Q&A

以下、今回の発表に関して Twitter などで寄せられた質問への回答です。いろいろな感想や質問があり、とても嬉しかったです。ありがとうございました!

リファクタリングはどうやって進めたのか?リファクタリングの工数はどうしているのか?

メーカーズタウン(今回の発表の題材となったサービスです)の開発チームでは、週に1回、モブプロをする日を作っています。 モブプロには、開発メンバー4人に加え、えにしテック@darashiさんと、@cafedomancerさんにも参加していただき、6人で行っています。 モブプロでは、

  • リリース後に真の仕様がわかったため、より良いデータ構造に変更したい
  • リファクタリングをしたくても、コードが難しい上に、テストもないが、どうにかしたい
  • リリースが迫っているタスクをモブプロで行い、マージまでの時間を短くしたい

など、いろいろなテーマに取り組んでいます。
「いいね」テーブルの種類が増えていき、そろそろ実装が辛くなってきたという段階でリファクタリングのモブプロを行いました。

「いいね」のモブプロでは、現状の仕様や振る舞いの理解を深め、リファクタリングの戦略を検討し、実際に1種類の Like / AnonymousLike を移行しました。そこまでをモブプロで行い、その他の「いいね」は普段の開発をしながら移行していきました。

大規模なリファクタリングは、一人でコツコツ取り組むのは難しいものです(気持ち的にも、正しいリファクタリングができているのかという点においても)。モブプロにおいて議論しつつ移行戦略を確立させたことで普段の開発をしながら移行を進めることができたため、大きな問題もなく移行することができました。

匿名いいねの like_identifier ってなに?

like_identifier は、ログインをしていないユーザーに対して発行するランダムな文字列です。これを Cookie に保存して、匿名いいねに利用しています(文字通り、like をしたユーザーの識別のみが目的の値です)。

like_identifier はユーザーのブラウザのみにある情報であり、Cookie が有効な間は、そのユーザーの「いいね」の情報を取得することができます(Cookie が切れた場合は、「いいね」の取得ができないのですがそれを仕様としました)。

何で中間テーブルを使う方法がリファクタリングの候補になかったの?

今回は、「いいね」の対象が増えるたびにテーブルが増えて、開発やメンテナンスが難しくなっていたので、テーブル数やモデル数は減らしたいという方向に考えていました。また、今後も「各いいねの振る舞いを同じ」にするという制約をおいたため、ポリモーフィック関連か STI でのリファクタリングを検討しました。

「ポリモーフィック関連で if 文を書くと破綻する」の例は?

ポリモーフィック関連のモデルのタイプ別に、異なる振る舞いを持たせたくなった時が典型的な例でしょう。例えば、商品やトピックが「いいね」されたときだけ購入導線を出すとした場合、いいねメソッドを呼び出す側でその実装を意識する必要に迫られ、どんどん複雑になっていきます。ポリモーフィック関連のモデル、つまり、Like には if 文は絶対に書かない=異なる振る舞いはさせない、という強い意志を持ちリファクタリングを行いました。

おわりに

本記事では、森羅万象に「いいね」するためのデータ構造の発表の Q&A の解説をしました。発表では話せなかったリファクタリングの詳細についても別途記事を書くので、次回作もお待ちください。

クックパッドでは、データの設計やリファクタリングについて熱く話し合う人を募集しています!もし少しでも興味があればカジュアル面談も実施していますので、ぜひご応募ください。

cookpad.jobs

Amazon ECS と AWS Lambda で汎用 self-hosted runner を提供する基盤

$
0
0

技術部 SRE グループの @s4ichiです。ここ最近は本業に加えて Overwatch2 のヒーローとして戦いに明け暮れています。救わなければならないレートがある。

GitHub flow に従った開発では GitHub Actionsが非常に便利です。特に最近では CI 用途だけでなく、ソフトウェアのデリバリーなども Actions で完結させる事例も見かけます。しかしながら、クックパッド社内では GitHub Enterprise Serverを使っているため、GtiHub Actions の利用には self-hosted runnnerの利用が不可欠になっています。

そこで、社内では Amazon ECS 上に ephemeral で汎用的な self-hosted runner を提供しています。実行する job の数に応じた autoscaling を備え、runner の起動を司る部分は AWS Lambda を用いたサーバーレスな構成です。今日はその基盤についての構成やチャレンジ、そして独自拡張の話をします。

クックパッドにおける GitHub Actions

社内では、ほとんどのチームの開発に GitHub Enterprise Server(以下、GHES)が使われています。GHES は EC2 上にデプロイされており、開発者に不自由が無いよう、利用規模や用途に合わせて調整する形で SRE チームが運用をしています。ただし、最新のバージョン 3.7.0 現在においても GHES には GitHub-hosted runnerが提供されていないため、self-hosted runner を用意しないことには GitHub Actions を使えませんでした。

そんな中、昨年12月にリリースされた GHES のバージョン 3.3.0 にて、self-hosted runner を ephemeral に扱えて、かつ autoscale に用いることができる webhook が提供されるようになりました。依然として self-hosted runner 自体を用意することには変わりませんが、GHES でも比較的容易に GitHub Actions を運用するための機能が提供されました。世の中的にも GitHub Actions の機能が拡充されるに連れて利用の幅が大きくなっているのを感じていたのもあり、GitHub Actions を試してみようという動きが始まりました。

今回はこうした背景のもとで構築された GitHub Actions を提供する基盤の話になります。

ghe-actions: 汎用的な self-hosted runner を提供する基盤

ghe-actions は社内の GHES 上で GitHub Actions を提供する内製の仕組みです。汎用的な用途で使える self-hosted runner を ECS Task として提供します。runner は ephemeral な用途で使えるよう job ごとにクリーンな環境が用意され、同時実行する job の数に応じて必要な Container instance を調整する autoscaling の機能を備えています。

ghe-actions 概略

ghe-actions は GitHub Apps と AWS 上のいくつかのサービスを組み合わせて実現しています。

  • self-hosted runner 本体である Amazon ECS Task
  • GHES から webhook を受け取って runner を起動する AWS Lambda function
  • runner の ECS Task を起動するためのAmazon SQS queue

ghe-actions の構成は運用の手間が少なくなるよう、GHES 本体以外に状態を持たない設計にしました。状態遷移を保存するためのデータストアを持たないことで依存するコンポーネント管理の手間を削減するのが目的です。

利用の流れ

社内の GitHub Actions のユーザーは、まず ghe-actions のための GitHub App を自身のユーザーや organization にインストールします。GitHub App には workflow の job が起動した webhook(workflow_job)を Lambda の function URL へ送る設定がされており、インストールされた organization のリポジトリで workflow が起動すると、workflow_job の webhook から Lambda が起動します。

workflow_job の webhook を受け取った Lambda は、webhook の内容を解釈して必要なデータを抽出します。それらのデータは runner 起動のための SQS に SendMessage されます。SQS は別の Lambda から ReceiveMessage されたのち、GHES の API を使って runner 登録のための token を発行し、job を起動したリポジトリ専用の self-hosted runner を ECS Task として起動します。

runner は起動して job を実行したら Task を終了します。ephemeral な runner なので、同じ Task で複数回 job を実行することはありません。実行ごとに完全に削除されます。

構成要素の詳細

ユーザーがインストールする GitHub App は必要な permission を最低限要求し、以下の操作のために使われます。

  • 有効にしたリポジトリの workflow_job webhookの購読
  • リポジトリ向けの Registration Token の発行
    • self-hosted runner はこの token を利用して runner を GHES に登録する

2つの Lambda function はそれぞれ以下の役割を持ちます。

  • function URL から webhook を受け取る Lambda function
    • workflow_job の webhook の中でも Queued となるイベントにフィルターする他、payload を解釈して必要なデータだけを抽出して SQS に SendMessage する
  • SQS から ReceiveMessage してrunner を起動する Lambda function
    • SQS 経由で受け取った payload からリポジトリや labels を特定し、Registration Token の発行と labels に応じた適切な runner を決定・起動する

self-hosted runner 本体である ECS Task は次の特徴を持ちます。

  • マルチサイズ・アーキテクチャに対応
    • 最小 1 vcpu, 2 GiB RAM から最大 8 vcpu, 15 GiB RAM まで4種類のサイズを提供
    • アーキテクチャは amd64, arm64 環境を提供
  • Ubuntu ベースで基本的なパッケージは導入済み
    • 社内のユースケースに合わせつつ、GitHub-hosted runner に近い体験を提供
  • sidecar コンテナ内で dockerd を rootless で起動して利用
    • workflow 内の Docker コンテナの実行をセキュアに保つ

以上が汎用的な self-hosted runner を提供するための基盤である ghe-actions の概要です。ここからは ghe-actions の提供にあたって検討したチャレンジを紹介します。

汎用的な self-hosted runner を提供するために

GitHub-hosted runner では ubuntu-latestなどの labels を指定すると、様々なパッケージが事前に用意されている汎用的な runner の環境が手に入ります。しかし、GHES の GitHub Actions には GitHub-hosted runner はありません。そのため ubuntu-latestで起動してくるような汎用的な self-hosted runner の提供が必要です。まったく同程度の汎用性までは求めずとも、社内の開発における不便さを解消できるところまでは提供する必要がありました。

self-hosted runner は https://github.com/actions/runnerをインストールした計算機を repository や organization に登録することで runner として機能し、job を実行できるようになります。actions/runner 自体の requirements はそこまで大きくないため、いくつかのパッケージを入れるだけで満たせます。しかしながら、その状態の self-hosted runner は私たちが普段開発する上で必要な機能が不足しています。

ubuntu-latestなどの labels で起動してくる GitHub-hosted runner は https://github.com/actions/runner-imagesから提供されているイメージです(着手当時は virtual-environments という名前だったんですが、変わってました)。例えばこれを Docker イメージ化することで同等の環境が提供できるのですが、イメージのサイズが数十 GiB とかなり大きくなってしまいます。AMI 化して EC2 で提供する方法も試しましたが、ephemeral な環境を提供するにあたって、毎度 EC2 の起動を待つことは実行時間の増加が見込まれるため諦めることにしました。

さて、管理や運用の楽さ、そして起動時間の観点から、やはりコンテナ化して提供することが好ましいです。しかし、コンテナ環境で self-hosted runner を提供するには課題があります。それはコンテナの中で Docker CLI を使えるようにしなければならないことです。workflow ではイメージのビルドはもちろん、MySQL などのミドルウェアの起動にも Docker を使います。

Docker コンテナ内で Docker CLI を使うためには、dockerdが必要です。コンテナ内で dockerd を提供する方法として、ホストで動いている dockerd のソケットをコンテナ内に共有する dood(docker-outside-of-docker)、もしくはコンテナ内でホストとは別な dockerd を立ち上げる dind(docker-in-docker)があります。前者はホストの dockerd を直接操作できるため、ホストで動いているコンテナを触ることができ、後者はコンテナごとに完全に独立した dockerd の環境が与えられます。

今回、リポジトリによっては秘匿値を扱ったり AssumeRole の操作を含むこともあり、汎用的な用途を目指すうえでセキュリティ面を重視する必要があります。そのため、dood のようにホストの dockerd が共有される環境は、簡単に他のコンテナへ侵入できるためセキュリティの観点から危うく、必然的に dind を選ばなければなりません。しかし dind でもコンテナ内で dockerd を実行するにはコンテナに特権を渡す必要があります。特権を持つコンテナは、ホスト環境を参照できてしまうので脆い状態には変わりません。そういった状況で、今回はコンテナに特権を与えつつ dind でセキュアな環境を提供するため rootless な dind の docker を利用しました。docker は公式で dind-rootless なイメージが配布されています。

そのイメージを ECS Task の sidecar コンテナとして起動する方針を取りました。dind の特権付与済みコンテナを runner のコンテナで dood した、というわけです。これで runner が参照する dockerd の環境は特権を持ちますが、root を持たないため、できることが限られます。以下に構成図を示します。

dind-rootless を sidecar として起動する ECS Task

図中では書いていませんが、sidecar に切り分けた dind-rootless コンテナ内の dockerd は、runner のコンテナと同じ network で動かし、localhost アドレスを共有するため、ECS Task を awsvpc network mode で起動しています。volume も共有して使うことで、基本的なユースケースでは runner と dockerd のコンテナが異なることを気にすることはほとんどありません。

ここまで来たらあとは必要なパッケージを同梱するだけです。actions/runner のアプリケーションをベースに、よく使われるパッケージを事前に入れた Docker イメージを提供しました。イメージサイズは ubuntu をベースにした actions/runner で 1.47 GiB、sidecar の dind-rootless コンテナは 367 MiB です。

多様な self-hosted runner を提供する

今回提供している self-hosted runner は ECS Task として起動できる形式ならば様々な拡張が可能です。ghe-actions は社内のユースケースに合わせてマルチアーキテクチャに対応しており、用途に応じて runner サイズの選択もできるようにしています。

workflow_job の webhook から受け取れて、かつ input とみなせる情報として labels があります。この labels で表現できる範囲であれば容易に多様な runner を提供することが可能です。現状は前述したアーキテクチャとサイズのみを展開していますが、例えば特定の SecurityGroup を持つ runner を起動するなど、GitHub-hosted runner では難しい領域で拡張していく予定です。

AWS 利用料金の節約

self-hosted runner が提供できるようになったとはいえ、提供する ECS Task は無料ではありません。今回は継続的に基盤を提供し続けるためにも、できるだけ利用料金を節約する方向に ECS Cluster を構築しました。

ghe-actions の ECS Cluster に属する Container instance は全て Spot Instance で構成されています。Spot Instance の利用は、On-Demand Instance と比べてかなり利用料金を削減できるためです。Spot Instance interruptions により workflow が途中で終了してしまう可能性もありますが、capacity-optimized allocation strategy を用いることで頻度を抑えるようにしているため、今のところ困ることもほとんど無く済んでいます。

Container instance は job の実行数に応じて autoscaling する構成になっています。そのため、利用頻度の低いときは台数を減らして待機させることにしました。autoscaling を有効にすることで、一度に大量の job が起動する場合は Container instance の起動時間のペナルティがかかりますが、大量の job を一気に起動する用途は限られるため、通常の用途では困ることは少ないでしょう。

現行の制約

ghe-actions は社内のほとんどの用途で問題無く使えますが、本家の GitHub-hosted runner と比べればいくつか制約が残ります。

Service containers の機能が使えない

Service containersは GitHub Actions の workflow で記述できる Docker コンテナ起動のための記法です。actions/runner はまだコンテナ内での起動を公式にサポートしているわけではないため、 Service containers の機能を使う前の実行環境の validation を違反してしまう状態です。

GitHub Marketplace にある action が全て使える状態ではない

ほとんどのケースにおいて問題にはならないんですが、GitHub Marketplace にある action の一部は使えないことがあります。action の実装によっては ghe-actions の構成と非互換な使われ方をしていることがあるためです。都度回避策を用意するなどして回避していますが、稀に存在するため、GitHub-hosted runner と完全な互換だと想定することができない現状です。例えば、docker/build-push-actiondocker/setup-buildx-actionは README にある通りの設定では ghe-actions の環境で動作しないため、事前に別途コマンドを実行した上での利用が必要です。

おわりに

本記事では社内の GHES に導入した ghe-actions の基盤についてその背景やチャレンジ、独自拡張について紹介しました。GitHub-hosted runner と比較して不足する部分もあるものの、self-hosted runner であるために細かな調整や独自拡張を入れやすい環境が提供できています。

運用の手間や起動時間を最小限にしつつ、現実的なコストで運用ができる程度の汎用的な runner を提供する基盤が整っています。今後は社内にある開発フローやシステムとの連携を進め、より使いやすい環境を提供していきます。

クックパッドでは、このような CI/CD 環境の構築を始めとした開発者向けの仕組みの基盤整備にも積極的に取り組んでいます。こうした分野の改善には幅広い領域への理解と深い専門性が必要になります。開発者向けの基盤、及びサービスの信頼性向上に興味のある方は、ぜひお気軽にご連絡ください。下記の採用ページからも申し込むことができます。

cookpad.careerscookpad.wd3.myworkdayjobs.com

Cookpad Summer Internship 2022 を開催しました!

$
0
0

こんにちは、ボイスサービス部の ymd (@y_am_a_da) です。今年は新卒採用エンジニアリーダーもやっています。

さて、 https://techlife.cookpad.com/entry/2022/02/25/100000でも告知させていただいた通り、今年の夏は 15-day Tech Course と 3-day Tech Course の 2 種類のエンジニア向けインターンシップを開催しました。この記事ではその内容を紹介します。

15-day Tech Course

15-day Tech Course は前半 5 日間が講義形式、後半 10 日間が実戦形式でした。また、今年も前半の講義パートはオンラインのみ、後半の実践パートではオフィスに来訪されることを希望した方にはオフィスで、それ以外の方々は前半から引き続きオンラインで参加する形式を取りました。また、昨年と違う点として今年のオンライン講義は Gather を利用しました。

講義の際には右側に集まり、サービス開発講義などでグループワークをする際には左の小部屋に集まり作業をしました。参加者全員にビルダー権限を付与したおかげで日々画面が賑やかになっていく様が見ていて楽しかったです。 Gather 跡地

1日目: オンボーディング & Git/GitHub 講義

初日はまず午前中にイントロダクションとしてクックパッドの取り組みの紹介や自己紹介などを行いました。 また、例年と違い今年は全ての参加者が後半の 10 日間では OJT に取り組むため、それに必要なセットアップなどもこの時間にやりました。

イントロダクションを終えランチをした後、 Git/GitHub 講義から技術講義がスタートしました。

講義の後は、引き続き OJT を進める上での各種説明や、 CTO の成田による技術組織の紹介などがありました。

2 日目: iOS 講義

2 日目の iOS 講義では、簡易なレシピサービスを SwiftUI で実装するという内容でした。この時点ではまだモックですが、 API 通信をしてデータを取得したものを表示させるところまできちんと網羅しています。 Swift を書いたことが無い人も多かったため、独自の記法に慣れない場面も見受けられましたが、ほとんどの人が無事にハンズオンパートを完了させることができていました。

https://github.com/cookpad/cookpad-internship-2022-summer-ios

3 日目: サーバーサイド講義

3 日目はサーバーサイド講義でした。今回は Backend For Frontend (BFF) を題材に、複数のリソースサーバーを操作してデータのやりとりをする単一の GraphQL API サーバーを実装しました。 BFF という概念自体一定の規模の大きいサービスでないとあまり見かけないため、初見の方も多く見受けられましたが、最終的にはきちんと使いこなして実装を進められているようでした。

また、最終的には何名かが iOS アプリケーションとの繋ぎ込みまで完成させることができ、フロントエンドでデータが表示できている様子に喜んでいる姿も見受けられました。

https://github.com/cookpad/cookpad-internship-2022-summer-serverside

4日目 ~ 5 日目: サービス開発講義

講義の後半パートでは、サービス開発講義を実施しました。今回の内容は、ユーザーインタビューからそのユーザーが抱いている課題を見つけ出し、プロトタイプを作成してユーザーテストを実施して最終的なフィードバックを受ける。というものでした。

まず最初に、クックパッドのサービス開発に対する考え方や開発プロセスを座学形式で学びました。ここでユーザーインタビューのコツやその後のアイディア出しのためのテクニックについて、クックパッドで実際にやっている手法についても学びました。

その後は、グループごとに別れてユーザーインタビューをし、価値仮説から始めてアイディア出しをしました。参加者同士がガッツリコミュニケーションを取るタイミングはここが初めてだったので不安もありましたが、 Gatherのワイワイ感もあったのか議論は盛り上がっているように見えました。アイディア出しのパートでは講師や TA に壁当てをすることが出来るのですが、講師も TA もかなり忙しそうに色々なグループを回っている姿が見えました。

5 日目は、まず前日に引き続き価値仮説やユーザーストーリー、プロトタイプの作成を引き続き行い、午後からユーザーインタビューの時と同じユーザーを対象にユーザーテストを行いました。 あまりこういったユーザーインタビューやユーザーテストの経験が無い方も多かったため苦戦している様子もありましたが、普段個人サービスを出しているもののこういったことが出来る機会は無いというフィードバックもあり好評でした。

6~15 日目 OJT

後半は OJT として実際にチームに配属されての実践パートでした。 事前に参加者からヒアリングした部署や技術領域などの希望をもとに配属され、実際にチームの一員としてメンターと共に 2 週間の間いくつかのタスクをこなしていただきました。

最終日には各自やったことをまとめてもらい、成果発表会をやりました。配属先のチームが多種多様だったこともあってか、発表内容も非常にユニークなものに仕上がってました。 最終日の成果発表

CTO も全ての発表に目を通し、最終的に CTO 賞の決定をしました。加えて今回は、最優秀賞を参加者含むオーディエンスの方々による投票で決定しました。それぞれ特別な商品を贈呈しました。

発表会に参加している CTO の様子

成果発表会終了後には、それぞれのメンターから修了証書を渡しつつ、最後のフィードバックをしました。 メンターからのフィードバックの様子

3-day Tech Course

15-day Tech Course が終わった後、少し日をあけて 3-day Tech Course を開催しました。 https://internship.cookpad.jp/でも触れているように、今年のサマーインターンシップは開発者として働いている「未来の自分」に出逢うことがテーマとなっています。しかしながら 15-day Tech Course と違いこちらのコースは期間が 3 日間しかなく、 OJT で現場の仕事を体験するには短すぎます。 それ以外で可能な限り実際の現場に近い体験ができるよう、クックパッドと実際に関わりのあるクリエイターさんを相手にユーザーインタビューや壁当てをしながらお題に沿ったサービスを実装してもらう、というプログラムを用意しました。クックパッドでのサービス開発の一部を体験してもらえるような内容にしています。

まず、午前中はクックパッドのサービス開発に対する考え方やユーザーインタビューのテクニックなどを座学形式で学びました。この後にクリエイターさんへのユーザーインタビューとなるため、とても重要な講義です。

サービス開発講義の様子

お昼休憩を挟んで講義を終えたところでクリエイターさん方が到着しました。弊社が提供している生鮮 EC プラットフォームのクックパッドマートに出品されている方など、様々な場面でご協力を頂いている方々です。

クックパッドで活躍されているクリエイターさん達

参加者はそれぞれのクリエイターさんごとにグループに分かれ、ユーザーインタビューをしました。個人開発やアルバイトをしていても実際にユーザーインタビューをする機会が無い方も多く、新鮮な経験ができているみたいでとても盛り上がっていました。 クリエイターさんにインタビューをしている様子

インタビューが終わった後はまた個人ワークに戻り、話の内容から価値仮説を組み立てていきました。この間も必要に応じてクリエイターさんやメンターに壁当てをしたり、社内のスタッフから条件に当てはまりそうな人に声をかけて別途インタビューをしている方もいらっしゃいました。

2 日目からはひたすら価値仮設をもとにサービスを実装していき、 3 日目に成果発表を行いました。同じクリエイターさんをインタビューしたグループ内で見てもそれぞれが個性的なサービスを考えて実装しており、きちんと動くものを組み立てられていました。それぞれの発表にたいしてクリエイターさんとメンターからフィードバックをしました。 また、こちらも 15-day Tech Course と同様に CTO による賞と、各グループのクリエイターさんとメンターによる賞を決定しました。こちらもそれぞれ特別な商品を贈呈しました。 最終日の成果発表

ノベルティ

今年のノベルティ

写真のノベルティセットを当日は用意しました。特にネックストラップやタンブラーなどはオフィスに出社する時などに活用されていました。

まとめ

以上が今年のサマーインターンシップの開催報告です。ご参加頂いた皆さま、本当にありがとうございました! 今年は 15-day Tech Course でのオンライン講義に Gather を利用するなど初めての試みもいくつかありましたが、無事盛況に終わることができました。 加えて、昨年と比べてほとんどオフラインでの開催となりましたが、学生同士で横の繋がりを作って盛り上がっていたり、社員と共にオフィスでパフェを作るなど様々な現地でのイベントに参加したりと活発な交流が行われているようでした。 このサマーインターンシップが参加者の皆さまにとってひと夏の思い出になってくれていることを願っています。

さて、今年のサマーインターンシップは終わってしまいましたが、クックパッドでは就業形インターンシップを通年で募集しています。ぜひご応募ください!

internship.cookpad.jp

Viewing all 802 articles
Browse latest View live