工事設計認証(技適)をとってみた

こんにちは、クックパッドの齋藤です。 私はハードウェアPdMとして、クックパッドマートで事業に関わるハードウェア(マートステーション、プリンタ、温度監視システム等)の企画開発・開発ディレクション・調達・保守等をやっています。

クックパッドマートとハードウェア

クックパッドマートは2018年9月20日にリリースされた生鮮食品のECプラットフォームです。リリースから4年以上経ち、新規事業ならではのスピードを維持しつつサービス拡大のため試行錯誤を日々続けています。

cookpad-mart.com

クックパッドマートはiOSとAndroidの専用アプリで利用可能です。このアプリで商品を購入して、近所の受け取り場所(マートステーションと呼んでいます)で受け取れます。有料で自宅配送するオプションもあります。

クックパッドマートでは、食材の輸配送や保管といった現実世界を相手にビジネスを展開しているため、冷蔵庫をはじめとした様々な機材が必要になるのですが、その中にはまだ世の中になく新しく開発する必要があったり、海外から調達する必要がある物がたくさんあります。

時には自分たちでオリジナルの基板を開発・量産することもあります。

そのため社内にはハードウェアチームがあり、ハードウェアエンジニアや組込エンジニア等、普通のWeb系企業にはいない、ユニークな人材がいます。

今日はその中でも、海外のデバイスメーカーから工事設計認証(技適)を自分たちで取得したお話をしたいと思います。

なぜ技適を取るのか

今クックパッドマートではチルド食材の配送時、シッパーという断熱ボックスの中に食材と蓄冷剤を一緒に入れて、軽自動車のバンなどで運んでいます。

バンの中にはいくつもシッパーが入っているのですが、食材の安全性を担保するため、シッパー内の温度が異常になっていないか監視を行っています。

現在はGPSマルチユニットSORACOM Editionという機材を用いて温度監視を行っていますが、サービスローンチから時間が経って事業規模が大きくなってきたため、より低コストで温度監視ができる仕組みが必要となってきました。

techlife.cookpad.com

そのためより効率的に配送中のシッパーの温度を把握し、異常を検知したり、万一品質不具合が出た時のトレーサビリティを確保する仕組み “TemperatureRightHear”、通称「TempRa(テンプラ)」を開発しています。

ざっくりとしたポンチ絵

開発するものとしては、

  1. シッパーの中に入れ、温度センサーが取得する値をBLEで送信するビーコン
  2. バンのシガーソケットに刺し、1のデータを受信してLTE経由で送信する車載IoTゲートウェイ
  3. 2のデータを集計分析し、蓄積したりアラートを促すバックエンドシステム

の3つがありますが、現在マートでは配送バンを数多く運用しており、その時のシッパーは膨大な量になります。

1で用いる市販のビーコンは、1個あたり9,000円程度してしまいます。配送時には数千ものシッパーを用いるため、普通に調達してしまうと数千万円もの高額出費を覚悟する必要があります。

そこでまず、必要な個数を減らせないか考えました。実はこのビーコンは既にマートステーションでの温度監視に用いていたため、これを回収して転用することで新規調達の個数を減らす計画です。マートステーションの側ではより安価な有線の温度センサを利用するように変更します。余談ですがこの有線の温度センサも新規開発を行いました。

しかしながらそれでも足りないため、今回直接中国の深圳にあるメーカーから現地のビーコンを直接購入し、調達価格を抑えることとしました。 その場合1個あたり15米ドル程度で調達することができるため、1個あたりの差額は7,000円程度安く調達することができます。そのため大幅に調達価格を抑えることができるのです。

であればはじめからそうすればいいじゃん、となると思いきや、そうは問屋がおろしません。 日本で電波を発生する機器を使用する際は、「技術適合認証」(通称技適)を取得する必要があります。技適を取得していない機器で電波を発した場合、電波法違反という法令違反となります。

詳細な説明はこのブログ https://www.musen-connect.co.jp/blog/course/other/japan-radio-law-basic/ がわかりやすいです。

これを取得するのは結構面倒くさいですし、それなりに電気や電波に関するエンジニアリングの知見が必要なため、なかなか大変だったりします。

しかしながらクックパッドマートには、強力なハードウェアエンジニアがいますし、私自身もマートステーションの設置をするときや将来的に物流倉庫等をIoT化するときに使えるかと思い「第一級陸上特殊無線技士」という電波についての資格も取っていました。そこで認証機関に相談したところ3-50万円で取得できることがわかり、将来的なクックパッドの知見にもなると思ったので、社内で相談した結果、量産するもの1つ1つに技適を適用するときに用いる「工事設計認証」を取得した上で調達を行うこととしました。

取得の流れ

取得に当たっては、具体的に下のような流れですすめていきました。

  1. デバイスの調達元からデータシート、金額見積等を照会します。 見積はサンプル費用、正式調達時の数量における単価と総額の2つにわけてもらいます。予算上問題がないことを確認し、サンプルを調達しました。

    サンプルデバイス

  2. サンプルデバイスについて『技適未取得機器を用いた実験等の特例制度』の届出をし、動作や私たちの要求仕様を満たしているか確認します。 制度の詳細は https://www.tele.soumu.go.jp/j/sys/others/exp-sp/ をご確認ください。

  3. 認証機関に相談の上、申請書類を作成します。 認証期間はいくつかあるのですが、今回は老舗である「一般財団法人テレコムエンジニアリングセンター」(通称TELEC)に依頼しました。 書類作成は結構大変だったのですが、先方はとても親切で、細かい表現に至るまで細かくフィードバックいただけました。それだけ書類の「てにおは」含め細かく校正が必要で、このフィードバックがなければ書類作成ができませんでした。

  4. 認証機関で試験を行います。 試験の際はサンプルデバイスとは別にテスト用のデバイスが必要です。このデバイスはスペクトルアナライザ等に接続するためRF出力ができる必要があり、また先方の安定化電源に繋げられる必要があります。このテストデバイスを用いて、入力電圧などを変えながら挙動を確認する試験を行っていただきました。

取得費用

デバイスの仕様によって異なりますが、今回はBLEのみのデバイスということで、約30万円が費用としてかかりました。

申請書類

主な申請書類は認証申込書・別紙資料、工事設計書、無線設備系統図、確認方法書、 部品配置図又は写真、外観図又は写真です。詳細は https://www.telec.or.jp/services/tech/offer.html をご確認ください。 無線設備が1チップになっていたりしたときはどうするのか、その半導体の詳細構成が開示されてない場合はどうするのか、等細かい不明点が山のように出てきますし、製造元とのやりとりもいろいろ発生します。

面白かったのは、部品が容易に変えられないことを説明するやり取りです。調達予定のデバイスは普通のプラスドライバーでケースが開いてしまうのでそこがいけないのかと思い、ネジが保護ゴムで覆われていて容易には開けられないということを書いたところ、「部品が表面実装部品で構成されている」と書けばよいとのことでした。この辺のニュアンスは、慣れていないと全く分からないですね……。

試験当日

ここから書類の修正作業をしたりテストデバイスをメーカーから取り寄せたりに10営業日くらい使いました。申請書類がOKとなれば、試験当日です。 それまでにテストデバイスの制御用ソフトウェアの動作確認や、デバイスとの接続確認をした上で、試験機関を訪問してテストです。 テスト中デバイスが想定外の挙動をするなど、ヒヤヒヤする場面もありましたが、なんとかみんなのファインプレーで切り抜けることができました!

審査と認証書交付

その後先方内で審査があり、終了後1週間程度で無事認証書が交付されました! やったね!

実際の認証書

正式調達

実際に交付された認証番号をデバイスに記載した上で、ようやく正式調達です! せっかくなので、ロゴと弊社ミッション “Make everyday cooking fun!” も書いておいてもらいました!

正式調達のビーコン

終わりに

ということで、今回クックパッドではじめて、工事設計認証の取得を試みてみました。 今までマートのハードウェアチームはステーションの開発やプリンタの開発等、リアルワールドで仕事をする上で欠かせないハードウェアを開発・量産・保守してきました。 もちろん今回のデバイスはスクラッチ開発ではないですし、なんなら別メーカーにて日本に導入済みのデバイスです。 しかしながら今回取得した工事設計認証も、そういったハードウェアを扱えるチームがあってこそ、無事取得まで漕ぎ着けることができたのは間違いありませんし、 社内ブログ「Groupad」にて知見共有が積極的に行われていたので、そういったクックパッドならではの技術的知見で、はじめてのことでもチャレンジすることができました。 取引先との雑談でそういった話が出た時には、「そこまでやるんですか!」と言われることも多いです。

このような形で、ハードウェアチームはユーザの皆様に安全・安心で高品質な食材をお届けするという、「あたりまえのことを」「あたりまえに」実現するために日々開発や保守をおこなっています。

もしご興味がある方がいらっしゃったら、是非こちらから採用情報をご確認ください!

cookpad.careers

SwiftUIで画面内の各コンテンツの表示ログを送る

こんにちは、レシピサービス開発部の@miichan_ochaです。普段はiOS版クックパッドアプリの開発をしています。

クックパッドアプリでは開発した機能の評価を行うために、画面のPVログや画面内の各コンテンツの表示・タップログなどの様々な行動ログを送っています。

今回は、SwiftUIで新たに作った画面内の各コンテンツの表示ログを送る仕組み(ShowContentLog)についてご紹介します。この仕組みは昨年7月にリリースされたiOS版クックパッドアプリ「のせる」タブ開発時に作られたもので、現在約半年ほど運用しています。

ShowContentLogの仕組み

ログの要件

レシピサービス開発部では、iOS版クックパッドアプリの画面内の各コンテンツの表示ログを以下の要件で取っています。

  1. コンテンツが初めて画面に表示される時に、そのコンテンツの表示ログを送る
  2. 画面のスクロールによって、コンテンツが一度画面外に出てから再度画面内に表示された時には、そのコンテンツの表示ログは送らない
  3. 一定時間経過後の画面自動更新やPull to Refreshによる画面更新を行った時は、更新後の画面に表示されているコンテンツの表示ログを送る
  4. 別画面にプッシュ遷移した後、遷移先から戻ってきて画面が再度表示された時に、その画面に表示されているコンテンツの表示ログを送る
  5. タブの切り替えによって画面が再度表示された時に、その画面に表示されているコンテンツの表示ログを送る
  6. アプリがバックグラウンドからフォアグラウンドに復帰した時に、復帰時の画面に表示されているコンテンツの表示ログを送る

各要件に対応するデモ動画

1 2 3
要件1のデモ動画 要件2のデモ動画 要件3のデモ動画
4 5 6
要件4のデモ動画 要件5のデモ動画 要件6のデモ動画

なお、ここでいう各コンテンツとは「表示・タップ回数を計測したいViewのまとまり」のことを指しており、ある画面では画面内のセクション単位であったり、別の画面ではバナー・カルーセルなどViewのコンポーネント単位であったりと、その粒度は画面によって様々です。

UIKitで作られた画面では、ViewController内で表示された各コンテンツのIDを管理するSetをpropertyとして保持し、UICollectionViewで作られた画面であればUICollectionViewDelegatecollectionView(_:willDisplay:forItemAt:)をトリガーにログを送信することでこの要件を実現していました。

仕組みが必要になった背景

きっかけは前述の通り、レシピサービス開発部で「のせる」タブの画面をSwiftUIで作ったことです。

「のせる」タブでは上記の仕様で各コンテンツの表示ログを送る要件があったため、SwiftUIの画面でも各コンテンツの表示ログを送る必要が出てきました。加えて、UIKitでは画面ごとに都度表示ログを送る実装をしていましたが、SwiftUIでは仕組み化して簡単に送れるようにしたいという動機から、今回の仕組みが生まれました。

動作する環境について

今回の仕組みはiOS版クックパッドアプリ上で使用されており、以下の条件・環境で動作しています。

  • iOS版クックパッドアプリで採用しているVIPERアーキテクチャに適合したまま、View層のみでSwiftUIを使っている

  • 表示ログを送る各コンテンツは、LazyVStackListなど、遅延ロードを行うViewの中に配置されている必要がある

    • 各コンテンツが表示されたかどうかは onAppear/onDisappear で判定しているため
  • Markdownで書かれたログ定義から自動生成された行動ログ(以下「自動生成行動ログ」と呼びます)を送る前提で設計されている(追加で別のログを送ることも可能)

  • iOS Deployment Targetが 14.0 の時代に開発された

    • iOS13での動作は未検証となっています

ShowContentLogの使い方

まずは、仕組みの使い方について簡単に説明します。

*説明のためコードを簡略化しています。完成版のコードに関しては「完成版のコード」の章をご覧ください。

最初に、UIHostingControllerを保持しているViewController内でShowContentLogControllerというクラスのインスタンスを作成します。

// ViewController
private lazy var showContentLogController = ShowContentLogController(screenViewController: self)

次に、表示ログを送る各コンテンツを内包しているView(以下「大元のView」と呼びます)に ShowContentLogRootModifierというViewModiferを付与します。これにより、このViewModiferを付与したViewが表示されている時にだけ、各コンテンツのログが送られるようになります。

シンプルな画面ではUIHostingControllerの引数に渡すrootViewのViewにShowContentLogRootModifierを付与すれば良いですが、タブがある画面においては、各タブの中身のViewごとに付与します(各タブで表示状態が異なるため)。

ShowContentLogRootModifierの引数controllerには、先程作成したShowContentLogControllerのインスタンスを渡します。各タブの中身のViewごとにShowContentLogRootModifierを付与した場合は、タブの数だけShowContentLogControllerのインスタンスを作成し、タブごとに別々のインスタンスを渡してください。

*このViewModiferに対応するshowContentLogRootというメソッドをSwiftUI.Viewにextensionとして定義しています。

// ViewController
override func viewDidLoad() {
    super.viewDidLoad()

    let rootView = HogeView(delegate: self, dataSource: dataSource)
        .showContentLogRoot(controller: showContentLogController)

    let hostingVC = UIHostingController(rootView: rootView)
    ...
}

最後に、表示ログを送りたい各コンテンツのViewそれぞれにPostShowContentLogModifierというViewModiferを付与します。引数eventにはLogCategory protocolに準拠したログイベントを渡します(「自動生成行動ログ」の全てのログイベントはLogCategoryに準拠しています)。

RecipeView()
    .postShowContentLog(SampleCategory.showRecipe(recipeId: recipe.id))

以上で、ログの要件通りに各コンテンツの表示ログを送ることができるようになります。

ShowContentLogの設計

次に、このShowContentLogの設計・内部実装について詳しく見ていきます。

*ここでもコードは適宜簡略化しています。簡略化されていないものは「完成版のコード」の章をご覧ください。

ShowContentLogController

ShowContentLogControllerは、表示ログを送る「大元のView」の表示状態を、子Viewである各コンテンツのViewに通知する役割を担うクラスです。isRootViewAppearingというPublisherを持ち、各コンテンツのViewはこれを監視することで「大元のView」の表示状態を知ることができます*1

@MainActor
final class ShowContentLogController {
    private let isRootViewAppearingSubject = CurrentValueSubject<Bool, Never>(false)

    lazy var isRootViewAppearing: AnyPublisher<Bool, Never> = isRootViewAppearingSubject
        .removeDuplicates()
        .eraseToAnyPublisher()

    func setIsRootViewAppearing(_ appearing: Bool) {
        isRootViewAppearingSubject.send(appearing)
    }
}

ShowContentLogRootModifier

ShowContentLogRootModifierは、表示ログを送る「大元のView」に付与するViewModifierです。

このViewModifierは、

  • ShowContentLogControllerのインスタンスをEnvironmentValuesに設定する
  • ViewModiferが付けられた「大元のView」の表示状態をShowContentLogControllerに伝える

という2つの役割を担っています。

struct ShowContentLogRootModifier: ViewModifier {
    let controller: ShowContentLogController
    @State private var isAppearing: Bool = false

    func body(content: Content) -> some View {
      content
        .onAppear {
            isAppearing = true
        }
        .onDisappear {
            isAppearing = false
        }
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
            if didEnterBackground {
                isAppearing = true
                didEnterBackground = false
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
            if isAppearing {
                isAppearing = false
                didEnterBackground = true
            }
        }
        .onChange(of: isAppearing) { appearing in
            controller.setIsRootViewAppearing(appearing) // 表示状態を ShowContentLogController のインスタンスに伝える
        }
        .environment(\.showContentLogController, controller) // EnvironmentValues に設定する
    }

PostShowContentLogModifier

PostShowContentLogModifierは、表示ログを送りたい各コンテンツのViewに付与するViewModifierです。 このViewModifierは、ShowContentLogControllerのisRootViewAppearing Publisherを監視しつつ、適切なタイミングで表示ログを送る役割を担っています。

また、ViewModifierが付与されたViewが現在表示されているか(isAppearing)、既に表示ログを送ったか(didPostLog)の状態をStateとして保持していて、監視しているisRootViewAppearingがfalseになった時(「大元のView」が非表示になった時)に、既に表示ログを送ったか(didPostLog)の状態をリセットしています。

ShowContentLogControllerRequired(とAppEnvironmentRequired)については「実用段階にするまでに用意した仕組み」の章で詳しく説明します。

struct PostShowContentLogModifier<Category: LogCategory>: ViewModifier {
    let event: Category
    @State private var isRootViewAppearing: Bool = false
    @State private var isAppearing: Bool = false
    @State private var didPostLog: Bool = false

    func body(content: Content) -> some View {
        AppEnvironmentRequired { appEnvironment in
            ShowContentLogControllerRequired { showContentLogController in
                content
                    .onAppear {
                        isAppearing = true
                        if isRootViewAppearing && !didPostLog {
                            postLog(appEnvironment)
                        }
                    }
                    .onDisappear {
                        isAppearing = false
                    }
                    .onReceive(showContentLogController.isRootViewAppearing) { rootAppearing in
                        if rootAppearing {
                            isRootViewAppearing = true
                            if isAppearing && !didPostLog {
                                postLog(appEnvironment)
                            }
                        } else {
                            isRootViewAppearing = false
                            didPostLog = false // didPostLog の状態をリセット
                        }
                    }
            }
        }
    }

    private func postLog(_ appEnvironment: any AppEnvironment) {
        appEnvironment.activityLogger.post(event)
        didPostLog = true
    }
}

設計時に検討したこと

ログを送ったかどうかの管理をどこで行うか

UIKit時代は、表示したコンテンツのIDをViewControllerのSetで管理し、親Viewが中央集権的に各コンテンツのログ送信フラグの管理を行っていました。しかし、SwiftUIでは全てのViewがIdentityを持っており、ログを送るView自身がStateでログを送ったかどうかを管理する方がSwiftUI的に自然だと考えました(加えて子Viewから親Viewに自身の Identifierを伝えて管理させるやりとりも減らすことができて、実装もシンプルになります)。そのためShowContentLogでは、PostShowContentLogModifierを付与したView自身がログ送信フラグの管理を行う設計となっています。

実用段階にするまでに用意した仕組み

ShowContentLogController

isPresented

対応するログの要件

  1. アプリがバックグラウンドからフォアグラウンドに復帰した時に、復帰時の画面に表示されているコンテンツの表示ログを送る

開発中に気付いたのですが、ShowContentLogを使っている画面上でモーダルを表示し、そのモーダルが表示されている状態でバックグラウンド→フォアグラウンド復帰した時に表示ログが送られていました。原因は、モーダルが表示されている状態でバックグラウンド→フォアグラウンド復帰した時にShowContentLogRootModifierのwillEnterForegroundNotificationdidEnterBackgroundNotificationが発火していたことでした。

iOS版クックパッドアプリでは、モーダル表示を含めた画面遷移はUIKitで行われているため、ShowContentLogControllerの初期化時にSwiftUIのViewを表示しているViewControllerを渡して、モーダルを表示しているかどうかを取得するクロージャisPresented: () -> Boolを保持することにしました。

final class ShowContentLogController {
    ...
    let isPresented: () -> Bool

    init(screenViewController: UIViewController) {
        isPresented = { [weak screenViewController] in
            screenViewController?.presentedViewController != nil
        }
    }
}

これをShowContentLogRootModifier内でdidEnterBackgroundNotificationの通知を受け取った時に参照することで、モーダルが表示されている状態でバックグラウンド→フォアグラウンド復帰した時には表示ログを送らないようにしています。

// ShowContentLogRootModifier
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
    if didEnterBackground {
        isAppearing = true
        didEnterBackground = false
    }
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
    // モーダルが表示されていない時だけ処理を行う
    if isAppearing && !controller.isPresented() {
        isAppearing = false
        didEnterBackground = true
    }
}

willRefreshOnForeground

対応するログの要件

  1. 一定時間経過後の画面自動更新やPull to Refreshによる画面更新を行った時は、更新後の画面に表示されているコンテンツの表示ログを送る
  2. アプリがバックグラウンドからフォアグラウンドに復帰した時に、復帰時の画面に表示されているコンテンツの表示ログを送る

iOS版クックパッドアプリの一部の画面では、一定時間経過後に再びタブ切り替えで戻ってきたりバックグラウンド→フォアグラウンド復帰したりすると、画面の自動更新が行われます(自動更新の判定はViewControllerで行われています)。この時UIApplication.willEnterForegroundNotificationが送られるタイミングが自動更新が走るタイミングより早いので、バックグラウンド→フォアグラウンド復帰時に自動更新が走る場合は、更新前後で古いコンテンツの表示ログと更新後のコンテンツの表示ログが2回送られてしまっていました。

この場合は更新後の表示ログのみを送りたいので、ShowContentLogControllerに画面の更新が予定されているかどうかを取得するwillRefreshOnForeground: () -> Boolというクロージャを保持し、バックグラウンド→フォアグラウンド復帰時に画面の更新が予定されている場合は更新が終わるまでisAppearingの変更を待つようにしました。

final class ShowContentLogController {
    ...
    let willRefreshOnForeground: () -> Bool

    init(screenViewController: UIViewController, willRefreshOnForeground: @escaping () -> Bool = { false }) {
        isPresented = ...
        self.willRefreshOnForeground = willRefreshOnForeground
    }
}
// ShowContentLogRootModifier
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
    if didEnterBackground {
        // 画面をリフレッシュする場合はリフレッシュを待ってから isAppearing を true にする
        if !controller.willRefreshOnForeground() {
            isAppearing = true
        }
            didEnterBackground = false
        }
    }
}

ShowContentLogRootModifier

forceDisappear

対応するログの要件

  1. タブの切り替えによって画面が再度表示された時に、その画面に表示されているコンテンツの表示ログを送る

forceDisappearをtrueにするとisAppearingの値を常にfalseにしてShowContentLogControllerに伝えることができます。

TabContentView(...)
    .showContentLogRoot(
        controller: tabState.showContentLogController,
        forceDisappear: selection != tabState.tabType // 違うタブが選択されている時は `isAppearing` の値を常に false にする
    )
// ShowContentLogRootModifier
.onChange(of: forceDisappear) { newForceDisappear in
    controller.setIsRootViewAppearing(!newForceDisappear && isAppearing)
}
.onChange(of: isAppearing) { appearing in
    controller.setIsRootViewAppearing(!forceDisappear && appearing)
}

元々はiOS14のTabViewで、選択されていないタブのonAppearが呼ばれることがあり、それを回避するために生まれました。それ以外にも、OSバージョン問わずタブの選択が完全に切り替わっていない時(スワイプで隣のタブが少しだけ見えている状態)にも隣のタブのonAppearが呼ばれていたので、iOS14のサポートを終了してからもこの指定は続けています。

ちなみにsetIsRootViewAppearing自体をスキップしてしまうと、各種イベントの発火タイミングによっては本来送られるべきログが送られなくなってしまう可能性があるため、このように値を上書きしてsetIsRootViewAppearingを呼ぶ方法を取っています。

isRefreshing

対応するログの要件

  1. 一定時間経過後の画面自動更新やPull to Refreshによる画面更新を行った時は、更新後の画面に表示されているコンテンツの表示ログを送る

更新時にdidPostLog(ログを送ったかどうか)をリセットするために用意されているpropertyです。

現在レシピサービス開発部でSwiftUIで新規画面を開発する時は、下記のScreenStateのようなものを使って画面の状態管理をしているので、このpropertyを使う必要はありません(画面更新時はScreenStateloadingloadedとなりloadedに対応するViewが再生成されるため)。

enum ScreenState<T, E: Error> {
    case initial
    case loading(T?)
    case loaded(T)
    case error(E)
}

例えば画面の表示切り替えをZStack内のViewのopacity変更で行っていて、画面更新時にViewが再生成されない場合にこのpropertyを使ってdidPostLogをリセットすることができます。

// ShowContentLogRootModifier
var isRefreshing: Bool
...
    .onChange(of: isRefreshing) { refreshing in
        isAppearing = !refreshing
    }

PostShowContentLogModifier

ShowContentLogControllerRequired

ShowContentLogControllerRequiredは、PostShowContentLogModifierを付けたViewよりも上の階層でShowContentLogControllerのインスタンスがEnvironmentValuesに設定されていない(つまりShowContentLogRootModifierを付け忘れている)時にassertionFailureを起こすためのViewです。

import SwiftUI

struct ShowContentLogControllerRequired<Content: View>: View {
    @Environment(\.showContentLogController) private var showContentLogController: ShowContentLogController?
    private let content: (ShowContentLogController) -> Content

    init(@ViewBuilder content: @escaping (_: ShowContentLogController) -> Content) {
        self.content = content
    }

    private func noEnvironment() -> some View {
        assertionFailure("You must pass the showContentLogController from a parent or ancestor view. If you use postShowContentLog modifier, add showContentLogRoot modifier to a parent or ancestor view.")
        return EmptyView()
    }

    var body: some View {
        if let showContentLogController = showContentLogController {
            content(showContentLogController)
        } else {
            noEnvironment()
        }
    }
}

AppEnvironmentRequiredも同じような実装となっています(むしろShowContentLogControllerRequiredが先に実装されたAppEnvironmentRequiredの実装を参考にしています)。

AppEnvironmentRequiredがクロージャの引数に渡しているappEnvironmentは、iOS版クックパッド上で用意されている依存関係を取り出すためのDIコンテナで、PostShowContentLogModifier内ではappEnvironmentを用いて行動ログを送るための依存にアクセスしています。

appEnvironmentについては下記に詳しい説明があります(記事内ではEnvironmentと呼ばれています)。 https://techlife.cookpad.com/entry/2021/06/16/110000

onPostLog

onPostLog は「自動生成行動ログ」が送られるタイミングで呼び出されるクロージャで、「自動生成行動ログ」とは別のログを追加で送るためのものです。

// PostShowContentLogModifier
private let onPostLog: ((any AppEnvironment) -> Void)?
...
private func postLog(_ appEnvironment: any AppEnvironment) {
    appEnvironment.activityLogger.post(event)
    onPostLog?(appEnvironment)
    didPostLog = true
}

完成版のコード

以下が、「実用段階にするまでに用意した仕組み」を踏まえた完成版のコードです。

*動作確認時の環境: Xcode 14.1、iOS Deployment Target 15.0

ShowContentLogController

import Combine
import SwiftUI
import UIKit

@MainActor
final class ShowContentLogController {
    private let isRootViewAppearingSubject = CurrentValueSubject<Bool, Never>(false)
    lazy var isRootViewAppearing: AnyPublisher<Bool, Never> = isRootViewAppearingSubject
        .removeDuplicates()
        .eraseToAnyPublisher()

    let isPresented: () -> Bool
    let willRefreshOnForeground: () -> Bool

    init(screenViewController: UIViewController, willRefreshOnForeground: @escaping () -> Bool = { false }) {
        isPresented = { [weak screenViewController] in
            screenViewController?.presentedViewController != nil
        }
        self.willRefreshOnForeground = willRefreshOnForeground
    }

    func setIsRootViewAppearing(_ appearing: Bool) {
        isRootViewAppearingSubject.send(appearing)
    }
}

private struct ShowContentLogControllerKey: EnvironmentKey {
    static let defaultValue: ShowContentLogController? = nil
}

extension EnvironmentValues {
    var showContentLogController: ShowContentLogController? {
        get { self[ShowContentLogControllerKey.self] }
        set { self[ShowContentLogControllerKey.self] = newValue }
    }
}

ShowContentLogRootModifier

import SwiftUI

private struct ShowContentLogRootModifier: ViewModifier {
    private let controller: ShowContentLogController
    private var isRefreshing: Bool
    private var forceDisappear: Bool
    @State private var isAppearing: Bool = false
    @State private var didEnterBackground: Bool = false

    init(controller: ShowContentLogController, isRefreshing: Bool, forceDisappear: Bool) {
        self.controller = controller
        self.isRefreshing = isRefreshing
        self.forceDisappear = forceDisappear
    }

    func body(content: Content) -> some View {
        content
            .onAppear {
                isAppearing = true
            }
            .onDisappear {
                isAppearing = false
            }
            .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
                if didEnterBackground {
                    // 画面をリフレッシュする場合はリフレッシュを待ってから isAppearing を true にする
                    if !controller.willRefreshOnForeground() {
                        isAppearing = true
                    }
                    didEnterBackground = false
                }
            }
            .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
                if isAppearing && !controller.isPresented() {
                    isAppearing = false
                    didEnterBackground = true
                }
            }
            .onChange(of: isRefreshing) { refreshing in
                isAppearing = !refreshing
            }
            .onChange(of: forceDisappear) { newForceDisappear in
                controller.setIsRootViewAppearing(!newForceDisappear && isAppearing)
            }
            .onChange(of: isAppearing) { appearing in
                controller.setIsRootViewAppearing(!forceDisappear && appearing)
            }
            .environment(\.showContentLogController, controller)
    }
}

extension View {
    func showContentLogRoot(controller: ShowContentLogController, isRefreshing: Bool = false, forceDisappear: Bool = false) -> some View {
        modifier(ShowContentLogRootModifier(controller: controller, isRefreshing: isRefreshing, forceDisappear: forceDisappear))
    }
}

PostShowContentLogModifier

import SwiftUI

private struct PostShowContentLogModifier<Category: LogCategory>: ViewModifier {
    private let event: Category
    private let onPostLog: ((any AppEnvironment) -> Void)?
    @State private var isRootViewAppearing: Bool = false
    @State private var isAppearing: Bool = false
    @State private var didPostLog: Bool = false

    init(event: Category, onPostLog: ((any AppEnvironment) -> Void)?) {
        self.event = event
        self.onPostLog = onPostLog
    }

    func body(content: Content) -> some View {
        AppEnvironmentRequired { appEnvironment in
            ShowContentLogControllerRequired { showContentLogController in
                content
                    .onAppear {
                        isAppearing = true
                        if isRootViewAppearing && !didPostLog {
                            postLog(appEnvironment)
                        }
                    }
                    .onDisappear {
                        isAppearing = false
                    }
                    .onReceive(showContentLogController.isRootViewAppearing) { rootAppearing in
                        if rootAppearing {
                            isRootViewAppearing = true
                            if isAppearing && !didPostLog {
                                postLog(appEnvironment)
                            }
                        } else {
                            isRootViewAppearing = false
                            didPostLog = false
                        }
                    }
            }
        }
    }

    private func postLog(_ appEnvironment: any AppEnvironment) {
        appEnvironment.activityLogger.post(event)
        onPostLog?(appEnvironment)
        didPostLog = true
    }
}

extension View {
    func postShowContentLog<Category: LogCategory>(_ event: Category, onPostLog: ((any AppEnvironment) -> Void)? = nil) -> some View {
        modifier(PostShowContentLogModifier(event: event, onPostLog: onPostLog))
    }
}

まとめ

今回ご紹介したShowContentLogによって、SwiftUIの画面でもUIKit同様に各コンテンツの表示ログを送ることができるようになりました。また、UIKit時代は画面を作る度に一から表示ログの実装が必要だったのですが、仕組みを作ったことでSwiftUIでは表示ログを簡単に送ることができるようにもなりました。

仕組みを作る中で様々な意見・指摘をくれたチームの同僚に感謝します。この記事が、SwiftUIを使った画面で行動ログを送る際の参考に少しでもなれば幸いです。

*1:isRootViewAppearingを最初computed propertyにしていたのですが、それだとSwiftUIのViewのbodyが再実行される度にonReceiveで毎回新たなPublisherのインスタンスを購読するという挙動になりremoveDuplicatesが効かなくなってしまうので、lazy varで宣言しています。

【後編】企業所属のRubyコミッター対談! 〜Ruby開発の裏話と今後の取り組み〜

こんにちはCTO室の緑川です。今回はアンドバッドさんが主催しているPodcast「ANDPAD TECH TALK」のゲストに弊社の@mameが出演した記事の後半です。Podcastとしてお聞きしたい方は下記のアンドパッドさんの記事からお聴きください。

tech.andpad.co.jp

前編の記事はこちらです。

【前編】企業所属のRubyコミッター対談! 〜企業に所属するOSS開発者って何?〜 - クックパッド開発者ブログ

トーク本編

櫻井:皆さま、こんにちは。アンドパッドの開発本部でエンジニアリングマネージャーをしている櫻井です。

櫻井:13回目のANDPAD TECH TALKです。ANDPAD TECH TALKはアンドパッドの開発チームの中の人をゲストに招いて、あれやこれやお話しするカジュアルなテック系Podcastなのですが、今回は前回に引き続き社外ゲストをお招きしたスペシャル会の後編となっております。企業に所属するRubyコミッターであるお二人をお招きしています。

櫻井:アンドパッドからはフェローでありRubyコミッターの柴田さん。対談相手はクックパッド株式会社所属のRubyコミッターであるmameさんこと遠藤侑介さんをお呼びしています。前回はお二人の今までの経緯、Rubyコミッターが普段どんなことをしているのかをお話いただき、非常に良いところで後編となっていたところです。今回はRuby開発の裏話と今後の取り組みなど深振りをさせていただきたいと思っております。それでは後編をお聞きください。

Ruby開発の裏話

櫻井:まずは遠藤さんからお伺いしますが、開発裏話みたいなものはありますでしょうか?

遠藤:クックパッドでRubyをめちゃくちゃ現場で使っている職場に転職したから、現場感がつかめてなかったので、よくわからなかったところがうまくいくようになったのはちょっとあったりしますね。

遠藤:最初に述べたカバレッジの測定をする機能を担当しているんですけれども、カバレッジの測定を止めたり再開したりする機能が欲しいという要望が以前Rubyに来たことがあったんですけれどもその時は必要性がよくわからなかったんですね。実装するのも大変なので断っていたんですけども、クックパッドで働くようになって、現場で働いている人 から同じ問題で困っているという話を聞くことができて、どういうふうに困っているのか理解がちゃんとできたので今回対応することにし、oneshot coverageという機能を導入するようにした話があったりします。

柴田:その話で言うと、RubyコミッターRubyのコード書いていない問題みたいなものをよく言われたりしています。Cプログラマーの皆さんだから会社の中でRubyをどう使って何を書いているかとか、またRailsをメインに開発されている人とかRailsを使ってアプリケーションを開発している人とかが「Rubyでこういうことができるといいんだけどな」みたいな部分とあとRubyを開発しているRubyのコミッターの人たちが「こういうRubyのコードは書かないでしょ」って言った矢先に「書きますよ」みたいな話とか、逆にこう書けたら便利じゃないって「誰も書きませんよ、そういうこと」みたいな話は結構イベントとかSlackとかそういったところでよく散見されたりするのはあるあるネタですよね。

遠藤:そうですね。本当に現場感がない人がRubyを作っているっていうのはちょっと問題としてはあったりしますね。

柴田:ただ最近はShopifyのエンジニアであるとか、あとはよく喋る場みたいな部分が割と増えてきたような気はします。スタートアップ中心にRubyを採用している会社も結構な数があったりするので、「こういうことができるといいんだけどなー」みたいな部分とか拾ったりヒアリングしたりしやすくなったりしているのかなというのはありますよね。

遠藤:ありますね。

柴田:最近のライブラリというかプログラミング言語のGoとかRustみたいな言語はライブラリ自体がGoとRustで書いてあるというような言語なので、初学者の方とかが最初にGoとかRustをやりましょうみたいなときに割りかし開発を始めるまでのつまづくステップが比較的ないんですよね。それに比較するとRubyはC言語で書かれていて、C言語を動かすためにはコンパイラーとコンパイルした後の実行パイナリを実行する場所が必要になっていて、その辺の組み合わせで動かないとかビルドできないとか、何かプログラムを書こうと思ったんだけどプログラムを書くまでに1日とか何なら数日かかってしまう。Googleで検索しても何かエラーメッセージが出てこないみたいなことが増えていますと。

柴田:あとはAppleのmacOS、MacBookのアーキテクチャーがガラッと変わったことでいろいろビルドできない問題が引き続き多いとか、ARMのCPUの上では何かうまく動かないとかそういったいろいろな社会のコンピューティング環境の変化にともなって、自分が使いたいものがすぐ使えない、みたいな部分に散見されるなっていう問題があります。僕はいろいろやってはいるんですけど、その辺のプログラムを書こうと思ったときにすぐ書けるようになるみたいな部分の時間をとにかく小さくしたいと思っているので、その辺をいろいろやったり、前回遠藤さんの方から紹介があったkateinoigakukunさんがmacOSについてすごい詳しくて本当に助かったんですけれどもその辺のmacOSでビルドできない問題、動かない問題みたいな部分もいろいろ複数人で協力しながら解決していったりしているっていうのが現在進行系の話になってますね。

開発者に知ってもらいたいこと

櫻井:ありがとうございます。Ruby開発の裏側を聞いてきましたが、開発者に知ってもらいたいことなどがあれば、お二人からお伺いしたいんですけれどもいかがでしょうか?

柴田:はい、そうですね。昨今のプログラミング言語界隈の流れというか流行りみたいな部分の話をちょっとしたいんですけど、VS Codeと呼ばれるエディターが割とメインというかメジャーな存在となっていて、VS CodeはTypeScriptであるとか、Go言語であるとか、最近だったらRust言語みたいなもののサポートが非常に豊富なんですよね。

柴田:それもちろんMicrosoftが今すごい投資をしているんですけど、開発者体験という言葉があってデベロッパーエクスペリエンスというんですけど、開発者が何かをしようとした時に「うっ」てつまずかないように、なおかつ、こういうものを書きたいと思った時にスラスラっと書けます、テストも実行できます、不具合があった場所を見つけますみたいなものをできる限り提供していこうというのがどのプログラミング言語でも非常に重要視されています。

柴田:RubyもVS Codeのサポートであるとか、そういった型システムみたいな部分については少しずつ頑張っているんですけれども、やはり他の言語と相対的にはまだまだだなという部分があります。その中でも2022年にリリースするRubyのバージョン3.2でも今話したようなエラーを見つけましょうとか、そういう開発者にとってスラスラっとRubyのコードを書けるようにするための機能がいくつかあるので、その機能の開発を頑張っていた遠藤さんに詳細を聞くといいんじゃないかなと思います。

遠藤:僕がRuby3.2の中で新しくやったこととしてはRubyの例外が出たときに「このコードのせいでおそらく例外が出てるんじゃないの?」というのをエラーメッセージでサジェストをするという機能を少し拡充するのをやってみました。ErrorHighlightと呼ばれる機能なんですけども、それを拡充していました。また僕が作ったやつ以外にもSyntaxSuggestっていう機能がRuby3.2に増えまして、Rubyでコードを書いててありがちなのはendでステートメントを区切る言語なんで、endを書きすぎたり、逆にendが足りなかったりした時にどこにendが足りなかったのかというのはよくわからなくなりがちなんですよね。

遠藤:シンタックスサジェストとはコードの構造を大きく抜粋して「おそらくここにendが多すぎるんじゃないか」とか「足りないんじゃないか」というのをエラーメッセージの中にヒント情報として出してくれるという拡張が行われていて、これもエラーがでた時に開発者がどこを直せばいいのかというのをサジェスチョンしてくれるという機能がちょっとずつ増えています。

柴田:今の遠藤さんのお話はRubyのコードを実行したときに「この辺がエラーではないか?」というのを開発者の方にすぐお知らせするというような機能だったりするんですけど、他にもRubyの開発会議とか開発のこういうふうな変更を加えてはどうかみたいな時にも割と新しい機能を入れたりこういうメソッドを入れて警告を出すとか、「この辺が良くないのでは?」みたいなのを教えた方がいいんじゃないっていう提案が来るたびにじゃあそのエラーメッセージなり警告メッセージをプログラマーが見て「なにかできることはあるの?」みたいな話をしたら「いや、ないかも」みたいな話とか結構開発の方針を決めたりするときはあるあるネタです。メッセージを出したりプログラミング言語の動きとして、それを使った人が、「じゃあそれ見てなんかできることはあるの?」とか逆に「こうすればもっと良くなる」みたいなことをすぐ適切に知らせるにはどうしたらいいのかみたいなのは本当にRubyコミッターの中でも熟考というか結構紛糾しがちなネタだったりします。

柴田:例えばワード1個をセーフって言い切ってしまっていいのかみたいな話とかでも、1時間とか2時間とか議論して「いやこれセーフって言うとダメでしょう」とか「いやじゃあなんて言えばいいの」みたいな話とかは結構あるあるネタだったりしますよね。

遠藤:名前はね、本当にデベロッパーエクスペリエンスに直結するところなので、だいぶ長く議論をしますね。その結果がやや不自然な結果になることがあるんですけど本当に熱意を持って設計されているところだと思います。

今後の計画

櫻井:なるほど。ありがとうございます。さまざまなお話を伺ってきまして、ぜひ今後のお話も伺いたいと思うんですけれども、今後お二人がやっていきたいRuby開発にはどんなことがあるでしょうか? もし計画などがあればぜひ教えていただきたいです。

柴田:はい。計画はないのでそれぞれが勝手にやりたいことをやっている、という話のあとに計画の話をするというのもあれなんですけど、僕が思っている部分としては、やっぱりRubyを開発する人が、開発をもっとしやすくなるようにできるといいなと思っている部分があるのでそこの部分の支援ですね。

柴田:具体的にはRubyがちゃんとサポートしている動く場所、コンピューターの上として10個とか20個とか、Linuxの上であるとかmacOSの上であるとかWindowsの上であるとか、そういったいろんな部分で動かせるようにしましょうというのをユーザーと約束していて、ちゃんとそこの上で動かせるようにするというのをやっているんですけど、何かの変更を入れた時にWindowsは動きませんでしたとか、Windows向けに入れた変更はmacOSではダメでしたみたいなことがよくあるんですね。

柴田:ただ、やはりRubyコミッター1人1人が持っているコンピューターは1個とか2個とかに限られているので、あるRubyコミッターがサポートするよって約束している10個とか20個とかの環境に即座に自分の目の前のコードを実行したりテストできるようにするみたいな部分を来年何かしら用意したいなというのが1個目の野望というか計画で、2つ目はリリース作業をもっと楽にしたいと思っていて現状は僕と遠藤さんとあとは3、4名のリリース担当のRubyコミッターと呼ばれる人たちが18時ぐらいから大体いつも22時から23時過ぎまでいつもリリース作業と呼ばれる作業をします。皆様に最新のRubyをご提供みたいな仕事をしているんですけど、もうなんかやるたびに(少しずつ良くはなっているんですけど)ほぼみんながもうやりたくないなっていう風に考えて、また次頑張るみたいな消耗戦を繰り広げているので、もう念じたらリリースできるというくらいまではちょっと頑張りたいなと思っているところです。だからいろいろ問題があるんですよね。

遠藤:不思議ですよね。リリースは本当にびっくりするぐらい何かしら必ずはまるという感じで。

柴田:なんか解決したはずなのに新しい問題がまた起きるみたいなことを本当に繰り返していて、誰かがサボリとか悪意を持ってやってしまったとか、そういう話ではなくて本当に新しい技術的な課題が毎回起きていて、そのたびにちゃんとこれはポストモーテムして対策を入れるみたいのを毎回少しずつ対策してるんですけど毎回新しい問題が起きるんですよね。

遠藤:普通にソースコードをtarballにまとめるだけでなんでこんなにハマるんだっていう風に思う人もいるかもしれませんけど、本当になぜかtarballに固めたバージョンだけで発生するバグとかが毎回ちょっとずつ混入するのでパッケージを作ってテストしてみたら失敗するとかっていうのが発生するんですよね。なので、いざ本当にリリースバージョンを作ったら新しい問題が分かるのでそこから慌てて直すとかっていうのが何かしら発生する感じで大変ですよね本当に。

柴田:ですよね。Webサービスの場合は自分たちが面倒を見て全責任を持っているコンピューターの上で動かすようにソフトウェアをリリースしますっていう感じなんですけど、Rubyとかプログラミング言語の場合は自分たちの外に向かってソフトウェアをリリースするみたいな部分です。なので、使う人の数だけそのソフトウェアが動く場所があって可能な限りそれを広くカバーしたいけどカバーしきれないこともあって「とあるソフトウェアがこういうバージョンだったらビルドできませんでした。これあかんみたい」な話とかを何回も繰り返しています。そこの大変さをちょっと広い目っていうか大きいスコープで捉えて解決するような仕組みを用意することでリリースとかはもっと毎月でもバンバンやっていいんじゃないのくらいのほうが、短いサイクルのほうがちょっとした不具合があっても「3ヶ月待たないとRubyは直らないんだよな」から「来月直るし」みたいなほうがユーザーにとってはおそらくいいことだろうし、開発する側にとっても何かミスっても来月直せばええやみたいな感じになって、みんなが楽になると思うので、少しずつがんばりたいなっていうのは僕の2023年の目標活動に入れています。

遠藤:ほんとリリース頻度上げたいですよね。定期に3、4ヶ月くらいしたら1回入れるかどうかっていう。

柴田:年3、4回ですもんね。

遠藤:nodeとかは実質的に2ヶ月に1回リリースしてるみたいなので、そういうのをまねしていきたいですよね。

柴田:そのくらいになりたい、、、隔月くらいになりたいですよね。

遠藤:隔月くらいになるとユーザーも「そろそろRubyの新しいバージョンが出るらしい」みたいな備えができるかなと思っていて、いいなと思ってます。

柴田:それに雑に壊れても、次回直せばいいなみたいな感じにもなれると思うので、やっぱり儀式化すると挑戦する意欲を高めるのにもすごい時間がかかるし、やってしまったときの「あー」みたいな気持ちもすごい高まるので、その辺の敷居はどんどん雑に下げていきたいですね。遠藤さんも何かあるんですか?

遠藤:そうですね。最初のほうに話したTypeProfっていうやつを今まで作ってきてるんですけど、今年はですね今の実装のアプローチ、バイトコードを解析するというベースのアプローチだとちょっと限界を感じてきていて、抽象構文木ベースで解析し直すように作り直すというのを考えています。そのためにまずParserをどうにかするという必要があって、その辺りをShopifyの人たちがやってくれているのがそろそろ形になってきているのでそれをベースに2023年に作り直したいなというふうに思っています。それによってVS codeでのRubyの対応が弱いとかって言われているのを改善するように1つの提案ができたらなというふうに思っております。

個人の開発者がRuby開発へ貢献できる方法

櫻井:では最後にですね、ここまで聞いてきたリスナーで自分もRuby開発に貢献したいと思ったエンジニアが結構いるんじゃないかなと思ってるんですが、どうやったら個人の開発者がRuby開発へ貢献できるのでしょうか?

柴田:一番簡単な方法はとにかくRubyを使うっていうのがあって、手元の仕事で開発しているソフトウェアであるとか仕事じゃないソフトウェアもいっぱい皆さんのお手元にあると思うんですけど、その辺のコードをとにかくRubyで実行するということがまず最初の一歩です。

柴田:2つ目はですね、ここがちょっとアクションが必要になってくるんですけど、その動かした結果をRubyの開発チームに伝えるっていうのがポイントだと思っていて、動いたっていうことも重要なんですよ実は。

柴田:Rubyの開発チームはPreviewバージョンとRCバージョンっていうのを1年間に2、3回リリースするんですけど、その2、3回リリースしたPreviewバージョンを使って自分の手元のコードを実行してみて動いたら動いたって言ってほしいですし、動かなかったらこういうエラーが出て動かなかったっていうのを教えてほしい。どちらも実は重要でまず実行してもらわないことには不具合なり、そのちゃんと正しいっていう動きも我々が知ることができませんし。で実行した後に動いた動かないっていう状況を伝えてもらわないことには我々はそれを知ることができないっという二段構えになってまして、本当に本当に大事で、ベータバージョンを出してみんながテストしてくれたから、大丈夫だったと思ってベータじゃない正式バージョンをリリースしたら全然動きませんでした。「なんでだ?」「ベータバージョンは誰も実行していなかったからだ」みたいなのは本当ソフトウェアあるあるな話なので、とにかくベータバージョンみたいなものを触ってみて「なんだこれ?」みたいなものがあったら動かなかったっていう報告であるとか、あとこの動きはちょっとおかしいんじゃないとかもぜひ教えてもらいたいなと思います。

柴田:それで、教えてもらう方法もできる限りいろんな窓口を用意していて、メインで使ってるのはRedmineっていう課題管理ソフトウェアなので、Redmineの方で報告してもらうっていうのが一番の王道というかメインの手段なんですけど、それ以外にも例えばSlackにRubyのグローバルなコミュニティーとかもあったりするんですけど、そこの部分で動かなかったとか動いたみたいなものを僕とか遠藤さんにお伝えてもらってもいいですし、なんかTwitterとかそういう類似のソーシャルネットワークのサービスでメンションして、このコードが動きませんでしたみたいなことを伝えてもらってもいいですし、何かしらの手段でとにかくRubyの開発をしている「Rubyコミッターです」って名乗っている人たちに伝えるっていうのが一番最初にまずできることだと思いますね。

柴田:で、第3ステップ目がちょっとハードルが上がるのかなと思うんですけど「なんだこれ」みたいな動きに対して「こうした方がいいんじゃないのか」っていうようなコードを書いて、それをGithubなりのプルリクエストでサブミットするとかRedmineの方にコードの断片をパッチとして貼り付けて投稿するとか、そういった部分を繰り返していくっていうのがRuby開発への貢献。Rubyのコミッターサイドとしてすごいありがたいなっていうような動きになるのかなと思います。

柴田:皆さんが会社で行われてるような陸続きだと思っていて、RubyコミッターはただRubyを開発してる人でしかないので、チーム開発とかサービスを開発するときに隣の人が作った機能を実行してみたら動かなかったんだけどみたいなことだったら、皆さんは多分すぐSlackとかGitHubとか何かしらのチャットツールとかで動かなかったよって伝えると思うんですね。本当それと同じようなノリでいいと思っていて、ほんとまつもとゆきひろさんにTwitterで「これ動かないんですけど」みたいなって言うくらいでもいいと思って、まつもとさんすごいフレンドリーなので、まあそういう形でこうどんどん伝えて一緒に作っていくっていうのがRubyの魅力だと思うのでぜひなんかやっていただけるといいのかなと思います。

遠藤:そうですね。本当に動かなかったときに誰にも言わないで諦めてしまうっていうのが一番残念で、誰かが困った問題では他の人が困るのでいったん声を上げるっていうのが重要だと思います。声を上げるのもできればTwitterで誰ともなしに語るだけではなくて、僕らのようにRubyの開発やってる人になんとか伝わる形で、一番簡単なメンションとか、より理想的にはやっぱりバグトラッカーに報告をするっていう形で伝えてもらえたら嬉しいなと思います。本当に時々あるんですけどTwitterで動かなかったというツイートを誰かコミッターが拾って直すっていう対応をすることもあるんですけども たぶんほとんどのやつは気が付かずに流れていってると思うので伝えてもらえると嬉しいなと思います。個人の開発者がRuby開発へ貢献できるかっていう話だと、RubyはCで書かれているのでちょっとハードルが高いっていう風におっしゃる方が時々見かけるんですけれども、そんなに難しく考えなくても(もちろんCが書けるに越したことはないと思うんですけども)そのようにバグ報告をするっていうのも重要な貢献ですし、特に機能提案に関してこういうユースケースでこの機能が欲しいとか、この機能提案だとこういうケースで問題があるだろうという議論に参加するという形の貢献もあると思います。実際にそのRubyのコードに手を動かして貢献したいっていう時も何かしら声をかけてもらえれば課題を紹介したりとか書こうとしているプログラムを手伝ったりとかもできると思いますので、やっぱりこれも声をかけてもらうというのがすごく重要かなと思います。

櫻井:ありがとうございます。思っているだけではなくて、アクションに起こすことでRuby開発への貢献もできるし、コミッターへの道も開けるのではないかと。皆さんぜひどうしても見構えてしまう方もいるのかなというところがあるので、そういったところはちょっと一旦置いておいて、ちょっとした勇気を持ってコミュニケーションを取ってみるとそこから先に進めるのかなという気がしました。

遠藤:そうですね。自分が実際にRubyを使っていて、こういう機能が欲しいっていう思いから貢献してもらうのがベストではあるんですけども、何かやりたいけれども特にアイデアがないっていう時には、時々Google Summer of CodeとかRubyのコミュニティからこういう課題があるっていうのを紹介することがあるので、それを参考にこれだったら自分ができるかもというのを選んでもらうというのもいいかなと思います。

遠藤:この間柴田さんが書いたブログの記事とかね。そういう感じでRubyの長い懸案になっている課題みたいなのを紹介することもあるのでそういうのを考えてみてもらえといいかなと思ったりします。

柴田:そうですよね。結構発信してなかったなと思ったんで、こういうことをやりたいとか。プログラミング言語業界の懸案事項って実は結構あって、どの言語でも実は今この問題があって、どの言語も解決できてないとか、ある言語だけこういうアプローチで解決できているとか、何なら無視しているとか、結構そういうのはRubyコミッターの中では議論として出たりしているんで、そういったものをできる限り開発ネタとして発信をして意欲のある人が「えいっ」て頑張って作るみたいな部分でネタを提供したりもできればいいかなと思ってますね。

遠藤:そうですね。バグトラッカー上で議論しているのを頑張っておりますが、ちゃんとまとまってないのでどういう課題があるかというのを一覧できないのがやっぱり難しいですね。発信していかないとですね。

櫻井:ありがとうございます。今回も本当にいろいろな裏のお話だったりとか、深いお話もお伺いできたかなと思うのですが、そろそろお時間となりますので、今回のANDPAD TECH TALKとしては以上で終了とさせていただきたいと思います。

さいごに

Rubyコミッターによる対談はいかがだったでしょうか?クックパッドではサーバーサイドやOSSに関わりたい仲間を募集しています。Rubyについてもう少し詳細を知りたい方はカジュアル面談も実施していますので、ご興味のある方はぜひ気軽にご連絡ください。

cookpad.careers