Google I/O 2019 に参加しました

こんにちは、技術部品質向上グループの加藤です。 普段は主にモバイルアプリのテスト周りに関わっています。 今回は先日開催された Google I/O 2019 に参加したので、現場の環境や気になったセッションを初参加の目線で書いていきます。

Google I/O 2019

毎年5月ごろに Google が開催するカンファレンスです。 Google が展開するプロダクトやサービスに関する情報が多く発表され、カンファレンス冒頭にある Keynote は毎年非常に注目を集めています。 カンファレンス中は広い会場で多くの発表が行われていますが、発表のセッション以外にも多くの企画があります。

Office Hour & Sandbox

カンファレンス中にはセッションが行われる施設とは別にいくつもの施設が併設されています。 そのなかでも今回は Office Hour と Sandbox について触れたいと思います。

Office Hour では時間帯ごとにテーマが定められ、枠を予約することでテーマに沿った内容についてそれに関わる Google 社員と直接会話することができます。 テーマは非常に多岐に渡り、Kotlin for Android のような一般的なものから、R8 / shrinking app code のような少しニッチな部分まで数多くのテーマが存在しています。 もちろんコミュニケーションには英語が必要とされますが、社員の方も熱心に耳を傾けてくれるため英語に自信がない自分でも内容を十分に伝え合う程度には会話をすることが可能でした。

Sandbox ではテーマごとにテントが設置されており、その中でカジュアルに Google 社員と会話をすることが可能です。 Office Hour と違い予約制ではないため、常に人に溢れているような環境ですが想像以上にしっかりと会話をすることができ、Office Hour と合わせて直接コミュニケーションを取ることができる場となっています。 ちなみに人が溢れているおり必然的に場がかなり騒がしくなっているので、強い気持ちでコミュニケーションをとる必要がありました。

I/O 参加前に社内で Android や Firebase 関連の質問と要望を取り纏めていましたが、Office Hour 及び Sandbox で全ての内容を Google 社員と直接議論することができました。 特に要望に関しては実際のユースケースを合わせて会話をすることで、一方的に要望を伝えるだけでなく、現状取り組めるアプローチ等の提案もあり非常に価値がありました。

セッション紹介

ここからは I/O のセッションで興味が惹かれたものを少し紹介します。

※筆者は Android アプリの開発に関わっているので、内容は Android のものばかりなってしまっています。

New Tools to Optimize Your App's Size and Boost Installs on Google Play

https://www.youtube.com/watch?v=rEuwVWpYBOY

タイトルの通り、アプリサイズを最適化(サイズダウン)することでアプリインストールを促進させるという内容です。 Play Store 上でのいくつかの変更と合わせてアプリインストールへ如何につなげるか、具体的にどの程度の効果が見込めるのかという話でした。 いくつか Play Store の変更点がありましたが、私が注目した点としては以下の2点です。

  • Play Store 上で表示されるアプリ評価で評価者のアプリ利用年数に応じて重み付けが始まる
  • Play Console 上にアプリサイズの項目が追加

1点目については昔から長くリリースを続けているアプリであればあるほど、開発側にとってメリットとなるように受け取ることができます。 利用年数に対しての重み付けの具体的なロジックは好評されていませんが、弊社のモバイルアプリに関しては評価を上げることとなりました。 新しいロジックによる評価への切り替えは 2019年8月 から始まるようです。

2点については情報が多いのですが、まとめると Play Console 上でアプリサイズについての情報をいくつか確認できるようになるようです。 設定した類似アプリのアプリサイズの中央値との比較や、端末の空き容量が 1GB 未満の端末の利用者やその環境でアンインストールを行った利用者を確認できるようになるとのことです。 App Bundle も合わせて、Google のアプリサイズ減少への強い意向が感じられる機能です。

他にもアプリサイズが大きくなってしまうゲームアプリ向けに対しての施策や、デバイス間でのファイル共有の話題がでました。

Customizable Delivery with the App Bundle and Easy Sharing of Test Builds

https://www.youtube.com/watch?v=flhib2krW7U

昨年の Google I/O で発表された App Bundle ですが、次のレベルとしてコンテンツ配信についての新たな仕組みが紹介されました。 In-app updates はそのうちの1つですが、緊急の強制アップデートとユーザが選択可能なアップデートの2種類の仕組みが提供されるとのことでした。

また新たな仕組みに合わせてそれらをテストするツールについてもアップデートがありました。 従来 App Bundle や Dynamic Feature を検証する際には、Play Console 上にアプリをアップロードする必要があり、なおかつアップロードされたテスト版のアプリの利用側にも登録が必要であるなど制約が存在していました。 これらの問題に対して Internal App Sharing の発表がありました。

Internal App Sharing では、version code の制限と配信対象者の登録が必要なくなり、インストールリンクを踏むだけでテスト番のアプリを利用することが可能となりました。 各インストールリンクごとに利用者の制限はあるようですが、これにより App Bundle を利用したアプリの検証の難易度が下がることとなりそうです。 App Bundle に関わらず幅広い用途が見込めるため、社内の多くのチームで検証が効率化されることを期待しています。

Build Testable Apps for Android

https://youtu.be/VJi2vmaQe6w

Testable な実装を目指して、テストピラミッドから実際の実装例までを包括的に説明する発表でした。 昨年の Google I/O で発表された Android Test を利用したモダンな実装例が紹介され、同日にこのセッションに合わせて Android Testing の Codelab が更新されたので、ご興味ある方はぜひお試しください。 また昨年テスト実行環境として、Nitrogen という ART や JVM、 Firebase Test Lab 等実行環境を意識することなくテストの実行を可能とする概念が発表されました(Jetpack の燃料ということで Nitrogen という命名)。 これまで完全に謎なものとなっていましたが、今回 Early Access Program が発表されました。 まだ全貌は明らかになっていませんが、いち早く応募してみなさんの目で確かめてください。

参加してみての感想

今回の Keynote 中で発表されたように すべての人に向けて 技術を提供するという目的に向けた内容が多かった印象でした。 紹介しなかったセッションでもアプリサイズの最小化や機械学習技術のアクセシビリティへの応用など発表や展示が非常に多く見受けられました。

また昨今のカンファレンスでは、セッションの発表内容がインターネット上で公開されることが多いなかで、 発表された多くの内容について直接開発者とコミュニケーションをとることで、実際の開発にどう活かせるのかという点を確認することができ、現地に赴く重要性が強く感じられました。

f:id:ksfee:20190604210755p:plain

クックパッドでは Google I/O で発表されるような最新の技術をガンガン取り込んでいく Android エンジニアを募集しています。

XcodeGenによる新時代のiOSプロジェクト管理

こんにちは。モバイル基盤部の@giginetです。平成最後のエントリを担当させていただきます。

iOSアプリの開発では、Xcodeが生成するプロジェクトファイルである、*.xcodeprojをリポジトリで共有するのが一般的です。

しかし、この運用は大規模なプロジェクトになるほど、数多くの課題が発生します。

クックパッドiOSアプリは巨大なプロジェクトであり、通常の*.xcodeprojによる管理には限界が生じていました。

そこで、昨年秋にXcodeGenというユーティリティを導入し、プロジェクト管理を改善したので、その知見をお伝えします。

f:id:gigi-net:20190425150234g:plain

従来のプロジェクト管理の問題点

ファイル追加の度にコンフリクトが発生する

*.xcodeprojファイルはプロジェクトに含まれるソースファイルの管理を行っています。

開発者がプロジェクトにファイルを追加すると、このプロジェクトファイルが更新されることになります。

そのため、同時に数十人が開発するクックパッドiOSアプリの開発環境では、プロジェクトファイルのコンフリクトが日常茶飯事で、解消に多くの工数が発生していました。

レビューがしづらい

*.xcodeprojは特殊なテキストデータで表現されますが、とても人間が読める物ではなく、差分をレビューするのは困難です。

簡単なファイルの移動でも複数の差分が発生します。

大きな変更に向かない

クックパッドiOSアプリでは、巨大なビルドターゲットを分割し、ビルド時間を改善する『霞が関*1と呼ばれる取り組みを行っています。

マルチモジュール化を行っていくに当たって、ドラスティックな*.xcodeprojの変更に耐える必要がありました。

単なるファイル追加であれば、コンフリクトの解消や、レビューの難しさという問題はまだ解決可能でしたが、ターゲットやBuild Configurationの追加、大量のファイルの移動といったプロジェクトの変更をもはや人類が適切に扱うことは困難でした。

XcodeGenとは

そこで導入したのがXcodeGenです。

XcodeGenは、XcodeのプロジェクトデータをYAMLで記述し、定義から冪等に*.xcodeprojを生成できるユーティリティです。

このようなYAMLを定義し

targets:
  Cookpad:
    type: application
    platform: iOS
    sources:
      - path: Cookpad

XcodeGenを実行すると、*.xcodeprojを自動生成することができます。

$ xcodegen
Loaded project:
  Name: Cookpad
  Targets:
    Cookpad: iOS application
  Schemes:
    Cookpad
⚙️  Generating project...
⚙️  Writing project...
Created project at Cookpad.xcodeproj

f:id:gigi-net:20190425150303p:plain

このツールの導入により、数々の問題が解消できました。

導入して良かったこと

ファイルツリー構成が強制される

まず、*.xcodeprojの問題点として、ファイルシステム上のツリーと、プロジェクトの保持するツリーが一致しないという問題がありました。

追加されるファイルは、ファイルシステム上の階層と必ずしも一致しませんし、思い思いに追加されるため、プロジェクトが煩雑になります。

XcodeGenによる生成では、ファイルツリーからプロジェクト構造を生成するため、この不一致が解消されます。

targets:
  Cookpad:
    sources:
      - path: Cookpad

例えば、この指定では、Cookpad以下のファイル全てがCookpadターゲットに所属するため、ファイルシステム上の位置を強制することができます。

f:id:gigi-net:20190425150316p:plain

コンフリクト解消が不要に

上記の仕様による一番わかりやすい恩恵は、プロジェクト差分のコンフリクトからの解放です。

従来の*.xcodeprojでは、開発者がソースファイルを追加する度に更新が入り、差分が発生していました。

しかし、XcodeGenの仕様においては、ソースファイルの追加時にリポジトリへのファイル追加以外の操作が不要になり、一切のプロジェクトのコンフリクトがない世界が到来しました。

ターゲットの追加が容易に

上記の特性はビルドターゲットの追加にも役立ちます。XcodeGenでは、わずか数行のYAMLの定義のみでビルドターゲット追加を行うことができます。

targets:
  CookpadCore:
    type: framework
    platform: iOS
    sources:
      - CookpadCore

f:id:gigi-net:20190425150330p:plain

また、ターゲット間のソースファイルの移動も簡単です。 従来は、1ファイルごとにどのビルドターゲットでビルドされるか、という情報が保持されていたため、ファイルを移動する度にプロジェクトに大きな差分が発生していました。

しかし、XcodeGenでプロジェクトを生成することにより、特定のディレクトリ下のソースコードは、必ず特定のビルドターゲットに含まれることを保証することができるようになりました。

これにより、ビルドターゲット間の移動は単にgit mvするだけで済むようになりました。

この特性は、プロジェクトのマルチモジュール化に大きく役立ちました。

XcodeGenの導入

XcodeGenを導入したい場合、残念ながら既存の*.xcodeprojから簡単にプロジェクト定義ファイルを生成する方法はありません。

基本的には、ドキュメントを追いながら、生成結果を目で見て確認していきます。

GUIでの設定値をプロジェクト定義に忠実に移植する為には、既存の*.xcodeproj/project.pbxprojをテキストエディタで開き、設定値を探していくという地道な作業も発生しました。綺麗なプロジェクト定義を記述するには、Xcodeプロジェクトの構造をよく理解している必要があるでしょう。

最終的にクックパッドiOSアプリは、400行程度のYAMLファイルでほぼ元の挙動を再現することができました。

そこで、複雑なプロジェクトをXcodeGenの定義ファイルで記述するためのテクニックをいくつかご紹介します。

SettingGroup

まずはSettingGroupの機能です。 複数のターゲットで共通して利用したいビルドフラッグなどの設定をSettingGroupとして定義しておき、利用したいターゲットで読み込んで使用することができます。

settingGroups:
  SharedSettings:
    configs:
      OTHER_SWIFT_FLAGS: -DDEBUG
targets:
  Cookpad:
    type: application
    settings:
      groups: [Shared]
  OtherFramework:
    type: framework
    settings:
      groups: [Shared]

パッケージ管理

プロジェクトにCarthage*2でインストールしたライブラリを統合したい場合も簡単に記述できます。

targets:
  Cookpad:
    type: application
    platform: iOS
    dependencies:
      - carthage: RxSwift

このcarthage指定を用いるだけで、Embed Frameworkの設定や、Frameworkのコピーなど、Carthageの利用に必要な設定を自動で行ってくれます。

f:id:gigi-net:20190425150345p:plain

一方で、CocoaPodsを併用する場合、事態は複雑です。

CocoaPodsは、プロジェクトファイルにあとからビルド設定の注入を行う必要があるからです。現在のXcodeGenでは、プロジェクト定義だけでそれを管理することはできません。

例えば一連の処理をMakefileに記述するというアプローチが考えられるでしょう。

xcodegen
bundle exec pod install

ソースコード生成

Sourceryなどのコードジェネレーションと、XcodeGenを併用する場合には少し工夫が必要です。

XcodeGenによるプロジェクトツリーは、通常、存在しているソースファイルのファイルシステム上の構成により構築されます。

一方で、ソースジェネレーションを行うためには、他の定義からソースコードを生成するため、プロジェクトツリーが必要になります。 このように、鶏と卵問題が発生してしまうのです。

そこで、optionalオプションで、生成前のソースファイルの参照だけ持ち、先にプロジェクトを構築し、あとからソースジェネレーションを行うことでこの問題を解決しています。

targets:
  CookpadTests:
    type: unit-test
    platform: iOS
    sources:
      - path: "CookpadTests/AutoGenerated/AutoGenerated.swift"
        optional: true
        type: file

f:id:gigi-net:20190425150358p:plain

今後の課題

XcodeGenを大規模に運用している中で以下のような問題が発生しました。

主にプロジェクトの生成時間に関する課題で、現在解決している最中です。

CocoaPodsを利用するときの生成速度

CocoaPodsの利用時に、プロジェクト生成後、毎回pod installが必要なことは、先ほど触れました。

XcodeGenは冪等に実行されますが、それ故に、プロジェクト生成ごとにCocoaPodsによるビルド設定の注入を毎回行う必要が出てくるのです。

この仕組みでは、XcodeGenの生成ごとに毎回パッケージインストールが走り、数十秒の待ち時間が発生しています。

この問題を解決するアイディアはいくつかあります。

まずは、CocoaPodsによるプロジェクトの設定変更を無効化し、自分で依存関係を記述する方式です。*3

もう一つの方法は、CocoaPods 1.7で利用可能になったincremental_installを有効にすることです。 このオプションを有効にすることで、差分がある依存関係のみが生成されるため、プロジェクト生成速度が改善すると踏んでいます。

いずれの方式も構想段階でまだ実用できていません。

プロジェクトキャッシュの問題

毎回プロジェクトファイルを上書きしていると、希にXcodeのビルドキャッシュが無効になり、フルビルドが発生してしまう問題にも遭遇しています。

この問題は、生成されたプロジェクトの差分が発生しないようにしても再現しており、解決していく必要があります。

まとめ

ご覧いただいたように、XcodeGenを使ったプロジェクト運用は、クックパッドiOSアプリほどの規模であっても十分に実用できていると言えます。

*.xcodeprojで苦しむのは平成までです。皆さんもプロジェクトを破壊して新しい時代を迎えませんか。

クックパッドではXcodeプロジェクトのコンフリクト解消で消耗したくないエンジニアを募集しています。

*1:詳しくは2月に行われたCookpad Tech Confの資料をご覧ください https://techconf.cookpad.com/2019/kohki_miki.html

*2:ちなみに筆者は最近Carthageのコミッターになりました 💪

*3:この手法は integrate_targets というオプションを有効にすることで実現できますが、難しいのでここでは解説しません

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

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

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

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

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

問題と解答を公開します。今からでも自力で挑戦したい人のために、まず問題だけ掲載します。(会議中に gist で公開したもの と同じです)

問題

Problem 1-1

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"

Problem 1-2

puts&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

Problem 1-3

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)

Problem 2-1

def say
  -> {
    "Hello world"
  }
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

Problem 2-2

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next

Problem 2-3

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

Problem 3-1

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p

Problem 3-2

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

Problem 3-3

def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say

以下、ネタバレになるので空白です

自力で解いてみたい人は挑戦してみてください。











解答

では、解答です。重要なネタバレですが、すべての問題は 1 文字追加するだけで解けるようになってます。

Answer 1-1

作問担当は ko1 でした。問題再掲↓

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"

解答↓

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" ..; "Hello"} world"

"Goodbye" .. の後に ; を入れています。これにより、Ruby 2.6 で導入された終端なし Range (Feature #12912) になります。この Range は使われずに捨てられ、"Hello" が返り値になって文字列に式展開されるので、Hello world が出力されるようになります。

この問題の勝者は tompng さんでした。なお、tompng さんは 1-2 と 1-3 も最初に 1 文字解答を発見しましたが、勝者になれるのは 1 人 1 問だけ、としました。

Answer 1-2

作問担当は ko1 でした。問題再掲↓

puts&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

解答↓

puts$&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

&. の前に $ を入れて $&. にしています。$& は正規表現にマッチした部分文字列を表す特殊変数です。ここでは正規表現マッチは使われていないのでこの変数は nil になりますが、重要なのはこの書換によって puts メソッドに $&.then { "Hello world" } を引数として渡す、というようにパースされるようになることです。then メソッドはブロックの返り値を返すので、この引数は文字列 "Hello world" になり、めでたく Hello world プログラムになります。

この問題の勝者は Seiei Miyagi さんでした。

Answer 1-3

作問担当は mame でした。問題再掲↓

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)

解答↓

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2i * PI)
Out.puts("Hello world" *
         Count.abs.round)

E ** (2i * PI) というように i を入れました。

これはちょっと知識問題で、e^{i\pi} = -1 という公式を使います。この公式は「オイラーの公式」と呼ばれ、ヒントにあるように「最も美しい等式」などと言われることもあります。Ruby で書くと Math::E ** (1i * Math::PI) #=> -1 です。E ** (2i * PI) はそれの二乗になので、浮動小数点数演算の誤差もあるのでおよそ 1 になります。Count,abs,round によって正確に 1 になって、Hello world プログラムとなります。

この問題には別の意図もありました。これらの問題は 1 文字で解けると知っていたら、ブルートフォース(いろんな箇所にいろんな文字を挿入して実行してみるのを網羅的に試す)によって頭を使わずに解けてしまうのですが、この問題はそれをじゃまするために用意しました。というのは、$< の前に * を挿入して *$< とすると、標準入力を配列化する演算となり、標準入力を待ち受けて動かなくなるようになります。よって、下手にブルートフォースをするとここで実行が止まります。ただ、このトラップにひっかかった人はいたかどうかはわかりません。

この問題の勝者は pocke さんでした。

Answer 2-1

作問担当は ko1 でした。問題再掲↓

def say
  -> {
    "Hello world"
  }
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

解答↓

def say
  -> {
    "Hello world"
  }.
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

}.. を追加してあります。これにより、yield はブロック呼び出しではなく、上の Proc 式に対して yield メソッドを呼び出すようになります。Proc#yieldProc#call の別名なので、このラムダ式が実行され、"Hello world" を返すようになります。

この問題の勝者は Shyouhei さんでした。

Answer 2-2

作問担当は mame でした。問題再掲↓

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next

普通に考えたら、次の 2 文字の解答になります。

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  g.yield "Hello world"
end

puts e.next

Enumerator の最初の要素として "Hello world"yield メソッドで渡し、Enumerator#next によってそれを取り出し、それを表示します。Enumerator についてはドキュメントの class Enumerator を参照ください。

ヒントに従って考えると、次の 6 文字の解答にたどり着きます。

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  Fiber.yield "Hello world"
end

puts e.next

Enumerator は Fiber のラッパのようなものなので、実はブロックの中で Fiber.yield を呼ぶことでも要素を渡すことができ、上のプログラムと同じように動きます。

ただしこれは 6 文字も追加しているのでまったく最短ではありません。どうすればよいかというと、次が 1 文字解答です。

解答↓

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially
 Fiber.
  yield "Hello world"
end

puts e.next

コメントの中の essentiallyFiber. の間に改行文字を追加しました。コメントの中にある Fiber. という文字列を利用するのがミソでした。すべての問題に適当なヒントコメントが書いてあるのは、この問題にだけヒントコメントをもたせることで不自然になってしまわないようにするためでした。

余談ですが、より面白い想定回答は↓でした。

e = Enumerator.new do |g|
  # Hint: Enumerator is
  #
 essentially Fiber.
  yield "Hello world"
end

puts e.next

essentially の前に改行を入れています。essentially は関数呼び出しとみなされますが、引数が Fiber.yield "Hello world" なのでこちらが先に評価され、essentially が実際に呼び出されることはなく、正しく動きます。この解答にたどり着いた人はいなかったようです。

この問題の勝者は youchan さんでした。

Answer 2-3

作問担当は mame でした。問題再掲↓

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

2 文字解答はたくさんあります。3535-8 に変えたり、say jsay j*2 に変えたり、or putsor 0;puts と変えたり、いろいろなやり方が発見されていました。

1 文字解答は、意外と理詰めでたどり着けるようになっています。say メソッドは「$s を右に 2 ビットシフトし、引数 n を足す演算」です。ヒントにあるとおり 35 の二進数表現を考えると 10 00 11 になります。それぞれ二進数で 2, 0, 3 なので、say(2); say(0); say(3) という順序で say を呼び出せばいいことがわかります。say i; say j; say ksay(1); say(2); say(3) なので、say k はいじらなくて良さそうです。また、say の引数を省略したら 0 になるので、say i; say j をうまくいじって say j; say という意味にする方法はないか、と考えます。ということで答えです。

解答↓

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say if
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

say i のあとに f を足して、後置 if 文にします。条件式は次行の say j です。これにより、先に say j が評価されて、say j は真の値を返すので、if の中の say が無引数で呼び出されます。それから say k が呼ばれることで、所望の挙動になります。

この問題の勝者は k. hanazuki さんでした。

Answer 3-1

作問担当は ko1 でした。問題再掲↓

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p

解答↓

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say t:p

say :psay t:p に書き換えています。これにより、シンボルの :p を渡していたところから、キーワード t のキーワード引数として p を渡すように変わります。pKernel#p の呼び出しで、無引数の場合は単に nil を返します。よって、s = "Hello" かつ t = nil になり、"#{ s }#{ t } world""Hello world" になります。

この問題の勝者は Akinori Musha さんでした。

Answer 3-2

作問担当は mame でした。問題再掲↓

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

解答↓

def say s, t=#"Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

t=#"Goodbye " というように、オプショナル引数のデフォルト式をコメントアウトしています。これにより、次の行にある式がデフォルト式になります。この場合、s = "#{ s } #{ t }" がデフォルト式です。s はすでに受け取った引数で :Hello が入っています。引数 t は未初期化の状態で参照され、これは nil になります(コメントにあるとおり、それは問題ないです)。よってこのデフォルト式は "Hello " という文字列になります。あとはそのまま。

この問題の勝者は DEGICA さんでした。

Answer 3-3

作問担当は mame でした。問題再掲↓

def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say

解答↓

def say
  "Hello world" if%
    false && false
  # Hint: No hint!
end

puts say

if の後に % を書き足します。答えを見ても意味がわからない人のほうが多いのではないでしょうか。

Ruby には % 記法というリテラルがあります。%!foo! と書くと、文字列リテラル "foo" と同じです。デリミタ(先の例では !)には、数字とアルファベット以外の任意の文字を使うことができます。上の例は、このデリミタとして改行文字を使っています。わかりやすく、デリミタを改行文字から ! に書き換えると、こうなります。

def say
  "Hello world" if%!    false && false!
  # Hint: No hint!
end

puts say

後置 if の条件式に文字列リテラル(常に真)を書いたことになるので、このメソッド say は常に "Hello world" を返します。

なお、% 記法のデリミタに改行文字や空白文字を使える仕様は、matz が「やめたい」と言っていたので、将来廃止されるのかもしれません。

この問題の勝者は cuzic さんでした。

まとめ

Cookpad Daily Ruby Puzzles の問題と解答と解説でした。今回はわりと手加減せずに Ruby の仕様の重箱の隅をつつくような問題ばかりでしたが、「クックパッドのパズルがおもしろかった」という声も結構いただきました。まだやっていないかたは、今からでも(上の解説を見ずに)楽しんでいただければ幸いです。

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

cookpad.jobs

Special thanks

  • hogelog:クックパッドのブースに「超絶技巧パズル(ってなに?)置いておこう」と発案した人
  • sorah:シュッとチラシをデザインした人
  • ブースにいた全員:パズルの配布や運営をした人たち
  • 参加してくれた全員:解けた人も解けなかった人も

おまけ

もっと遊びたい人のためにエクストラステージを用意しておきました。答えはないので考えてみてください。

Extra 1

作問担当:mame

Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello() +
    " world"
end

puts Hello()

Extra 2

作問担当:mame

s = ""
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"

Extra 3

作問担当:ko1

(1 文字解答が 2 つあります)

def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  tp.binding.local_variable_set(:t, 'Ruby')
  tp.disable
}.enable(target: method(:say))

say