物件基盤システムで使うgemに型をつける

こんにちは!
イタンジ株式会社で物件基盤システムの開発をしている藤崎 (https://x.com/aki19035vc) です。

つい先日、弊社オフィスにて Roppongi.rb が開催されました。

roppongirb.connpass.com

「最近追加した型の紹介とその振り返り」というタイトルでLTをさせていただいたのですが、その内容をブログ用に再編し、公開しようと思います。

speakerdeck.com

Rubyの型とは

Rubyは動的型付け言語であるため、変数やメソッドの型は実行時に決定されます。しかし、型注釈を追加することで、静的型解析を行い、コードの品質や保守性を向上させることができます。

Ruby 3.0から導入されたRBS(Ruby Signature)は、Rubyコードの型定義を記述するための言語であり、これを用いて静的解析を行えるようになります。

rbs gemとgem_rbs_collectionリポジトリについて

rbs gemは、型情報を扱うツールが共通で使いたくなる機能が集められたgemで、RBSのパーサーや標準ライブラリの型定義などが含まれています。

一方、gem_rbs_collectionリポジトリは、rbs gemには含まれていないgemの型定義を集めたリポジトリです。このリポジトリにはRailsなど多くのgemの型定義が含まれており、コミュニティにより積極的に整備されています。

しかしRubyの型はまだまだ発展途上で、型定義が間違っていたり、使用したいメソッドの型が定義されていなかったりするのが現状です。

型へのコントリビュートのきっかけ

私が型にコントリビュートしようと思ったきっかけは、業務で書いているRailsアプリケーションにおいて型を積極的に活用していることにあります。

私が開発しているRailsアプリケーションでは型をほぼ100%記述しているのですが、使用するgemの型定義が整備されていないことも多く、自前で型をつける必要がありました。

今まではそれで不便は感じていなかったのですが、最近では他のRailsアプリケーションでもこれらの型を利用したいと考えることが多くなってきました。
そのため、自前で書いていた型をrbsやgem_rbs_collectionに移していくことで自分のためにもコミュニティのためにもなると考え、コントリビュートをし始めました。

PRを作る前に何をしたか

追加したり修正したりしたい型はたくさんあるのですが、いきなりPRを作れません。

まずは下記のようなものをしっかりと読み、そのリポジトリの文化について学ぶ必要がありました。

  • CONTRIBUTING.md
  • 既存の型定義
  • 他の人の過去のPR

その次に、コントリビュート対象となるgemのドキュメントや実装を読みました。

業務では特定の状態のみを記述して型チェックを通すようにしていましたが、コントリビュートする際は取りうる状態をすべて考慮して型をつける必要があります。

要は人力型推論なので、ここが一番大変な作業です。

直近1ヶ月でやったこと

直近1ヶ月は下記のPRを作成しました。
※ 記事執筆時点ではまだマージされていないものも含んでいます

github.com github.com github.com github.com github.com

この記事では、その中でも少し大変だった2つのPRを取り上げようと思います。

ActiveModel::Errorの型を追加

このRPでは、Rails 6.1 から追加されたActiveModel::Errorの型を追加しました。

github.com

具体的なコードで示すと下記の通りで、#errorsで返ってくる値がRails 6.1から変わるのですが、この型が存在していませんでした。

class Person
  include ActiveModel::Validations

  attr_accessor :name

  validates :name, presence: true
end


person = Person.new
person.valid?
person.errors.each do |error|
  error #=> ActiveModel::Error
end

困ったこと

activemodelの型のディレクトリ構造を良く見ると、7.0のディレクトリ内にある型ファイルが6.0のディレクトリ内の型ファイルへのシンボリックリンクになっていました。

そのため、7.0の方にだけ型を追加する場合、既存の型定義をactivemodel-6.0.rbsに移す必要があり、差分が多くなってしまいます。

gem_rbs_collection/gems/activemodel
├── 6.0
│   ├── activemodel-generated.rbs
│   ├── activemodel.rbs
│   └── patch.rbs
└── 7.0
    ├── activemodel-7.0.rbs
    ├── activemodel-generated.rbs -> ../6.0/activemodel-generated.rbs
    ├── activemodel.rbs -> ../6.0/activemodel.rbs
    └── patch.rbs -> ../6.0/patch.rbs

どうしたか

PRを作った当初はActiveModel::Errorsの型も一緒に修正しようとしていたのですが、差分が多くなりすぎてしまいもう少しスコープを絞って欲しいということでした。

そのため、今回は既存の型は変更せず、ActiveModel::Errorの型をactivemodel-7.0.rbsに追加するだけにしました。

余談

レビューしてくださった方が「シンボリックリンクを使用した現在の方法に満足していない」とコメントされていたので、今後改善されるかもしれません。

mini_magick gemの型を追加

このPRでは、mini_magick gem の型を新しく追加しました。

github.com

mini_magick とは

github.com

mini_magickとはImageMagickという画像を扱うソフトウェアのRubyバインディングです。画像をリサイズしたり、フォーマットを変更したりする際に良く使われています。

imagemagick.org

$ magick mogrify -resize 100x100 -format png -write output.png input.jpg

例えば上記のようなコマンドを、下記のようなRubyスクリプトとして組み立てることができます。

require "mini_magick"

image = MiniMagick::Image.open("input.jpg")
image.resize "100x100"
image.format "png"
image.write "output.png"

困ったこと

下記のようにmethod_missingで黒魔術している箇所をどうするかで悩みました。

module MiniMagick
  class Tool
    # ~~ 省略 ~~

    def method_missing(name, *args)
      option = "-#{name.to_s.tr('_', '-')}"
      self << option
      self.merge!(args)
      self
    end
  end
end

そもそもなぜこのような実装になっているかというと、ImageMagickで提供されているオプションが多すぎるからだと思います。

良く使われるmagick mogrifyというコマンドには200以上のオプションがあり、これらを全てgem側で実装をすることは現実的ではありません。

どうしたか・どうしたいか

ひとまず実際に定義されているメソッドの型を追加したうえで、定義されていないがREADMEに記載されている一般的なメソッドを追加しました。

しかし、ImageMagickで使える数百個も存在するオプションを全て定義することはできていませんし、手動で定義するのは現実的ではありません、、、

そのため、ImageMagickのGitHubリポジトリからドキュメントを取得できるため、これをパースして型定義を生成することを考えています。

https://github.com/ImageMagick/ImageMagick/blob/7.1.1-34/www/mogrify.html

ただし、ImageMagickのバージョンによって使用できるオプションが異なる事を考えると、gem_rbs_collectionに型定義を置くかどうかは悩みどころではあります。生成用のgemを新しく作ったりするか、別のcollectionのリポジトリを作ったりするかなど、方法を模索中です。

最後に

本記事では、業務で使用するgemに型をつけた時の話を記載しました。

既存のgemの型を修正するのは意外と簡単です。既にあるコードベースに対して型を追加・修正するだけなので、手順さえ理解していればスムーズに進めることができます。

一方で新規にgemの型を追加する場合は考慮すべきことが多く、その分手間がかかります。

とはいえ、rbs_collectionはマージできればすぐに利用することができるためコントリビュートの実感を得やすく、モチベーションにも繋がります。

今後もRubyの型周りに積極的にコントリビュートしていき、発信していこうと思います。