Slide 1

Slide 1 text

森塚 真年(@sanfrecce_osaka) 2024/10/05(Sat) 松江Ruby会議11 #matrk11 gem_rbs_collection への コントリビュートから始める Ruby の型の世界

Slide 2

Slide 2 text

自己紹介 • 名前: 森塚 真年 • GitHub: @sanfrecce-osaka • Twitter(X): @sanfrecce_osaka • 所属: 株式会社エンペイ • 開催コミュニティ: Machida.rb・Hirakata.rb • 最も好きな機能: パターンマッチ

Slide 3

Slide 3 text

Ruby の型 触ってますか?

Slide 4

Slide 4 text

型の恩恵を受ける場面も増えてきました • irb --type-completor • エディタによるサポート • rbs-inline

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

自分の場合のモチベーション • Ruby という言語が好き • 今までもこれからも Ruby で仕事していきたい • 「型がないから Ruby やめよう」と言われるのが辛い

Slide 7

Slide 7 text

もっと開発体験を 良くしていきたくないですか?

Slide 8

Slide 8 text

Ruby の型

Slide 9

Slide 9 text

コントリビュートチャンス だらけ!!

Slide 10

Slide 10 text

ところで

Slide 11

Slide 11 text

@sanfrecce_osaka の由来

Slide 12

Slide 12 text

森塚

Slide 13

Slide 13 text

森塚 => moritsuka

Slide 14

Slide 14 text

mori => もうり

Slide 15

Slide 15 text

もうり => 毛利元就

Slide 16

Slide 16 text

毛利元就 => 三本の矢

Slide 17

Slide 17 text

三本の矢 => サンフレッチェ サンフレッチェは数字の3とイタリア語のフレッチェ(矢)を合わせた造語

Slide 18

Slide 18 text

サンフレッチェ + 出身地の大阪 => @sanfrecce_osaka

Slide 19

Slide 19 text

@sanfrecce_osaka => 中国地方に関連がある Rubyist

Slide 20

Slide 20 text

@sanfrecce_osaka => 松江に関連がある Rubyist

Slide 21

Slide 21 text

そんなゆるい 定義の人が

Slide 22

Slide 22 text

gem_rbs_collection(型) に コントリビュートする手順を 紹介します

Slide 23

Slide 23 text

Ruby の型に 興味を持ってくれる人が 増えてくれると嬉しいナ♪

Slide 24

Slide 24 text

今回のネタ: roo https://github.com/roo-rb/roo

Slide 25

Slide 25 text

1. コントリビュート先を確認する • 組み込みライブラリである • => rbs(https://github.com/ruby/rbs) • rbs/stdlib(https://github.com/ruby/rbs/tree/master/stdlib) で型が提供されている • => rbs(https://github.com/ruby/rbs) • gem のリポジトリに sig ディレクトリがある • => gem 自体のリポジトリ • それ以外 • => gem_rbs_collection(https://github.com/ruby/gem_rbs_collection)

Slide 26

Slide 26 text

2. 開発環境の構築 $ git clone https://github.com/ruby/rbs.git 略... $ bin/setup 略...

Slide 27

Slide 27 text

2. 開発環境の構築 $ git clone https://github.com/ruby/rbs.git 略... $ bin/setup 略... $ bin/init_new_gem roo bin/init_new_gem GEM_NAME

Slide 28

Slide 28 text

2. 開発環境の構築 $ git clone https://github.com/ruby/rbs.git 略... $ bin/setup 略... $ bin/init_new_gem roo Gem version you want to add (MAJOR.MINOR is recommended. e.g. 4.2): > 2.10 型を追加する gem のバージョンを入力

Slide 29

Slide 29 text

2. 開発環境の構築 $ git clone https://github.com/ruby/rbs.git 略... $ bin/setup 略... $ bin/init_new_gem roo Gem version you want to add (MAJOR.MINOR is recommended. e.g. 4.2): > 2.10 Your GitHub account if you want to become the maintainer of RBS for this gem (default: skip adding you to the maintainer) We recommentd to add your account to maintain the RBS actively. See https://github.com/ruby/gem_rbs_collection/blob/main/docs/CONTRIBUTING.md#gem-reviewer > sanfrecce-osaka ・gem の型の maintener になる場合は GitHub のアカウント名を入力 ・ならない場合は入力無しで OK

Slide 30

Slide 30 text

2. 開発環境の構築 $ git clone https://github.com/ruby/rbs.git 略... $ bin/setup 略... $ bin/init_new_gem roo Gem version you want to add (MAJOR.MINOR is recommended. e.g. 4.2): > 2.10 Your GitHub account if you want to become the maintainer of RBS for this gem (default: skip adding you to the maintainer) We recommentd to add your account to maintain the RBS actively. See https://github.com/ruby/gem_rbs_collection/blob/main/docs/CONTRIBUTING.md#gem-reviewer > sanfrecce-osaka create gems/roo/2.10/roo.rbs create gems/roo/2.10/_test/test.rb create gems/roo/2.10/_test/metadata.yaml create gems/roo/2.10/manifest.yaml create gems/roo/_reviewers.yaml The boilerplate for roo gem has been generated. Start writing the RBS! We recommend to focus on the main API of roo. See the CONTRIBUTING.md for more information. boilerplate が生成される

Slide 31

Slide 31 text

gems/GEM_NAME/x.xx/_reviewers.yaml reviewers: - sanfrecce-osaka bin/init_new_gem で入力したアカウント名が追加される

Slide 32

Slide 32 text

参考: about maintener(gem-reviewer) • https://github.com/ruby/gem_rbs_collection/blob/main/docs/ CONTRIBUTING.md#gem-reviewer • https://rubykaigi.org/2024/presentations/p_ck_.html#day2 • https://speakerdeck.com/pocke/community-driven-rbs-repository • https://www.timedia.co.jp/tech/20240513-tech/

Slide 33

Slide 33 text

3. 型をつけていく 選択肢は2つ • generator を利用する方法 • 主要な機能に手動で型を付けていく方法(推奨)

Slide 34

Slide 34 text

参考: generator での型の生成 • 以前は generator を利用する方法が推奨されていた • https://speakerdeck.com/fugakkbn/types-teaches-success-what- will-we-do?slide=74 • gem_rbs_collectionでジェネレーターが推奨されなくなった • https://fuga-ch85.hatenablog.com/entry/generator-not-recommend

Slide 35

Slide 35 text

今回は generator で型を生成後 全てに対して型をつけてみました

Slide 36

Slide 36 text

3.1. generator を利用して型を生成する https://github.com/ruby/gem_rbs_collection/pull/665/commits/5a8699243974da630ce1c90c4c7b9cbbee727a32 $ git submodule add \ https://github.com/roo-rb/roo.git gems/roo/2.10/_src $ cd gems/roo/2.10/_src/lib $ rbs prototype rb -o ../../ roo Processing `roo`... Generating RBS for `roo/base.rb`... - Writing RBS to `../../roo/base.rbs`... Generating RBS for `roo/constants.rb`... - Writing RBS to `../../roo/constants.rbs`... Generating RBS for `roo/csv.rb`... - Writing RBS to `../../roo/csv.rbs`... Generating RBS for `roo/errors.rb`... ...

Slide 37

Slide 37 text

3.1. generator を利用して型を生成する https://github.com/ruby/gem_rbs_collection/pull/665/commits/5a8699243974da630ce1c90c4c7b9cbbee727a32 module Roo class Excelx class Extractor @path: untyped @options: untyped include Roo::Helpers::WeakInstanceCache COMMON_STRINGS: { t: "t", r: "r", s: "s", ref: "ref", html_tag_open: "", html_tag_closed: "" } def initialize: (untyped path, ?::Hash[untyped, untyped] options) -> void private def doc: () -> untyped def doc_exists?: () -> untyped end end end 生成される型の例

Slide 38

Slide 38 text

3.2. 型を付けている間に出てきた話 • 標準添付ライブラリに型がない場合 • クラス自体に型を付けたい場合 • メタプロで定義されたメソッドの場合

Slide 39

Slide 39 text

3.2.1 標準添付ライブラリに型がない場合 • 依存している matrix gem に型がなかった • time や csv は rbs/stdlib に型がある • 一方で gem 自体に型を同梱する Pull Request が出ている • https://github.com/ruby/matrix/pull/16

Slide 40

Slide 40 text

どうしよう?

Slide 41

Slide 41 text

ruby-jp で聞いてみた ・bundled/default gem が rbs-gem に入っているのは歴史的経緯 ・今後新しく追加する場合は gem 自体に同梱するか gem_rbs_collection

Slide 42

Slide 42 text

3.2.2 クラス自体に型を付けたい場合 https://github.com/roo-rb/roo/blob/v2.10.1/lib/roo/libre_office.rb#L4 require 'roo/open_office' # LibreOffice is just an alias for Roo::OpenOffice class Roo::LibreOffice = Roo::OpenOffice

Slide 43

Slide 43 text

3.2.2 クラス自体に型を付けたい場合 Roo::LibreOffice: Roo::OpenOffice これだと Roo::OpenOffice のインスタンスになってしまう

Slide 44

Slide 44 text

3.2.2 クラス自体に型を付けたい場合 https://github.com/ruby/gem_rbs_collection/blob/257e560cb101c5369b56aef9f3fe2a0c8f3f7e8e/gems/roo/2.10/ roo/libre_office.rbs#L2 Roo::LibreOffice: Roo::OpenOffice Roo::LibreOffice: singleton(Roo::OpenOffice) # cf. https://moneyforward-dev.jp/entry/2023/10/13/rbs-new-syntaxes class Roo::LibreOffice = singleton(Roo::OpenOffice) これだと Roo::OpenOffice のインスタンスになってしまう 正解はこう rbs 3.0.0 からはこう書いた方が良い

Slide 45

Slide 45 text

3.2.3 メタプロで定義されたメソッドの場合 https://github.com/roo-rb/roo/blob/v2.10.1/lib/roo/base.rb#L116-L122 %i(first_row last_row first_column last_column).each do |key| ivar = "@#{key}".to_sym define_method(key) do |sheet = default_sheet| read_cells(sheet) instance_variable_get(ivar)[sheet] ||= first_last_row_col_for_sheet(sheet)[key] end end メタプログラミングで定義されているメソッドは rbs prototype で型が生成されない

Slide 46

Slide 46 text

3.2.3 メタプロで定義されたメソッドの場合 https://github.com/ruby/gem_rbs_collection/blob/257e560cb101c5369b56aef9f3fe2a0c8f3f7e8e/gems/roo/2.10/ roo/base.rbs#L78-L84 def first_row: (?String | Integer sheet) -> (Integer | nil) def last_row: (?String | Integer sheet) -> (Integer | nil) def first_column: (?String | Integer sheet) -> (Integer | nil) def last_column: (?String | Integer sheet) -> (Integer | nil) 愚直に定義する

Slide 47

Slide 47 text

3.2.3 メタプロで定義されたメソッドの場合 https://github.com/roo-rb/roo/blob/v2.10.1/lib/roo/base.rb#L226-L242 # when a method like spreadsheet.a42 is called # convert it to a call of spreadsheet.cell('a',42) def method_missing(m, *args) # #aa42 => #cell('aa',42) # #aa42('Sheet1') => #cell('aa',42,'Sheet1') if m =~ /^([a-z]+)(\d+)$/ col = ::Roo::Utils.letter_to_number(Regexp.last_match[1]) row = Regexp.last_match[2].to_i if args.empty? cell(row, col) else cell(row, col, args.first) end else super end end #a1 や #aa42 といったメソッドが無限に生える

Slide 48

Slide 48 text

ruby-jp で聞いてみた ・自分なら諦めるかも ・必要なら各自で rbs を書けば型検査でエラーになっても回避できる ・コードのコア部分じゃなければ労力かけたくないよね

Slide 49

Slide 49 text

無理せず 諦めましょう

Slide 50

Slide 50 text

3.3. 今回のやり方で型を付けてみて • Pros • コードを読むときの助けになる • 正確な型を付けやすい • Cons • 無限に時間がかかる • リリース頻度によってはメンテが非常に辛くなる

Slide 51

Slide 51 text

参考: prototype 以外の型の生成方法 • orthoses • https://github.com/ksss/orthoses • typeprof • https://github.com/ruby/typeprof • rbs-dynamic • https://github.com/osyo-manga/gem-rbs-dynamic • rbs_goose • https://github.com/kokuyouwind/rbs_goose

Slide 52

Slide 52 text

参考: prototype 以外の型の生成方法 • rbs-trace • https://github.com/sinsoku/rbs-trace • rbs_activesupport • https://github.com/tk0miya/rbs_activesupport • rbs_activemodel • https://github.com/tk0miya/rbs_activemodel • rbs_active_hash • https://github.com/tk0miya/rbs_active_hash

Slide 53

Slide 53 text

さあ Pull Request を出すぞ!

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

4. テストを書く 1. _test/test.rb にテスト対象を使ったコードを書く 2. bin/test を実行

Slide 56

Slide 56 text

4.1. _test/test.rb にテスト対象を使ったコードを書く require 'roo' file_name = './test.xlsx' xlsx = Roo::Excelx.new(file_name) xlsx.info xlsx.sheets xlsx.sheet('Info').row(1) xlsx.sheet(0).row(1) ・テスト対象のメソッドを呼び出すだけで OK ・test.xlsx を実際に用意する必要はない

Slide 57

Slide 57 text

4.2. bin/test を実行 % bin/test gems/roo/2.10 Testing gems/roo/2.10... Fetching gem metadata from https://rubygems.org/......... Resolving dependencies... Writing lockfile to /xxx/gem_rbs_collection/gems/roo/2.10/_test/Gemfile.lock Using csv:0 (/xxx/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rbs-3.5.3/stdlib/csv/0) Using date:0 (/xxx/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rbs-3.5.3/stdlib/date/0) Using forwardable:0 (/xxx/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rbs-3.5.3/stdlib/forwardable/0) Using nokogiri:1.11 (/xxx/gem_rbs_collection/gems/nokogiri/1.11) Using openssl:0 (/xxx/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rbs-3.5.3/stdlib/openssl/0) Using roo:2.10 (/xxx/gem_rbs_collection/gems/roo/2.10) Using rubyzip:2.3 (/xxx/gem_rbs_collection/gems/rubyzip/2.3) Using socket:0 (/xxx/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rbs-3.5.3/stdlib/socket/0) Using uri:0 (/xxx/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rbs-3.5.3/stdlib/uri/0) It's done! 9 gems' RBSs now installed. # Type checking files: ............................................................................................................................ ............................................. No type error detected. No untyped calls detected. => Success

Slide 58

Slide 58 text

4.2.1 bin/test でやっていること • テストのセットアップ • _test ディレクトリのクリーンアップ • 依存する gem・型のインストール • Steep のセットアップ • rbs validate • steep check • 最終的な返り値が untyped でないかのチェック

Slide 59

Slide 59 text

4.2.2. manifest.yaml .gem_rbs_collection/roo/2.10/roo/excelx/sheet.rbs:49:74: [error] Cannot find type `::Date` Diagnostic ID: RBS::UnknownTypeName def row: (String | Integer row_number) -> Array[nil | Link | bool | ::Date | ::DateTime | ::Time | Float | Integer | String] ~~~~~~ .gem_rbs_collection/roo/2.10/roo/link.rbs:28:23: [error] Cannot find type `::URI::File` Diagnostic ID: RBS::UnknownTypeName def to_uri: () -> (::URI::File | ::URI::FTP | ::URI::HTTP | ::URI::HTTPS | ::URI::LDAP | ::URI::LDAPS ... ~~~~~~~~~~~ csv_test.rb:7:6: [error] UnexpectedError: /xxx/gem_rbs_collection/gems/roo/2.10/ _test/.gem_rbs_collection/roo/2.10/roo/csv.rbs:51:78...51:88: Could not find ::CSV::Row(RBS::NoTypeFoundError) ... 型がない?

Slide 60

Slide 60 text

4.2.2. manifest.yaml https://github.com/ruby/gem_rbs_collection/blob/257e560cb101c5369b56aef9f3fe2a0c8f3f7e8e/gems/roo/2.10/ manifest.yaml # manifest.yaml describes dependencies which do not appear in the gemspec. # If this gem includes such dependencies, comment-out the following lines and # declare the dependencies. # If all dependencies appear in the gemspec, you should remove this file. # dependencies: - name: date - name: forwardable - name: uri - name: csv - name: openssl gemspec に記載されていない依存を教えてあげる必要がある

Slide 61

Slide 61 text

今度こそ Pull Request cf. https://github.com/ruby/gem_rbs_collection/pull/665

Slide 62

Slide 62 text

CI を pass した後に `/merge` とコメントすれば squash merge される

Slide 63

Slide 63 text

レビューが必要?

Slide 64

Slide 64 text

Gem 以外のファイル (今回は submodule 関連) を revert すれば /merge が実行できる

Slide 65

Slide 65 text

特に理由がなければ submodule は コミットしないように

Slide 66

Slide 66 text

付録

Slide 67

Slide 67 text

Ruby の型について知れる コミュニティやアカウントの情報

Slide 68

Slide 68 text

コミュニティ • ruby-jp の #types • https://ruby-jp.github.io/ • Asakusa-bashi.rbs • https://asakusa-bashi-rbs.connpass.com/ • Machida.rb(予定) • https://machidarb.connpass.com/

Slide 69

Slide 69 text

企業 • Money Forward • https://moneyforward-dev.jp/ • STORES • https://product.st.inc/ • Timeee • https://tech.timee.co.jp/

Slide 70

Slide 70 text

企業 • タイムインターメディア • https://www.timedia.co.jp/tech/ • MIXI • https://mixi-developers.mixi.co.jp/

Slide 71

Slide 71 text

アカウント(思いついた限り) • https://x.com/soutaro • https://x.com/mametter • https://x.com/p_ck_ • https://x.com/_ksss_ • https://x.com/kokuyouwind • https://x.com/tk0miya • https://x.com/sinsoku_listy • https://x.com/Little_Rubyist • https://x.com/euglena1215 • https://x.com/fugakkbn • https://x.com/joker1007 • https://x.com/buta_botti • https://x.com/nemunemu3desu • etc...

Slide 72

Slide 72 text

最後に

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

ご静聴 ありがとうございました!