マイクロサービスアーキテクチャ 第2版では2週間で作り直せるサイズが良いという記述が削除されている
社内でマイクロサービスのサイズについての議論になり、ふと気になってマイクロサービスアーキテクチャ 第2版を確認すると削除されていたことに気付いたよ、というのがこの記事で最も言いたかったことです。*1
以下蛇足です。
マイクロサービスアーキテクチャ 第1版ではマイクロサービスの特徴として、簡単に作り直しができる(2週間で作り直せる程度)ほど十分に小さい点が挙げられていました。*2
マイクロサービスは小さければ小さいほど良いという言説は、マイクロサービスアーキテクチャ 第1版の記述を根拠としていることが多かったように思います。
また、マイクロサービスアーキテクチャ 第1版で挙げられた際に引用していたのは以下のブログだと思います。*3
対して、マイクロサービスアーキテクチャ 第2版ではサイズに関するセクションはあるものの、2週間で作り直せるサイズという記述は削除されていました。代わりに以下のような主張が行われています。
- サイズという概念は最も関心の低い特性の1つ
- 最適なコードベースのサイズはチームや個人など状況に強く依存する
- コードベースのサイズではなく、インターフェースのサイズを小さくすべき
感想
このように第1版と第2版で主張がガラッと変わる書籍はなかなかないのではないでしょうか。 それだけここ数年でマイクロサービスアーキテクチャに関する知見が集まってきたのではないかと思います。
ここ1年くらいでモノリスからマイクロサービスへの書籍がグッと増えたような気がする。先行的に取り組んできた組織がだいたいひと段落して体系的にまとめられる段階に来たということなんだろうか
— てっぺー (@euglena1215) 2023年2月21日
rubocop-ast.wasm を作った
便利そうなツールを作ったという紹介です。
Ruby 30th LT に出したのですが落ちたので供養として書きました。
解決しようと思った課題
Ruby には RuboCop という linter 兼 formatter があります。 RuboCop に元々入っているルール(Cop と呼ばれます)だけでも十分に便利なのですが、自身の Ruby プロジェクトに合わせて独自のルール(Custom Cop)を作ることができます。
Custom Cop の作り方:
- Rubocop でカスタムルールを作る - MoneyForward Developers Blog
- https://sinsoku.hatenablog.com/entry/2018/04/24/02291
- RuboCop の Cop の実装について - Qiita
記事に書かれているように、 ちょっとした実装で Custom Cop が作れるようになっています。しかし、自分を含む普段 Web アプリケーションを作っているエンジニアには親しみの薄い概念が登場します。それは S式(AST)です。*1
RuboCop は対象となるプログラムを parse した AST に対して定義されているルールに一致する AST を見つけ、該当箇所に対して linter として怒るという仕組みになっているので、Custom Cop を作るためには AST を表現する必要があることは理解できます。しかし、難しい…
RuboCop に精通した人であれば以下のようなS式を見て
(send
(send
(const
(cbase) :SomeKlass) :new
(int 1)
(str "foo")
(splat
(send nil :args))) :hoge_method)
「ああ、これはこんなプログラムだな」と理解できるものなのでしょうか。自分には難しすぎる…
::SomeKlass.new(1, "foo", *args).hoge_method
こういった難しさには既視感があるなと思い、思い出したのが正規表現でした。自分も最初は正規表現にとっつきにくさを感じ悪戦苦闘しながら使っていましたが、今ではある程度簡単な正規表現であれば「ああ、こういう文字列とマッチしそうだな」と理解できるようになっています。
なぜそうなったかで思い出すと、体が覚えるまで 正規表現を書いてみる → テスト文字列と一致しているか確かめる → 正規表現を修正する → ... といったトライアンドエラーを繰り返すのが有効だったように思います。
とっつきにくい RuboCop の AST(S式)に対して楽にトライアンドエラーを繰り返せるようになることで、正規表現と同様に「気付いたらなんとなく理解できるようになっている」状態にするにはどうしたらいいか?を考え始めました。
既存の解決方法
既存の方法としては、RuboCop の Cop の実装について - Qiita で紹介されている pocke/rpr が挙げられます。これはプログラムを渡すとS式を返すコマンドラインツールで、以下のような使い方ができます。
% rpr -e "::SomeKlass.new(1, "foo", *args).hoge_method" -p rubocop
s(:send,
s(:send,
s(:const,
s(:cbase), :SomeKlass), :new,
s(:int, 1),
s(:send, nil, :foo),
s(:splat,
s(:send, nil, :args))), :hoge_method)
上級者がパッとS式を確認するのにピッタリなツールのように感じました。しかし、初学者観点だと以下のような特性も持ち合わせているとより使い勝手の良いツールになりそうだと考えました。
- プログラムからS式への変換だけでなく、S式から一致するテストプログラムを確かめられる双方向性
- 入力するたびに変換が行われるようなインタラクティブ性
1,2 の特性を踏まえると、コマンドラインツールよりも Web ツールの方が良さそうです。 そこで、1,2 の特性の持つ Web ツールを作成してみることにしました。 また、個人的なチャレンジとして Ruby 3.2 からの wasm サポートを利用し frontend で完結するスタンドアローンな Web ツールを目指しました。
作ったもの
rubocop-ast.wasm を作成しました。

ソースコード: https://github.com/euglena1215/rubocop-ast.wasm
プログラムを渡すとS式に変換する機能とS式とテストプログラムを渡すと一致するテストプログラムを教えてくれる機能を提供しています。どちらの機能も一文字入力を行うごとに変換・検証が行われるので、楽にトライアンドエラーを繰り返すことができます。 このツールを使うことで、S式も正規表現と同様に「気付いたらなんとなく理解できるようになっている」になれると考えています。
ツール名の通り、このツールは wasm を使ってブラウザ上で Ruby を動かしているのでバックエンドの管理が必要ないことも個人開発をする上では嬉しいポイントの1つです。 wasm を使ってブラウザで Ruby を動かす流れについては別途ブログに書く予定です。
(TODO: wasm を使ってブラウザで Ruby を動かす流れについての記事リンクを貼る)
感想
Ruby 3.2 で wasm がサポートされたことにより、ちょっとした CLI の Web ツール化が進むのではないかと感じました。
初学者にとって、コマンドラインツールよりも Web ツールの方が嬉しいことは色々あるんじゃないかと思っています。Web ツール化が進むことで Ruby コミュニティの裾野が広がったらいいですね。
参考にさせていただきました
marginalia(ActiveRecord::QueryLogs) を使うと ActiveRecord::Relation#explain が空文字を返す問題
困っている人向けまとめ
- marginalia(ActiveRecord::QueryLogs) を
prepend_comment = trueの設定で使うとActiveRecord::Relation#explainが空文字を返す問題がある - marginalia に issue が立っていて修正は既に rails/rails に取り込まれている Merge pull request #44350 from fatkodima/explain-prefix-comment · rails/rails@a32c54e · GitHub
- 2022/12/27 時点での最新版の Rails 7.0.4 ではまだ上記の修正が取り込まれていないので同様の monkey patch を当てる必要がある
marginalia を使おうとしたときにちょっと困ったことがあったのでメモ書き。
marginalia とは
marginalia gem とは、rails/rails を作った basecamp が作った gem で SQL のクエリの先頭か末尾にどの controller / action で発行されたかをクエリのコメントとして追記してくれる機能を持っている。
また、この機能は Rails 7 では ActiveRecord::QueryLogs という名前で標準の機能に取り込まれている。便利で使ってたけどメンテされてる気配がなかったので標準に取り込まれるのは嬉しい。
どんな問題が起きたのか
marginalia のデフォルトの挙動では、クエリ発行位置をクエリの末尾にコメントするようになっているが下記のように prepend_comment option を true にするとクエリの先頭にコメントを追記してくれるようになる。
Marginalia::Comment.prepend_comment = true
これを設定した上で ActiveRecord::Relation#explain(e.g. User.all.explain) を実行すると、explain 結果が空文字になってしまう問題が起きていた。
この問題は既知で、ActiveRecord::Relation#explain は with, select, update, delete, insert のいずれかから始まるクエリでないと EXPLAIN が動作しないような実装になっているのが原因とのこと。
どう対応すればいいのか
既に rails/rails に修正は取り込まれているものの、2022/12/27 時点の Rails 最新版である 7.0.4 ではまだ反映されていないので変更に対応する monkey patch をあてる必要がある。具体的には以下。
# config/initializers/marginalia.rb # `Marginalia::Comment.prepend_comment = true` で marginalia を動かすと `ActiveRecord::Relation#explain` が空文字を返す問題の対応。 # 対応自体は https://github.com/rails/rails/commit/a32c54e49e46f08a910a993718bae78e57f3d85f で rails/rails の main branch に取り込まれている。 # 2022/12/27時点でのRails 最新バージョン(7.0.4)には取り込まれていないので、取り込まれたバージョンまで Rails が上げることができればこの monkey patch を消すことができる。 if defined?(ActiveRecord::ExplainSubscriber) && ActiveRecord::ExplainSubscriber.const_defined?(:EXPLAINED_SQLS) ActiveRecord::ExplainSubscriber::EXPLAINED_SQLS = /\A\s*(\/\*.*\*\/)?\s*(with|select|update|delete|insert)\b/i end
新卒で入社した会社を退職しました
この記事は 呉高専 Advent Calendar 2022 - Adventar の21日目の記事です。
新卒で Wantedly という会社に就職し、その会社を今年いっぱいで退職することになりました。 なので、数年後自分がお酒を飲みながらエモい気持ちになるための振り返りとして書き残しておきます。
Wantedly とは
まずは、私が新卒で入社した Wantedly という会社について軽く紹介します。
Wantedly は「シゴトでココロオドルひとをふやす」をミッションに以下のサービスを展開しています。
私が働いていた期間の多くは Wantedly Visit の開発に携わり、最後の2ヶ月ほどは Wantedly Engagement Suite の開発に携わっていました。
Wantedly に新卒で入ってどうだったか?
間違いなく良かったと思っています。
良かったと思うポイントは色々ありますが、以下の2つを取り上げたいと思います。
- 真っ当な toC 向けのプロダクト開発を経験できた
- 色々なことに入門できた
1. 真っ当なtoC向けのプロダクト開発を経験できた
Wantedly では 仮説を立てる → 検証する → 機能としてリリースする → 予想とのギャップを整理する → 予想と機能を修正して再度リリースする → ... という開発サイクルを回していて、 Wantedly のエンジニアはこれらの全工程に関わっています。
これらのサイクル自体はよくあるものだと思いますが、会社によってはエンジニアは機能としてリリースする部分だけを担うこともあると聞いています。 分業による生産性の向上などのメリットもあると思いますが、各ステップを一通り経験できたことは間違いなく自分の経験としてプラスに働いたと考えています。
2. 色々なことに入門できた
前述の 1. 真っ当な toC 向けのプロダクト開発を経験できた でもわかるように Wantedly のエンジニアのカバー範囲はとても広いと感じています。
プロダクトを伸ばすために必要だからやる、という総合格闘技としてのプロダクト開発は自分の性に合っていて、様々な分野で 完全に理解した → 何もわからない → チョットデキル → 完全に理解した → ... を繰り返すことでプロダクト開発力が高まっている実感がありました。
【少し入門した分野】
エンジニアリング、プロダクトマネジメント、プロジェクトマネジメント、グロース、データ分析、UXライティング、...
色々なことに入門したことにより、深淵な「プロダクト開発」というトピックに対して学ぶべきロードマップがうっすら見えたような気がしました。

伸ばしたいスキルの変化
学生の頃から現在までで伸ばしたいスキルが変わっていった*1ことも書いておこうと思います。
学生の頃~社会人1年目
学生の頃は プロダクトを作る能力を高める = 技術力を上げる だと思っていたので、技術力を高められる会社として Wantedly を選びました。*2
入社してからも技術力を高めることが最も重要と考えていたような気がします。
読んでいた本
社会人2~3年目
社会人2~3年目になり、一個人としての成果だけでなく1チームとしての成果を考えるようになりました。
やってきたプロジェクトを見返してみると、プロジェクトの中で設計・実装は一部分でしかなく、もし仮に実装・設計の期間が半分になったとしても、トータルのプロジェクト期間はそこまで短くならないことに気付きました。 さらに、プロジェクトが完遂できたとしてもユーザーのインサイトがズレていて思っていたように数字が伸びないことも多々ありました。
これらの経験から、プロダクトを伸ばすためには技術力を高めるよりもプロジェクトマネジメント・プロダクトマネジメントの能力を磨くことが重要だと考えるようになりました。
読んでいた本
- イシューからはじめよ ― 知的生産の「シンプルな本質」
- プロダクトマネジメント ―ビルドトラップを避け顧客に価値を届ける
- アジャイルサムライ――達人開発者への道
- Hacking Growth グロースハック完全読本
社会人4年目~現在
社会人4年目では、新規事業に近いような事業としての不確実性がとても高いプロジェクトを任されていました。
新規事業のフェーズではどう作るよりも何を作るか、それをどう届けるかの方が圧倒的に重要です。そのため、コンセプトを決める段階からのプロダクトマネジメント、UXライティングなどよりユーザーに近い部分のスキルを高めていました。
そのようなスキルを高めていく中でなんとなく、自分の中のエンジニア濃度が薄まっていくような感覚があり危機感を覚えていました。「技術はあくまで手段」と思っていた自分がなぜ危機感を覚えるのか、プロダクトを伸ばす能力は高まっているんだからそれで十分じゃないか、と思う気持ちと「なんか嫌だな」という気持ちが同時に存在していました。
自分の中で気持ちを整理してみたところ、2つの気持ちがあることに気付きました。*3 どうやらプロダクトを伸ばすだけでは満たされなかったようです。
- プロダクトを伸ばしたい
- エンジニアとしてやっていきたい
そして、エンジニアとしてプロダクトの伸ばす力をより高めるための手段として、転職を決意しました。
(具体的な転職の話は脱線してしまうのでこの記事では書きません)
読んでいた本
新卒の会社を選ぶということ
この記事は呉高専 Advent Calendar として書いたものなので、これから新卒の会社を選ぶであろう学生向けのセクションも書いてみようと思います。
まだ新卒の会社以外をほとんど知らない社会人4年目の言うことなので話半分くらいに読んでください。
パッと思いついたのは以下の3つです。
- 新卒で入った会社の文化や考え方は自分の価値基準に影響する
- 専門性を磨き上げたい!と思っていたとしても新卒で幅広く色んなことができる環境に身を置くのは悪くない
- 一度選んだら正しい選択だったかは考えず、選択を正しくすることに全力を注ぐ
1. 新卒で入った会社の文化や考え方は自分の価値基準に影響する
これはよく言われていることですが、新卒で入った会社の文化や考え方は自分の価値基準に影響する気がしています。
新卒のタイミングでは前職が存在しないため、他社の比較することができません。そのため、まずは良くも悪くもその会社の文化や考え方に染まることになると思います。 2社目以降では前職が存在するため、良くも悪くも「前の会社と比較してここは〜で、あそこは〜」と比較してしまいます。
新卒で選んだ1社目が基準になってくるため、どんな考え方を自分にとっての当たり前にしたいかで会社を選んでみるのも悪くないかもしれません。
2. 専門性を磨き上げたい!と思っていたとしても新卒で幅広く色んなことができる環境に身を置くのは悪くない
これは実体験によるものなので賛否両論あると思いますが、新卒では幅広く色んなことができる環境に身を置くのも悪くないなと感じています。
専門性を磨き上げることが目標であれば何の問題もないと思います。しかし、別の目標があり、その目標を達成するために専門性を磨き上げたいと考えているのであれば、目標を達成する手段は特定の専門性を磨き上げる以外にも存在しないかを確かめる期間があっても良いと思っています。
自分が学生のときは プロダクトを作る能力を高める = 技術力を上げる だと思っていましたが、実務でプロダクト開発を行うことで他にも上げるべきパラメータが存在することを知ることができました。
3. 一度選んだら正しい選択だったかは考えず、選択を正しくすることに全力を注ぐ
これもよく言われていることですが、一度新卒の会社を選んだら正しかったかどうかは考えず、選択を正しくすることに全力を注いだ方がいいと思っています。
これに関しては自分の好きな記事があるのでそちらを参照してみてください。
ActiveModel::Dirty を使うときに気をつけること
最近ハマってしまったのでメモとして残しておく。
TL;DR
自身のmodel と association を同時に save する場合は dirty attribute が上書きされる可能性があるので注意する必要がある。
本編
雑なサンプルを提示する。
- User : ユーザー作成時に新規ユーザー向けのメール送信する機能を持っている
- UserName : User と has_one の関係で UserName 作成時に親の User model の
filled_nameを更新する
# == Schema Information # # Table name: user_names # # id :integer not null, primary key # user_id :integer indexed # name :string(32) # created_at :datetime # updated_at :datetime # class UserName < ApplicationRecord belongs_to :user after_create :mark_as_filled_name private def mark_as_filled_name user.filled_name = true user.save! end end
# == Schema Information # # Table name: users # # id :integer not null, primary key # filled_name :boolean default(FALSE), not null, indexed # created_at :datetime # updated_at :datetime # class User < UsersModel::User has_one :user_name after_save :send_mail_for_new_user private def send_mail_for_new_user MailService.send_new_user_mail(user) if saved_change_to_id? end end
上記のような model が存在したとき、以下のような実装で User と UserName の作成を同時に行うとメールが送信されない。
user = User.new user.user_name.build(name: 'this is name') user.save!
User と UserName の作成を別々に行うときちんとメールは送信される。(同一トランザクションでも問題ない)
user = User.new user.save! user.user_name.create!(name: 'this is name')
どうしてこうなるかを解説していく。
まず、 User と UserName の作成を同時に行う場合の user.save! では以下の処理が順番に実行されている。
- User の create
- UserName の create
- UserName の after_create による User の update
- User の after_save
4. を実行するタイミングでは 1. の dirty attribute(id column の変化) が残っていることを期待するが 3. の User update(filled_name column の変化) によって 1. の dirty attribute が上書きされ、サンプルコードで言うところのsaved_change_to_id?がfalseを返してしまう。
学び
- model の同時 save を行う必要がないときはなるべく別々での save を心がける
- dirty attribute 難しい
sorbet は production で使えるのか [2020/03/29 時点]
sorbet は少し前に話題になっていた Ruby の type annotaion gem。
以下のようなコードを書くと静的型チェック、動的型チェックの両方をやってくれる。
# typed: true require 'sorbet-runtime' class A extend T::Sig sig {params(x: Integer).returns(String)} def bar(x) x.to_s end end def main A.new.barr(91) # error: Typo! A.new.bar("91") # error: Type mismatch! end
srb ts実行結果
editor.rb:12: Method barr does not exist on A https://srb.help/7003
12 | A.new.barr(91) # error: Typo!
^^^^^^^^^^^^^^
Autocorrect: Use `-a` to autocorrect
editor.rb:12: Replace with bar
12 | A.new.barr(91) # error: Typo!
^^^^
editor.rb:13: Expected Integer but found String("91") for argument x https://srb.help/7002
13 | A.new.bar("91") # error: Type mismatch!
^^^^^^^^^^^^^^^
editor.rb:5: Method A#bar has specified x as Integer
5 | sig {params(x: Integer).returns(String)}
^
Got String("91") originating from:
editor.rb:13:
13 | A.new.bar("91") # error: Type mismatch!
^^^^
Errors: 2
sorbet は前から気になっていたけど社内で運用できる状態になっているのかどうか気になったので土日で調べてみた。
気になっていたこと
- sorbet-typed に更新があったら各リポジトリに更新を反映した PR を自動で作ることは可能なのか
- 社内 gem 用の sorbet-typed は作れるのか
- CI に組み込んで型チェックにコケたら PR 上で自動で指摘させることは可能か
- 型の coverage のようなものを計測することはできるか
- メタプロで生成されたメソッドの型定義はどうなるのか
- 周辺環境はどのくらい整備されているのか
- エディタとの integrate, sorbet の型定義を何かから自動生成する何か
sorbet-typed に更新があったら自身のリポジトリに更新を反映した PR を自動で作ることは可能なのか
sorbet には TypeScript の DefinitelyTyped のような型定義の中央管理システムがあって、gem に型定義を追加する場合はここに PR を投げる形になっている。
GitHub - sorbet/sorbet-typed: A central repository for sharing type definitions for Ruby gems
また、sorbet は型定義ファイルを commit する方針を取っている1ので利用している gem の型定義が更新されると自身のリポジトリでsrb rbi updateを実行し、明示的に更新する必要がある。
型定義をいい感じに更新する PR を投げてくれる script がどこかに転がっていればいいなと思ったけど、パッと調べたところ見当たらなかった。作る必要がありそう。
travis cronjob とか使えばいい感じにできるんじゃないかと思っている。しらんけど
結論:自作すれば可能
社内 gem 用の sorbet-typed は作れるのか
上に書いたように sorbet は sorbet-typed を使って型定義を共有する仕組みを作っている。
ただ、これは public なので private gem の型定義をここに書いちゃうと全世界に公開することになってしまう。
それは困る。
社内用の sorbet-typed を作り、それを差し込めるような仕組みがあれば private gem でも社内で利用することはできるはず。
sorbet, sorbet-typed の issue や sorbet Slack を眺めたりしたけど private gem に対する言及はどこにもされていなかった。 その辺りのソースコードも眺めてみたけど sorbet-typed の URL がハードコードされていた。 サポートされていない可能性が高い?
じゃあ Stripe はどうしているんだという話はあるし、他で問題になっていないわけがないので何を根本的に勘違いしている可能性がある
結論:本家に PR 投げれば可能?
CI に組み込んで型チェックにコケたら PR 上で自動で指摘させることは可能か
PR 上でコメントをつけてくれるサービスはいくつがあるが、社内では reviewdog を使うことが多い。
reivewdog は sorbet 未対応だったので PR を出した。数日中に使えるようになるはず。
追記:出していた PR が merge された!
他のサービスが sorbet に対応しているかどうかはちゃんと調べてない
結論:reviewdog を使えば可能
型の coverage のようなものを計測することはできるか
ここのサポートは手厚く、かなり細かい情報まで取得することができる。
Tracking Adoption with Metrics · Sorbet
しかも、導入フェーズに合わせてどの metrics に着目すべきかまで書いてくれているのでとても分かりやすい。
https://sorbet.org/docs/metrics#suggestions-for-driving-adoption
% srb tc --metrics-file=metrics.json % cat metrics.json | jq ".metrics[].name" | sort "ruby_typer.unknown..error.max" "ruby_typer.unknown..error.min" "ruby_typer.unknown..error.p25" "ruby_typer.unknown..error.p50" "ruby_typer.unknown..error.p75" "ruby_typer.unknown..error.p90" "ruby_typer.unknown..error.total" "ruby_typer.unknown..release.build_scm_commit_count" "ruby_typer.unknown..release.build_timestamp" "ruby_typer.unknown..run.utilization.context_switch.involuntary" "ruby_typer.unknown..run.utilization.context_switch.voluntary" "ruby_typer.unknown..run.utilization.inblock" "ruby_typer.unknown..run.utilization.major_faults" "ruby_typer.unknown..run.utilization.max_rss" "ruby_typer.unknown..run.utilization.minor_faults" "ruby_typer.unknown..run.utilization.oublock" "ruby_typer.unknown..run.utilization.system_time.us" "ruby_typer.unknown..run.utilization.user_time.us" "ruby_typer.unknown..types.input.bytes" "ruby_typer.unknown..types.input.classes.total" "ruby_typer.unknown..types.input.files" "ruby_typer.unknown..types.input.files.sigil.autogenerated" "ruby_typer.unknown..types.input.files.sigil.false" "ruby_typer.unknown..types.input.files.sigil.ignore" "ruby_typer.unknown..types.input.files.sigil.strict" "ruby_typer.unknown..types.input.files.sigil.strong" "ruby_typer.unknown..types.input.files.sigil.true" "ruby_typer.unknown..types.input.methods.total" "ruby_typer.unknown..types.input.methods.typechecked" "ruby_typer.unknown..types.input.sends.total" "ruby_typer.unknown..types.input.sends.typed" "ruby_typer.unknown..types.sig.count"
metrics.json を人間が見やすい形式にしてくれる gem も見つけた。これを各 PR に comment する bot 作るととても良さそう。
bundle exec srb tc --metrics-file /tmp/sorbet_metrics.json # No errors! Great job. bundle exec sorbet_progress /tmp/sorbet_metrics.json # Sorbet Progress # Progress for sig coverage # total_signatures 7528 # total_methods 183447 # total_classes 112433 # Progress for file coverage # sigil_ignore 12 0.20 % # sigil_false 5466 91.60 % # sigil_true 460 7.71 % # sigil_strict 12 0.20 % # sigil_strong 17 0.28 % # --------------------------------------- # Total: 5967 100% # Keep up the good work 👍
結論:可能
メタプロで生成されたメソッドの型定義はどうなるのか
experimental な機能として、メタプロで定義したメソッドに対して型定義を行うための plugin を書くことができる。
Metaprogramming plugins · Sorbet
# metaprogramming.rb # typed: true class Metaprogramming def self.macro(name) define_method("#{name}_time") { name } end macro(:bed) macro(:fun) end hello = Metaprogramming.new hello.bed_time # error: Method `bed_time` does not exist on Metaprogramming hello.fun_time # error: Method `fun_time` does not exist on Metaprogramming
上記のような macro を定義する DSL があった場合、以下のような plugin を書くことで sorbet にメソッドが定義されていたかのような挙動をさせることができる。
# macro_plugin.rb # Sorbet calls this plugin with command line arguments similar to the following: # ruby --class Metaprogramming --method macro --source macro(:bed) # we only care about the source here, so we use ARGV[5] source = ARGV[5] /macro\(:(.*)\)/.match(source) do |match_data| puts "def #{match_data[1]}_time; end" end # Note that Sorbet treats plugin output as rbi files
Caveats
Sorbet decides which plugin to call using method names only. This might be a problem if different metaprogramming methods use the same name, have similar usages, but behave differently. To illustrate, using the configuration above, the following calls macro_plugin.rb twice but results in an error.
https://sorbet.org/docs/metaprogramming-plugins#caveats
気をつけないといけないこととして、同じ interface の DSL を作ると見分けるのが難しくなることが挙げられていた。
plugin には string のクラス名が渡ってくるので klass_str.constantize.method(method_name).owner で定義元を確かめることはできるが、クラスを load する必要があるのでsrb tcがかなり遅くなるはず。悩ましい。。。
※ Rails はメタプロの宝庫なのでどうなるんだろうと思っていたら、ちゃんとプロジェクトが進んでいた。
GitHub - chanzuckerberg/sorbet-rails: A set of tools to make the Sorbet typechecker work with Ruby on Rails seamlessly.
結論:基本的には可能
周辺環境はどのくらい整備されているのか
エディタとの integrate
Language Server Protocolに準拠しているので様々なエディタと互換性はあるらしいが、vscode plugin のようなものは見当たらなかった。 自分で設定して Language Server を立ち上げれば使えるのかも。
結論:可能(と言っている)
sorbet の型定義を何かから自動生成する何か
色々あった。
- proto file から rbi ファイルを生成してくれるやつ
GitHub - coinbase/protoc-gen-rbi: Protobuf compiler plugin that generates Sorbet .rbi "Ruby Interface" files. - YARD to rbi
GitHub - AaronC81/sord: Generate Sorbet RBIs from YARD documentation - rbi を spec file から tracepoint を使って自動生成するやつ、黒魔術が過ぎる
GitHub - camertron/gelauto: Automatically annotate your code with Sorbet type definitions.
ちょっと性質は違うけど sorbet 用の rubocop も見つけた。
GitHub - Shopify/rubocop-sorbet
まとめ
今からすぐに使える感じではなかったけど、社内 gem の sorbet-typed 問題さえ解決すれば手の届く範囲にあるなと思った。 前向きに検討したい。
-
その方針を取った理由は https://sorbet.org/docs/rbi#a-note-about-vendoring-rbis に書いてある↩
grpc_required_annotator gem つくった
TL;DR
- 会社で grpc ruby を使っていて、同じような実装何回もやってんなと思ったので共通化して gem にした
- request message を required チェックを簡単にできる DSL を提供してくれる
- いつも書いていた required をチェックするだけの冗長なテストがすごく短くなって生産性上がった
本編
最近会社で grpc ruby を使った backend を書いていて、「同じような実装何回もやってんな」と思うことがあった。
それは request message の required な field に対して、空だったときに GRPC::InvalidArgument を返すやつ。
class SampleService < SamplePb::Sample::Service def foo(request, call): raise GRPC::InvalidArgument.new("`num` is required") if req.num == 0 # これ raise GRPC::InvalidArgument.new("`str` is required") if req.str == "" # これも raise GRPC::InvalidArgument.new("`repeated_fields` is required") if req.repeated_fields.empty? # 嫌になってくる FooResponse.new(num: req.num, str: req.str, repeated_fields: req.repeated_fields) end end
これに対応するテストを書くのもめんどくさい。
describe SampleService do describe "#foo" do subject { described_class.new.foo(request) } context "when num is empty" do let(:request) { SamplePb::Sample::FooRequest.new( num: nil, str: "str", repeated_fields: ["a"] ) } it "raises GRPC::InvalidArgument" do expect { subject }.to raise_error(GRPC::InvalidArgument) end end context "when str is empty" do ... it "raises GRPC::InvalidArgument" do expect { subject }.to raise_error(GRPC::InvalidArgument) end end context "when repeated_fields is empty" do ... it "raises GRPC::InvalidArgument" do expect { subject }.to raise_error(GRPC::InvalidArgument) end end end end
shared_exaple を使えばそれなりに共通化はできるけど request のクラスは各 rpc ごとで異なるので微妙に使いにくい。
というわけでシュッと required な field を宣言できて、パッとテストを書けるいい感じの gem を作ろうと思った。
できたのがこちら
この gem に入っている GrpcRequiredAnnotator module を service の実装クラスに include することで required メソッドが使えるようになる。
class SampleService < SamplePb::Sample::Service include GrpcRequiredAnnotator required :num, :str, :repeated_fields def foo(request, call): # raise GRPC::InvalidArgument.new("`num` is required") if req.num == 0 # これ # raise GRPC::InvalidArgument.new("`str` is required") if req.str == "" # これも # raise GRPC::InvalidArgument.new("`repeated_fields` is required") if req.repeated_fields.empty? # 嫌になってくる FooResponse.new(num: req.num, str: req.str, repeated_fields: req.repeated_fields) end end
こんな感じで各 rpc に対応するメソッドのすぐ上で required にしたい field を symbol で宣言すると実行前に validation してくれるようになる。便利。
各 rpc に対応する required な field を取得できるメソッドも生やしているのでテストもかなり行数が減った。
RSpec.describe SampleService do describe "#foo" do describe "required" do it "num, str and repeated_fields are required" do expect(described_class.required_fields(:foo)).to eq [:num, :str, :repeated_fields] end end end end
結構便利だと思うので grpc ruby 書いてる人はぜひ使ってみてください。
method_addedで added された method の挙動を override して再定義してたりとか、その再定義を module の extend で実現しようとしたら grpc interceptor の治安の悪い仕様1によって壊れてしまった話は元気があればまた書きます。

