Let’s collect type info during Ruby running and automatically generate an RBS file! (ja)

16.7K Views

September 10, 22

スライド概要

RubyKaigi 2022: https://rubykaigi.org/2022/presentations/pink_bangbi.html#day3
gem: https://github.com/osyo-manga/gem-rbs-dynamic
en: https://www.docswell.com/s/pink_bangbi/5DX89K-2022-09-10-102005

現在 Ruby では開発体験を上げるための取り組みとして型情報を定義する RBS の導入や Ruby のコードを静的解析して型情報を取り出す TypeProf の開発が行われています。
そこで、わたしは TypeProf とは異なるアプローチとして `TracePoint` を使って Ruby のコードを実行したときのメソッド呼び出しの情報を収集し、その情報を元にして RBS ファイルを生成する取り組みを行っています。
このアプローチをしてみた結果 TypeProf と比較してなにが利点なのか、なにが課題なのか、またどのようにして実現したのかを解説します。
このセッションをきっかけにしてより多くの人に RBS に興味を持ってもらい、今後の Ruby の開発体験をよくするために一緒に考えてみましょう!

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Let’s collect type info during Ruby running and automatically generate an RBS file! RubyKaigi Takeout 2022

2.

みんな RBS ファイルを 書いてる???

3.

わたしは書いてない!!

4.

でも型の恩恵は受けたい!! エディタでコード補完したい!!

5.

そうだ!!

6.

そうだ!! 実行時に Ruby の処理をトレースして RBS データを自動生成しよう!!!

7.

自己紹介 名前:osyo github : osyo-manga https://github.com/osyo-manga ブログ : Secret Garden(Instrumental) http://secret-garden.hatenablog.com Rails エンジニア 好きな Ruby の機能は Refinements エディタは Vim Vim プラグインを150個以上つくってた C++ で静的片付け言語を書いていた事がある RubyKaigi は2回目の参加 Use Macro all the time ~ マクロを使いまくろ ~ RubyKaigi Takeout 2021 [slides en / ja] 6 / 67

8.

今日話すこと

9.

今日話すこと 実行時に Ruby の処理をトレースして RBS を生成する rbs-dynamic をつくっ た

10.

アジェンダ 1. そもそも RBS って? 2. なぜやろうと思ったのか 3. どのようにして実装したのか 4. RBS を自動生成する上で考えたこと 5. TypeProf と比較してみる 6. 今後の課題 8 / 67

11.

1. そもそも RBS って? 9 / 67

12.

RBS とは 10 / 67

13.

RBS とは Ruby で型情報を扱うためのデータフォーマット Ruby と似たような構文で記述する 静的型検査を行う `Steep` は RBS の情報を元にして精査する 10 / 67

14.

RBS とは Ruby で型情報を扱うためのデータフォーマット Ruby と似たような構文で記述する 静的型検査を行う `Steep` は RBS の情報を元にして精査する RBS でいう型とは Ruby でいうクラスのことを指す事が多い 『あるオブジェクトがどのクラスのオブジェクトを受け付けるのか』というような制約の意味合いを持つ 厳密にいうとちょっと違うけど一旦この認識で OK 10 / 67

15.

RBS サンプル 1 2 class String def replace: (String other_str) -> String 3 4 5 def chars: () -> Array[String] end 6 7 8 class Object def method: (String | Symbol name) -> Method 9 end 10 11 12 class Math PI: Float 13 end 14 15 class MyClass 16 17 11 / 67 @value: Integer end

16.

RBS サンプル 1 class String 2 3 def replace: (String other_str) -> String 4 def chars: () -> Array[String] 5 6 end 7 class Object 8 9 10 def method: (String | Symbol name) -> Method end 11 class Math 12 13 PI: Float end 14 11 / 67 15 class MyClass 16 17 @value: Integer end メソッドの引数型と戻り値型を定義する これは `String` オブジェクトを受け取って `String` 型の値を返す、という意味のメソッド型の 定義 `String` が型になる

17.

RBS サンプル 1 2 class String def replace: (String other_str) -> String 3 4 5 def chars: () -> Array[String] end 6 7 8 9 class Object def method: (String | Symbol name) -> Method end 10 11 12 class Math PI: Float 13 end 14 15 class MyClass 16 17 11 / 67 @value: Integer end メソッドの引数型と戻り値型を定義する これは `String` オブジェクトを受け取って `String` 型の値を返す、という意味のメソッド型の 定義 `String` が型になる `Array[String]` は `String` 型の要素を持った `Array` 型という定義になる

18.

RBS サンプル 1 2 3 def replace: (String other_str) -> String 4 def chars: () -> Array[String] 5 end 6 7 class Object 8 11 / 67 class String def method: (String | Symbol name) -> Method 9 10 end 11 12 class Math PI: Float 13 14 end 15 class MyClass 16 17 @value: Integer end メソッドの引数型と戻り値型を定義する これは `String` オブジェクトを受け取って `String` 型の値を返す、という意味のメソッド型の 定義 `String` が型になる `Array[String]` は `String` 型の要素を持った `Array` 型という定義になる `|` を使うと `String` 型または `Symbol` 型という型定 義になる

19.

RBS サンプル 1 2 class String def replace: (String other_str) -> String 3 4 5 def chars: () -> Array[String] end 6 7 class Object 8 9 10 def method: (String | Symbol name) -> Method end 11 12 class Math PI: Float 13 end 14 11 / 67 15 16 class MyClass @value: Integer 17 end メソッドの引数型と戻り値型を定義する これは `String` オブジェクトを受け取って `String` 型の値を返す、という意味のメソッド型の 定義 `String` が型になる `Array[String]` は `String` 型の要素を持った `Array` 型という定義になる `|` を使うと `String` 型または `Symbol` 型という型定 義になる 定数やインスタンス変数に対しても型定義できる

20.

と、いうことで 12 / 67

21.

と、いうことで 実際に試してみよう!! 12 / 67

22.

デモ 13 / 67

23.

2. なぜやろうと思ったのか 14 / 67

24.

`TypeProf` 15 / 67 とは別のアプローチをしてみたかった

25.

とは別のアプローチをしてみたかった 『型は書きたくないが型の恩恵を受けたい』という事で RBS を自動生成できないか考えていた `TypeProf` 15 / 67

26.

とは別のアプローチをしてみたかった 『型は書きたくないが型の恩恵を受けたい』という事で RBS を自動生成できないか考えていた Ruby のコードを静的解析して RBS を生成するのは既に `TypeProf` で行われている `TypeProf` 15 / 67

27.

とは別のアプローチをしてみたかった 『型は書きたくないが型の恩恵を受けたい』という事で RBS を自動生成できないか考えていた Ruby のコードを静的解析して RBS を生成するのは既に `TypeProf` で行われている それとは別のアプローチとして実際に Ruby の実行時に型情報を収集して RBS を生成してみたか った `TypeProf` 15 / 67

28.

とは別のアプローチをしてみたかった 『型は書きたくないが型の恩恵を受けたい』という事で RBS を自動生成できないか考えていた Ruby のコードを静的解析して RBS を生成するのは既に `TypeProf` で行われている それとは別のアプローチとして実際に Ruby の実行時に型情報を収集して RBS を生成してみたか った なので『 `TypeProf` で解決できない問題を解決したい!』『 `TypeProf` よりもいいものをつく りたい!』というよりは『やってみたかった』というモチベーションが強い `TypeProf` その上で `TypeProf` と比較してみたかった 15 / 67

29.

RBS に対して関心を持ちたい / 持ってほしい 16 / 67

30.

RBS に対して関心を持ちたい / 持ってほしい Ruby の型に対して関心がある人は一定数いるがまだ一般ユーザには広がっていない印象がある 16 / 67

31.

RBS に対して関心を持ちたい / 持ってほしい Ruby の型に対して関心がある人は一定数いるがまだ一般ユーザには広がっていない印象がある このセッションをきっかけに RBS や型に対する議論などが盛り上がってほしい 16 / 67

32.

RBS に対して関心を持ちたい / 持ってほしい Ruby の型に対して関心がある人は一定数いるがまだ一般ユーザには広がっていない印象がある このセッションをきっかけに RBS や型に対する議論などが盛り上がってほしい 今回アプローチした『動的に RBS を生成する』をゴールにするつもりはなく、これから RBS をど う扱っていくのかをみんなで考えていきたいと思っている 16 / 67

33.

3. どのようにして実装したのか 17 / 67

34.

実装イメージ 1. 実行時に Ruby のメソッド呼び出し情報を収集する 2. 収集した情報から RBS のデータを生成する 3. 生成したデータをファイルとして保存する 18 / 67

35.

1. 実行時に Ruby のメソッド呼び出し情報を収集する 19 / 67

36.

を利用して情報を収集する `TracePoint` ライブラリを使用すると任意のイベントが実行された時に処理をフックできる `TracePoint` 1 20 / 67 tp = TracePoint.new(:call, :return) { |tp| 1 # 実行結果 2 3 4 case tp.event when :call puts "call: #{tp.defined_class}##{tp.method_id}()" 2 3 4 call: Object#func() return: 42 call: Object#func() 5 6 when :return puts "return: #{tp.return_value}" 5 return: homu 7 8 9 end } 10 11 12 13 def func(a) a end 14 15 16 17 18 puts "# " tp.enable { func(42) func("homu") } 実行結果

37.

を利用して情報を収集する `TracePoint` ライブラリを使用すると任意のイベントが実行された時に処理をフックできる `TracePoint` 1 tp = TracePoint.new(:call, :return) { |tp| 2 3 4 5 6 7 8 9 20 / 67 case tp.event when :call puts "call: #{tp.defined_class}##{tp.method_id}()" when :return puts "return: #{tp.return_value}" end } 10 11 12 def func(a) a end 13 14 puts "# 15 16 17 18 tp.enable { func(42) func("homu") } 実行結果" 実行結果 1 # 2 3 4 5 call: Object#func() return: 42 call: Object#func() return: homu `TracePoint` のオブジェクトを生成 イベントが発生した時にブロックが呼ばれる

38.

を利用して情報を収集する `TracePoint` ライブラリを使用すると任意のイベントが実行された時に処理をフックできる `TracePoint` 1 2 20 / 67 tp = TracePoint.new(:call, :return) { |tp| case tp.event 3 4 5 6 when :call puts "call: #{tp.defined_class}##{tp.method_id}()" when :return puts "return: #{tp.return_value}" 7 8 9 end 実行結果 1 # 2 3 4 5 call: Object#func() return: 42 call: Object#func() return: homu `TracePoint` } 10 11 12 def func(a) a end 13 14 puts "# 15 16 17 18 tp.enable { func(42) func("homu") } 実行結果" のオブジェクトを生成 イベントが発生した時にブロックが呼ばれる ブロック内の処理をトレース

39.

を利用して情報を収集する `TracePoint` ライブラリを使用すると任意のイベントが実行された時に処理をフックできる `TracePoint` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 20 / 67 tp = TracePoint.new(:call, :return) { |tp| case tp.event when :call puts "call: #{tp.defined_class}##{tp.method_id}()" when :return puts "return: #{tp.return_value}" end 実行結果 # call: Object#func() return: 42 call: Object#func() return: homu `TracePoint` } def func(a) a end 実行結果" puts "# tp.enable { func(42) func("homu") } 1 2 3 4 5 のオブジェクトを生成 イベントが発生した時にブロックが呼ばれる ブロック内の処理をトレース イベントによって処理を分岐 はメソッド呼び出し `return` はメソッドから `return` した時 `call`

40.

を利用して情報を収集する `TracePoint` ライブラリを使用すると任意のイベントが実行された時に処理をフックできる `TracePoint` 1 2 3 4 5 6 when :return puts "return: #{tp.return_value}" 7 8 9 end def func(a) 11 12 13 14 15 16 a end 実行結果" puts "# tp.enable { func(42) func("homu") } 1 2 3 4 5 実行結果 # call: Object#func() return: 42 call: Object#func() return: homu `TracePoint` } 10 17 18 20 / 67 tp = TracePoint.new(:call, :return) { |tp| case tp.event when :call puts "call: #{tp.defined_class}##{tp.method_id}()" のオブジェクトを生成 イベントが発生した時にブロックが呼ばれる ブロック内の処理をトレース イベントによって処理を分岐 はメソッド呼び出し `return` はメソッドから `return` した時 `call` メソッド名、戻り値などのメタ情報を取 得

41.

で取得しているメタ情報 `rbs-dynamic` では `TracePoint` で取得できない情報も取得している `rbs-dynamic` インスタンス変数や RBS を定義する上で必要なデータ 21 / 67

42.

で取得しているメタ情報 `rbs-dynamic` では `TracePoint` で取得できない情報も取得している `rbs-dynamic` インスタンス変数や RBS を定義する上で必要なデータ メソッドの呼び出し元を調べるために自身のメソッド内で呼び出されたメソッドのメタ情報もネ ストして保持してる `func1` 21 / 67 メソッド中で呼び出された `func2` メソッドの情報を `func1` メソッドのメタ情報として保持している

43.
[beta]
`rbs-dynamic`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

22 / 67

20
21
22
23
24

で扱っているメタ情報

[{:method_id=>:func,
:defined_class=>X,
:receiver_class=>X,

#
#

メソッド名
メソッドが定義されているクラス

:receiver_defined_class=>X,
:receiver_object_id=>2080,
#
object_id
:arguments=>
#
[{:op=>:req, :name=>:a, :type=>Integer,
:rbs_type=>{:type=>Integer, :value=>42, :args=>[]}, # RBS

レシーバの
メソッドの引数情報

で必要な独自の型情報

引数の値の object_id
メソッドが定義されている行数
メソッドが定義されているファイルパス
メソッドの可視性
# メソッドの呼び出し元の行数
:called_path=> "/path/to/sample.rb",
# メソッドの呼び出し元のファイルパス
:called_method_id=>nil,
# メソッドの呼び出し元のメソッド名
:called_methods=>[..],
# メソッド内で呼び出しているメソッドのメタ情報
# メソッド呼び出しの情報をネストして保存している
:block?=>false,
:block=>[],
# メソッドのブロック引数のメタ情報
:trace_point_event=>:call,
:return_value_class=>String, # 戻り値のクラス
:return_value_rbs_type=>{:type=>String, :value=>nil, :args=>[]},
:instance_variables_class=>{}}]
# メソッドが呼び出された時のインスタンス変数情報
:value_object_id=>85}],
:lineno=>4,
:path=> "/path/to/sample.rb",
:visibility=>:public,
:singleton_method?=>false,
:called_lineno=>11,

#
#
#
#

44.

2. 収集した情報から RBS のデータを生成する 23 / 67

45.

rbs ライブラリを使って RBS データを生成する RBS データを Ruby で扱うための標準ライブラリもバンドルされている 今回はこれを利用して収集したメソッドのメタ情報から RBS データに変換している 24 / 67

46.

RBS データを生成する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 25 / 67 require "rbs" decls = RBS::AST::Declarations::Class.new( name: :X, type_params: [], super_class: nil, members: [ RBS::AST::Members::MethodDefinition.new( name: :func, kind: :instance, types: [ RBS::MethodType.new( type_params: [], type: RBS::Types::Function.new( required_positionals: [ RBS::Types::Function::Param.new( type: RBS::Types::ClassInstance.new( args: [], location: nil, name: :Integer ), name: :n, location: nil ) ], optional_positionals: [], rest_positionals: nil, trailing_positionals: [], required_keywords: [], optional_keywords: [], rest_keywords: nil, return_type: RBS::Types::ClassInstance.new(args: [], location: nil, name: :String), ), block: false, location: nil ) ], annotations: [], location: nil, comment: nil, overload: false, visibility: nil ) ], location: nil, annotations: [], comment: nil ) 形式で出力 # RBS stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string 実行結果 1 2 3 class X def func: (Integer n) -> String end

47.

RBS データを生成する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 25 / 67 require "rbs" decls = RBS::AST::Declarations::Class.new( name: :X, type_params: [], super_class: nil, members: [ RBS::AST::Members::MethodDefinition.new( name: :func, kind: :instance, types: [ RBS::MethodType.new( type_params: [], type: RBS::Types::Function.new( required_positionals: [ RBS::Types::Function::Param.new( type: RBS::Types::ClassInstance.new( args: [], location: nil, name: :Integer ), name: :n, location: nil ) ], optional_positionals: [], rest_positionals: nil, trailing_positionals: [], required_keywords: [], optional_keywords: [], rest_keywords: nil, return_type: RBS::Types::ClassInstance.new(args: [], location: nil, name: :String), ), block: false, location: nil ) ], annotations: [], location: nil, comment: nil, overload: false, visibility: nil ) ], location: nil, annotations: [], comment: nil ) 形式で出力 # RBS stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string 実行結果 1 2 3 class X def func: (Integer n) -> String end 引数型を定義

48.

RBS データを生成する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 25 / 67 require "rbs" decls = RBS::AST::Declarations::Class.new( name: :X, type_params: [], super_class: nil, members: [ RBS::AST::Members::MethodDefinition.new( name: :func, kind: :instance, types: [ RBS::MethodType.new( type_params: [], type: RBS::Types::Function.new( required_positionals: [ RBS::Types::Function::Param.new( type: RBS::Types::ClassInstance.new( args: [], location: nil, name: :Integer ), name: :n, location: nil ) ], optional_positionals: [], rest_positionals: nil, trailing_positionals: [], required_keywords: [], optional_keywords: [], rest_keywords: nil, return_type: RBS::Types::ClassInstance.new(args: [], location: nil, name: :String), ), block: false, location: nil ) ], annotations: [], location: nil, comment: nil, overload: false, visibility: nil ) ], location: nil, annotations: [], comment: nil ) 形式で出力 # RBS stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string 実行結果 1 2 3 class X def func: (Integer n) -> String end 引数型を定義 戻り値型を定義

49.

RBS データを生成する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 25 / 67 require "rbs" decls = RBS::AST::Declarations::Class.new( name: :X, type_params: [], super_class: nil, members: [ RBS::AST::Members::MethodDefinition.new( name: :func, kind: :instance, types: [ RBS::MethodType.new( type_params: [], type: RBS::Types::Function.new( required_positionals: [ RBS::Types::Function::Param.new( type: RBS::Types::ClassInstance.new( args: [], location: nil, name: :Integer ), name: :n, location: nil ) ], optional_positionals: [], rest_positionals: nil, trailing_positionals: [], required_keywords: [], optional_keywords: [], rest_keywords: nil, return_type: RBS::Types::ClassInstance.new(args: [], location: nil, name: :String), ), block: false, location: nil ) ], annotations: [], location: nil, comment: nil, overload: false, visibility: nil ) ], location: nil, annotations: [], comment: nil ) 形式で出力 # RBS stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string 実行結果 1 2 3 class X def func: (Integer n) -> String end 引数型を定義 戻り値型を定義 メソッド型を定義

50.

RBS データを生成する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 25 / 67 require "rbs" decls = RBS::AST::Declarations::Class.new( name: :X, type_params: [], super_class: nil, members: [ RBS::AST::Members::MethodDefinition.new( name: :func, kind: :instance, types: [ RBS::MethodType.new( type_params: [], type: RBS::Types::Function.new( required_positionals: [ RBS::Types::Function::Param.new( type: RBS::Types::ClassInstance.new( args: [], location: nil, name: :Integer ), name: :n, location: nil ) ], optional_positionals: [], rest_positionals: nil, trailing_positionals: [], required_keywords: [], optional_keywords: [], rest_keywords: nil, return_type: RBS::Types::ClassInstance.new(args: [], location: nil, name: :String), ), block: false, location: nil ) ], annotations: [], location: nil, comment: nil, overload: false, visibility: nil ) ], location: nil, annotations: [], comment: nil ) 形式で出力 # RBS stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string 実行結果 1 2 3 class X def func: (Integer n) -> String end 引数型を定義 戻り値型を定義 メソッド型を定義 クラス型を定義

51.

RBS データを生成する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 25 / 67 require "rbs" decls = RBS::AST::Declarations::Class.new( name: :X, type_params: [], super_class: nil, members: [ RBS::AST::Members::MethodDefinition.new( name: :func, kind: :instance, types: [ RBS::MethodType.new( type_params: [], type: RBS::Types::Function.new( required_positionals: [ RBS::Types::Function::Param.new( type: RBS::Types::ClassInstance.new( args: [], location: nil, name: :Integer ), name: :n, location: nil ) ], optional_positionals: [], rest_positionals: nil, trailing_positionals: [], required_keywords: [], optional_keywords: [], rest_keywords: nil, return_type: RBS::Types::ClassInstance.new(args: [], location: nil, name: :String), ), block: false, location: nil ) ], annotations: [], location: nil, comment: nil, overload: false, visibility: nil ) ], location: nil, annotations: [], comment: nil ) 形式で出力 # RBS stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string 実行結果 1 2 3 class X def func: (Integer n) -> String end 引数型を定義 戻り値型を定義 メソッド型を定義 クラス型を定義 定義した RBS データから任意の IO に出 力する

52.

こんな感じに Ruby の実行時にメソッドのメタ情報を収集し RBS データを生成するように実装している MEMO: ミニマムな実装 26 / 67

53.

実際に使ってみる 27 / 67

54.
[beta]
Ruby 上から RBS データを生成する
1
2
3
4
5
6
7
8
9
10
11
12
13
14

28 / 67

require "rbs/dynamic"
class FizzBuzz
def initialize(value)
@value = value
end
def value; @value end
def apply
value % 15 == 0 ? "FizzBuzz"
: value % 3 == 0 ? "Fizz"
: value % 5 == 0 ? "Fuzz"
: value

15
16
17
18
19
20

end
end

21

puts rbs

rbs = RBS::Dynamic.trace_to_rbs_text do
(1..20).each { FizzBuzz.new(_1).apply }
end

実行結果
1
2
3
4
5
6
7
8
9

class FizzBuzz
private def initialize: (Integer value) -> Integer
def apply: () -> (Integer | String)
def value: () -> Integer
@value: Integer
end

55.
[beta]
Ruby 上から RBS データを生成する
1
2

require "rbs/dynamic"

1
2

3
4
5
6
7
8

class FizzBuzz
def initialize(value)
@value = value
end

3
4
5
6
7
8
9

9
10
11
12
13

29 / 67

実行結果

def value; @value end
def apply
value % 15 == 0 ? "FizzBuzz"
: value % 3 == 0 ? "Fizz"
: value % 5 == 0 ? "Fuzz"

14
15
16
17
18
19

: value
end
end

20
21

end
puts rbs

rbs = RBS::Dynamic.trace_to_rbs_text do
(1..20).each { FizzBuzz.new(_1).apply }

class FizzBuzz
private def initialize: (Integer value) -> Integer
def apply: () -> (Integer | String)
def value: () -> Integer
@value: Integer
end

ブロック内で呼び出されたメソッドから RBS デ
ータを生成する

56.
[beta]
Ruby 上から RBS データを生成する
1

require "rbs/dynamic"

2
3

class FizzBuzz

4
5
6

def initialize(value)
@value = value
end

7
8
9
10

def value; @value end
def apply

11
12
13
14
15

29 / 67

value % 15 == 0 ? "FizzBuzz"
: value % 3 == 0 ? "Fizz"
: value % 5 == 0 ? "Fuzz"
: value
end

16
17
18

end

19
20
21

(1..20).each { FizzBuzz.new(_1).apply }
end
puts rbs

rbs = RBS::Dynamic.trace_to_rbs_text do

実行結果
1
2
3
4
5
6
7
8
9

class FizzBuzz
private def initialize: (Integer value) -> Integer
def apply: () -> (Integer | String)
def value: () -> Integer
@value: Integer
end

ブロック内で呼び出されたメソッドから RBS デ
ータを生成する

57.
[beta]
Ruby 上から RBS データを生成する
1

require "rbs/dynamic"

1

2
3

2
3

private def initialize: (Integer value) -> Integer

class FizzBuzz
def initialize(value)
@value = value
end

4
5
6

def apply: () -> (Integer | String)

def value; @value end

7
8
9

4
5
6
7
8
9
10

30 / 67

実行結果

def apply

11
12

value % 15 == 0 ? "FizzBuzz"
: value % 3 == 0 ? "Fizz"

13

: value %

5 == 0 ? "Fuzz"

14
15
16

: value
end
end

17
18
19

rbs = RBS::Dynamic.trace_to_rbs_text do
(1..20).each { FizzBuzz.new(_1).apply }

20
21

end
puts rbs

class FizzBuzz

def value: () -> Integer
@value: Integer
end

ブロック内で呼び出されたメソッドから RBS デ
ータを生成する
メソッド以外にもインスタンス変数の型付けも行
う

58.
[beta]
`rbs-dynamic`
1
2
3
4
5
6
7
8
9
10

コマンドで生成する

# sample.rb
class FizzBuzz
def initialize(value)
@value = value
end
def value; @value end
def apply
value % 15 == 0 ? "FizzBuzz"

11
12
13
14
15
16

: value %
: value %
: value
end
end

3 == 0 ? "Fizz"
5 == 0 ? "Fuzz"

17
18

puts "
"
(1..20).each { FizzBuzz.new(_1).apply }

コマンド結果
1
2
3
4
5
6
7
8
9
10
11
12
13

$ rbs-dynamic trace sample.rb
# RBS dynamic trace 0.1.0
class FizzBuzz
private def initialize: (Integer value) -> Integer
def apply: () -> (Integer | String)
def value: () -> Integer
@value: Integer
end
$

実行結果

NOTE: `rbs-dynamic` コマンドでは実行された Ruby の標準出力はされない
31 / 67

59.
[beta]
その他の型定義
1
2
3
4
5

class X
VALUE = 42

6
7
8
9
10

def self.get_value
VALUE
end

11
12
13
14
15
16

func2(X.get_value + value) { |a| a.to_s }
end

17
18
19
20
21
22

32 / 67

require "rbs/dynamic"

def func1(value:)

private def func2(a, &block)
block.call(a)
end
end
x = X.new
puts RBS::Dynamic.trace_to_rbs_text {
x.func1(value: 1)
}

実行結果
1
2
3
4
5

class X
VALUE: Integer
def self.get_value: () -> Integer

6
def func1: (value: Integer) -> String
7
8
private def func2: (Integer a) ?{ (?Integer a) ->
String } -> String
9
end

60.
[beta]
その他の型定義
1
2
3
4

require "rbs/dynamic"
class X
VALUE = 42

5
6
7
8
9
10

def self.get_value
VALUE
end
def func1(value:)

11
12
13
14
15

33 / 67

func2(X.get_value + value) { |a| a.to_s }
end
private def func2(a, &block)
block.call(a)

16
17

end
end

18
19
20

x = X.new
puts RBS::Dynamic.trace_to_rbs_text {

21
22

}

x.func1(value: 1)

実行結果
1
2
3
4

class X
VALUE: Integer
def self.get_value: () -> Integer

5
6
def func1: (value: Integer) -> String
7
8
private def func2: (Integer a) ?{ (?Integer a) ->
String } -> String
9
end

定数の定義

61.
[beta]
その他の型定義
1
2
3

require "rbs/dynamic"
class X

4
5
6
7
8
9

VALUE = 42
def self.get_value
VALUE
end

10
11
12
13
14
15
16
17
18
19
20
21
22

34 / 67

def func1(value:)
func2(X.get_value + value) { |a| a.to_s }
end
private def func2(a, &block)
block.call(a)
end
end
x = X.new
puts RBS::Dynamic.trace_to_rbs_text {
x.func1(value: 1)
}

実行結果
1
2
3

class X
VALUE: Integer

4
5
6
7
8
String
9

def self.get_value: () -> Integer
def func1: (value: Integer) -> String
private def func2: (Integer a) ?{ (?Integer a) ->
} -> String

end

定数の定義
クラスメソッドの定義

62.
[beta]
その他の型定義
1
2
3

class X

4
5
6

VALUE = 42

7
8
9

VALUE
end

10
11
12
13
14
15

35 / 67

require "rbs/dynamic"

def self.get_value

def func1(value:)
func2(X.get_value + value) { |a| a.to_s }
end
private def func2(a, &block)
block.call(a)

16
17
18
19

end
end

20
21
22

puts RBS::Dynamic.trace_to_rbs_text {
x.func1(value: 1)
}

x = X.new

実行結果
1
2
3
4
5
6
7
8

class X
VALUE: Integer
def self.get_value: () -> Integer
def func1: (value: Integer) -> String
private def func2: (Integer a) ?{ (?Integer a) ->

String } -> String
9
end

定数の定義
クラスメソッドの定義
キーワード引数のメソッド

63.
[beta]
その他の型定義
1
2
3
4
5
6
7
8
9
10
11
12

require "rbs/dynamic"
class X
VALUE = 42
def self.get_value
VALUE
end
def func1(value:)
func2(X.get_value + value) { |a| a.to_s }
end

13

36 / 67

14
15
16
17
18
19

private def func2(a, &block)
block.call(a)
end
end

20
21
22

puts RBS::Dynamic.trace_to_rbs_text {
x.func1(value: 1)
}

x = X.new

実行結果
1
2
3
4
5
6

class X
VALUE: Integer
def self.get_value: () -> Integer
def func1: (value: Integer) -> String

7
8
private def func2: (Integer a) ?{ (?Integer a) ->
String } -> String
9
end

定数の定義
クラスメソッドの定義
キーワード引数のメソッド
ブロック引数を持つ `private` メソッド
`?{ (?Integer a) -> String }`

がブロック引数の型

64.

rbs-dynamic がサポートしている型情報 クラス / モジュール スーパークラス `include / prepend / extend` インスタンス変数 定数 メソッド 各種引数 戻り値 ブロック引数 メソッドの可視化性 クラスメソッド リテラル型 ( `1` や `:hoge` など) `String` 37 / 67 リテラルは未対応 Generics 型 `Array` `Hash` `Range` サポートしてない型情報 クラス変数 Generics 型 `Enumerable` `Struct` Record 型 Tuple 型

65.

`rbs-dynamic` コマンドのオプション 1 $ rbs-dynamic --help trace --with-literal-type 2 Usage: 3 4 5 rbs-dynamic trace [filename] Options: 6 [--root-path=ROOT-PATH] # Rooting path. Default: current dir 8 [--target-filepath-pattern=TARGET-FILEPATH-PATTERN] # Target filepath pattern. e.g. hoge\|foo\|bar. Default '.*' 9 10 [--ignore-filepath-pattern=IGNORE-FILEPATH-PATTERN] # Default: .* # Ignore filepath pattern. Priority over `target-filepath-pattern`. e.g. hoge\|foo\|bar. [--target-classname-pattern=TARGET-CLASSNAME-PATTERN] # Target class name pattern. e.g. RBS::Dynamic. Default '.*' 7 # Default: /path/to/current Default '' 11 12 # Default: .* 13 [--ignore-classname-pattern=IGNORE-CLASSNAME-PATTERN] Default '' 14 [--ignore-class-members=one two three] 15 # Possible values: inclued_modules, prepended_modules, extended_modules, constant_variables, instance_variables, singleton_methods, methods 16 [--method-defined-calsses=one two three] 17 38 / 67 # Ignore class name pattern. Priority over `target-classname-pattern`. e.g. PP\|PrettyPrint. # Which class defines method type. Default: defined_class and receiver_class # Possible values: defined_class, receiver_class 18 [--show-method-location], [--no-show-method-location] # Show source_location and called_location in method comments. Default: no 19 [--use-literal-type], [--no-use-literal-type] # Integer and Symbol as literal types. e.g func(:hoge, 42). Default: no 20 21 [--with-literal-type], [--no-with-literal-type] [--use-interface-method-argument], [--no-use-interface-method-argument] # Integer and Symbol with literal types. e.g func(Symbol | :hoge | :foo). Default: no # Define method arguments in interface. Default: no 22 [--stdout], [--no-stdout] # stdout at runtime. Default: no 23 [--trace-c-api-method], [--no-trace-c-api-method] # Trace C API method. Default: no

66.

3. 生成したデータをファイルとして保存する 39 / 67

67.

まだ未実装 40 / 67

68.

こういうことをやりたかった 41 / 67

69.

こういうことをやりたかった Rails アプリでリクエスト毎に情報をトレースし、自動で RBS ファイルを生成 またその時に既存の RBS ファイルとコンフリクトしないようにマージする 41 / 67

70.

こういうことをやりたかった Rails アプリでリクエスト毎に情報をトレースし、自動で RBS ファイルを生成 またその時に既存の RBS ファイルとコンフリクトしないようにマージする CI でテストを実行する時に自動で RBS ファイルを生成 リポジトリへのコミットまでを自動化 41 / 67

71.

こういうことをやりたかった Rails アプリでリクエスト毎に情報をトレースし、自動で RBS ファイルを生成 またその時に既存の RBS ファイルとコンフリクトしないようにマージする CI でテストを実行する時に自動で RBS ファイルを生成 リポジトリへのコミットまでを自動化 どのタイミングで RBS ファイルを保存するべきなのかは別途考える必要がある 41 / 67

72.

4. RBS を自動生成する上で考えたこと 42 / 67

73.

Interface 型の実装 43 / 67

74.

Interface 型とは? 特定のメソッドが定義されている型のみ受け付ける型 44 / 67

75.

Interface 型とは? 特定のメソッドが定義されている型のみ受け付ける型 以下のような `#to_s` メソッドを内部で呼び出す `my_puts` メソッドがあるとする 1 2 3 44 / 67 def my_puts(a) puts a.to_s end

76.

Interface 型とは? 特定のメソッドが定義されている型のみ受け付ける型 以下のような `#to_s` メソッドを内部で呼び出す `my_puts` メソッドがあるとする 1 2 3 def my_puts(a) puts a.to_s end RBS ではこのような場合に Interface 型が利用できる Interface 型とは『特定のメソッドを持つ型のみ受け付ける』というような型が定義できる 44 / 67

77.

Interface 型とは? 特定のメソッドが定義されている型のみ受け付ける型 以下のような `#to_s` メソッドを内部で呼び出す `my_puts` メソッドがあるとする 1 2 3 def my_puts(a) puts a.to_s end RBS ではこのような場合に Interface 型が利用できる Interface 型とは『特定のメソッドを持つ型のみ受け付ける』というような型が定義できる Interface 型を使った `my_puts` の型定義は以下のようになる 1 class Object 2 # String 3 4 interface _ToS def to_s: () -> String 5 6 end 7 8 44 / 67 型を返す to_s メソッドを持つオブジェクトのみを受け付ける private def my_puts: (_ToS) -> NilClass end

78.

Interface 型とは? 特定のメソッドが定義されている型のみ受け付ける型 以下のような `#to_s` メソッドを内部で呼び出す `my_puts` メソッドがあるとする 1 2 def my_puts(a) puts a.to_s 3 end RBS ではこのような場合に Interface 型が利用できる Interface 型とは『特定のメソッドを持つ型のみ受け付ける』というような型が定義できる Interface 型を使った `my_puts` の型定義は以下のようになる 1 2 class Object # String 型を返す to_s メソッドを持つオブジェクトのみを受け付ける 3 4 interface _ToS def to_s: () -> String 5 6 end 7 8 private def my_puts: (_ToS) -> NilClass end 44 / 67rbs-dynamic では自動的に Interface 型を定義する機能を実装した

79.
[beta]
Interface 型を自動で定義
`rbs-dynamic` のコマンドオプション `--use-interface-method-argument` を付ける
1

# sample.rb

1

$ rbs-dynamic trace sample.rb --use-interface-method-argument

2

class Cat

2

# RBS dynamic trace 0.1.0

3
4

def to_s; "Cat" end
end

3
4

class Object < BasicObject
private def my_puts: (_Interface_have__to_s__1 a) -> NilClass

class Dog

5
6
7

interface _Interface_have__to_s__1

8
9

def to_s: () -> String
end

5
6
7
8
9

45 / 67

def to_s; "Dog" end
end

10
11

def my_puts(a)
puts a.to_s

10
11

end

12

end

12

class Cat

13
14

my_puts Cat.new

13
14

def to_s: () -> String
end

15

my_puts Dog.new

15
16

class Dog

17
18

def to_s: () -> String
end

19

$

80.

メソッドの定義元と参照先の情報を埋め込む 46 / 67

81.

メソッドの定義元や参照先を参照したい コードを読んでいる時にメソッドの定義元や参照先を参照したい事がある 47 / 67

82.

メソッドの定義元や参照先を参照したい コードを読んでいる時にメソッドの定義元や参照先を参照したい事がある しかし RBS ではメソッドの位置情報は保持していない 47 / 67

83.

メソッドの定義元や参照先を参照したい コードを読んでいる時にメソッドの定義元や参照先を参照したい事がある しかし RBS ではメソッドの位置情報は保持していない 今回の実装では RBS のコメントに位置情報を埋め込むようにしてみた 47 / 67

84.

参照先の出力サンプル `rbs-dynamic` のコマンドオプション `--show-method-location` を付ける 1 2 48 / 67 # sample.rb class X 1 2 $ rbs-dynamic trace sample.rb --show-method-location # RBS dynamic trace 0.1.0 3 def func1(a) 3 4 5 end 4 5 6 7 def func2(a) func1("homu") 6 7 # reference location: # func1(Integer a) -> NilClass sample.rb:12 8 end 8 # class X # source location: sample.rb:3 func1(String a) -> NilClass sample.rb:7 9 10 end 9 10 def func1: (Integer | String a) -> NilClass 11 12 x = X.new x.func1(42) 11 12 # source location: sample.rb:6 # reference location: 13 x.func2("mami") 13 14 # func2(String a) -> NilClass sample.rb:13 def func2: (String a) -> NilClass 15 end 16 $

85.

参照先の出力サンプル `rbs-dynamic` のコマンドオプション `--show-method-location` を付ける 1 2 # sample.rb class X 3 4 5 def func1(a) end 6 7 8 $ rbs-dynamic trace sample.rb --show-method-location # RBS dynamic trace 0.1.0 3 4 class X 5 # source location: sample.rb:3 def func2(a) func1("homu") 6 7 # reference location: # func1(Integer a) -> NilClass sample.rb:12 end 8 # 9 10 11 end func1(String a) -> NilClass sample.rb:7 x = X.new 9 10 11 # source location: sample.rb:6 12 x.func1(42) 12 # reference location: 13 x.func2("mami") 13 # `X#func1` 49 / 67 1 2 の定義箇所 def func1: (Integer | String a) -> NilClass func2(String a) -> NilClass sample.rb:13 14 15 def func2: (String a) -> NilClass end 16 $

86.

参照先の出力サンプル `rbs-dynamic` のコマンドオプション `--show-method-location` を付ける 1 # sample.rb 1 $ rbs-dynamic trace sample.rb --show-method-location 2 3 4 5 class X def func1(a) end 2 3 4 5 # RBS dynamic trace 0.1.0 6 def func2(a) 6 7 # reference location: # func1(Integer a) -> NilClass sample.rb:12 8 # 7 8 func1("homu") end 9 10 11 end 12 13 func1(String a) -> NilClass sample.rb:7 x.func1(42) 9 10 11 12 # source location: sample.rb:6 # reference location: x.func2("mami") 13 # x = X.new `X#func1` 50 / 67 class X # source location: sample.rb:3 の参照箇所 def func1: (Integer | String a) -> NilClass func2(String a) -> NilClass sample.rb:13 14 15 def func2: (String a) -> NilClass end 16 $

87.

Symbol を型として扱うか値として扱うか 51 / 67

88.

Symbol は値として扱える RBS では `Symbol` の値を型として定義できる 特定の `Symbol` の値のみを受け付けたい場合に利用できる 他の言語にある `enum` 型に近い 1 2 3 特定の の値のみを受け付けるメソッド型の定義 # Symbol def upcase: () -> String 4 | (:ascii | :lithuanian | :turkic) -> String 5 6 | (:lithuanian, :turkic) -> String | (:turkic, :lithuanian) -> String 7 52 / 67 class String end

89.

一方で `#attr_accessor` のように特定の値に依存しないメソッドも存在している 1 2 3 4 53 / 67 module Module どんな の値でも受け付ける必要がある # Symbol def attr_accessor: (*(Symbol | String) arg0) -> NilClass end

90.

一方で `#attr_accessor` のように特定の値に依存しないメソッドも存在している 1 2 3 4 module Module どんな の値でも受け付ける必要がある # Symbol def attr_accessor: (*(Symbol | String) arg0) -> NilClass end なので rbs-dynamic では `:hoge` ではなくて `Symbol` 型に寄せるようにしている 1 2 3 def func(a) 1 2 $ rbs-dynamic trace sample.rb # RBS dynamic trace 0.1.0 3 4 5 end end 4 5 class X def func: (Symbol a) -> NilClass 6 7 x = X.new 6 7 end $ 8 x.func(:hoge) 9 10 53 / 67 # sample.rb class X x.func(:foo) x.func(:bar)

91.

一方で `#attr_accessor` のように特定の値に依存しないメソッドも存在している 1 2 3 4 module Module # どんな Symbol の値でも受け付ける必要がある def attr_accessor: (*(Symbol | String) arg0) -> NilClass end なので rbs-dynamic では `:hoge` ではなくて `Symbol` 型に寄せるようにしている 1 2 3 # sample.rb class X def func(a) 1 2 $ rbs-dynamic trace sample.rb # RBS dynamic trace 0.1.0 3 4 5 end end 4 5 class X def func: (Symbol a) -> NilClass 6 7 x = X.new 6 7 end $ 8 x.func(:hoge) 9 10 x.func(:foo) x.func(:bar) とはいえ `Symbol` の値がわかっている方がわかりやすいケースもある 53 / 67

92.

`Symbol` 54 / 67 型 + `Symbol` の値で型定義するようにしてみた

93.

型 + `Symbol` の値で型定義するようにしてみた `Symbol` 型だけではなくて `Symbol | :hoge | :foo | :bar` というように `Symbol` 型 + その 値を型として定義するようにした `Symbol` 54 / 67

94.

型 + `Symbol` の値で型定義するようにしてみた `Symbol` 型だけではなくて `Symbol | :hoge | :foo | :bar` というように `Symbol` 型 + その 値を型として定義するようにした これであれば `Symbol` 型を受けつつ、RBS をみればどんな値を受け取っているのか確認する事が できる `Symbol` 54 / 67

95.

型 + `Symbol` の値で型定義するようにしてみた `Symbol` 型だけではなくて `Symbol | :hoge | :foo | :bar` というように `Symbol` 型 + その 値を型として定義するようにした これであれば `Symbol` 型を受けつつ、RBS をみればどんな値を受け取っているのか確認する事が できる `rbs-dynamic` のコマンドオプション `--with-literal-type` を付ける `Symbol` 1 # sample.rb 1 $ rbs-dynamic trace sample.rb --with-literal-type 2 class X 2 # RBS dynamic trace 0.1.0 3 4 class X 3 4 5 6 end 5 6 def func: (Symbol | :hoge | :foo | :bar a) -> NilClass end 7 x = X.new 7 $ 8 9 x.func(:hoge) x.func(:foo) 10 54 / 67 def func(a) end x.func(:bar)

96.
[beta]
Hash 型の場合
この機能を有効にしている場合に `Hash` では以下のように型定義される
`Hash` のキーがどのような値になっているのか推論しやすくなっている
1
2
3

# sample.rb
class X
def func(user)

4
5

end
end

6
7

x = X.new

8

x.func({ id: 1, name: "homu", age: 14 })

9

x.func({ id: 1, name: "mami", age: 15 })

1
2

$ rbs-dynamic trace sample.rb --with-literal-type
# RBS dynamic trace 0.1.0

3

55 / 67

4
5

class X
def func: (Hash[Symbol | :id | :name | :age, Integer | String | 1 | 14 | 15] user) -> NilClass

6
7

end
$

97.

5. TypeProf と比較してみる 56 / 67

98.

TypeProf とは 57 / 67

99.

TypeProf とは `TypeProf` 57 / 67 とは Ruby 3.0 から標準に bundle されている RBS データを生成するツール

100.

TypeProf とは とは Ruby 3.0 から標準に bundle されている RBS データを生成するツール `rbs-dynamic` とは違い静的に Ruby のコードを解析して RBS データを生成している `TypeProf` なので `rbs-dynamic` とは違い実行時の副作用は存在しない 57 / 67

101.
[beta]
生成される RBS を比較 その1

58 / 67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class FizzBuzz
def initialize(value)
@value = value
end

1
2
3
4
5
6
7
8
9
10
11

# TypeProf 0.21.3

def value; @value end
def apply
value % 15 == 0 ? "FizzBuzz"
: value % 3 == 0 ? "Fizz"
: value % 5 == 0 ? "Buzz"
: value
end
end
(1..20).each {
FizzBuzz.new(_1).apply
}

# Classes
class FizzBuzz
@value: untyped
def initialize: (untyped value)
-> void
def value: -> untyped
def apply: -> String
end

1
2
3
4
5
6
7
8
9
10
11
12

# RBS dynamic trace 0.1.0
class FizzBuzz
private def initialize: (Integer value)
-> Integer
def apply: () -> (Integer | String)
def value: () -> Integer
@value: Integer
end

102.
[beta]
生成される RBS を比較 その1

59 / 67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class FizzBuzz
def initialize(value)
@value = value
end

1
2
3
4
5
6
7
8
9
10
11

# TypeProf 0.21.3

def value; @value end
def apply
value % 15 == 0 ? "FizzBuzz"
: value % 3 == 0 ? "Fizz"
: value % 5 == 0 ? "Buzz"
: value
end
end
(1..20).each {
FizzBuzz.new(_1).apply
}

# Classes
class FizzBuzz
@value: untyped
def initialize: (untyped value)
-> void
def value: -> untyped
def apply: -> String
end

`#initialize`

や `@value` の型定義が異なる

1
2
3
4
5
6
7
8
9
10
11
12

# RBS dynamic trace 0.1.0
class FizzBuzz
private def initialize: (Integer value)
-> Integer
def apply: () -> (Integer | String)
def value: () -> Integer
@value: Integer
end

103.

生成される RBS を比較 その2 60 / 67 1 2 3 4 5 6 7 8 9 10 11 12 class Base def func; self end end 1 2 3 4 5 6 7 8 9 10 11 12 # TypeProf 0.21.3 class Sub1 < Base end class Sub2 < Base end Sub1.new.func Sub2.new.func # Classes class Base def func: -> (Sub1 | Sub2) end class Sub1 < Base end class Sub2 < Base end 1 2 3 4 5 6 7 8 9 10 11 12 13 # RBS dynamic trace 0.1.0 class Base def func: () -> (Sub1 | Sub2) end class Sub1 < Base def func: () -> Sub1 end class Sub2 < Base def func: () -> Sub2 end

104.

生成される RBS を比較 その2 1 2 3 4 5 6 7 8 9 10 11 12 class Base def func; self end end 1 2 3 4 5 6 7 8 9 10 11 12 # TypeProf 0.21.3 class Sub1 < Base end class Sub2 < Base end Sub1.new.func Sub2.new.func # Classes class Base def func: -> (Sub1 | Sub2) end class Sub1 < Base end class Sub2 < Base end 1 2 3 4 5 6 7 8 9 10 11 12 13 # RBS dynamic trace 0.1.0 class Base def func: () -> (Sub1 | Sub2) end class Sub1 < Base def func: () -> Sub1 end class Sub2 < Base def func: () -> Sub2 end は定義元でメソッドが定義され `rbs-dynamic` は定義元とレシーバのクラスの両方 で定義される `TypeProf` 61 / 67

105.
[beta]
生成される RBS を比較 その3

62 / 67

1
2
3
4
5
6
7
8
9

class X
def func(a)
a
end
end

1
2
3
4
5
6
7

# TypeProf 0.21.3

X.new.func(42)
X.new.func(:hoge)
X.new.func({ id: 1, name: "homu" })

# Classes
class X
def func: (:hoge | Integer | {id: Integer, name: String} a)
-> (:hoge | Integer | {id: Integer, name: String})
end

1
2
3
4
5
6
7
8

# RBS dynamic trace 0.1.0
class X
def func: (Integer a) -> Integer
| (Symbol a) -> Symbol
| (Hash[Symbol, Integer | String] a)
-> Hash[Symbol, Integer | String]
end

106.
[beta]
生成される RBS を比較 その3
1
2
3
4
5
6
7
8
9

class X
def func(a)
a
end
end

1
2
3
4
5
6
7

# TypeProf 0.21.3

X.new.func(42)
X.new.func(:hoge)
X.new.func({ id: 1, name: "homu" })

# Classes
class X
def func: (:hoge | Integer | {id: Integer, name: String} a)
-> (:hoge | Integer | {id: Integer, name: String})
end

`TypeProf`
`Hash`

63 / 67

1
2
3
4
5
6
7
8

では `Symbol` 型を値として扱っている

# RBS dynamic trace 0.1.0
class X
def func: (Integer a) -> Integer
| (Symbol a) -> Symbol
| (Hash[Symbol, Integer | String] a)
-> Hash[Symbol, Integer | String]
end

型ではなくて `{id: Integer, name: String}` のような Record 型定義になっている

107.
[beta]
生成される RBS を比較 その4
1
2
3
4
5
6
7
8
9
10

1
2
3
4
5
6
7

class X
def func(&block)
block.call(42)
block.call("homu")
[1, 2, 3].map(&block)
end
end
X.new.func { _1 + _1 }
X.new.func(&:to_s)

# TypeProf 0.21.3
# Classes
class X
def func: { (?Integer | String) -> (Integer | String) }
-> (Array[Integer | String])
end

ほぼ同じ

64 / 67

1
# RBS dynamic trace 0.1.0
2
3
class X
4
def func: () ?{ (?Integer | String _1) -> (Integer |
String) }
5
-> Array[Integer | String]
6
end

108.

6. 今後の課題 65 / 67

109.

やること / 考えること 66 / 67

110.

やること / 考えること RBS ファイルをどう生成するのか どのタイミングで RBS ファイルを自動生成するといいのか 既存の RBS ファイルをコンフリクトしないように生成する仕組みを考える必要がある 66 / 67

111.

やること / 考えること RBS ファイルをどう生成するのか どのタイミングで RBS ファイルを自動生成するといいのか 既存の RBS ファイルをコンフリクトしないように生成する仕組みを考える必要がある 『動いているコードが正となる』ことに対するギャップ 一般的な静的型付けの場合は型が先にあり、そのあとに動くコードがある `rbs-dynamic` の場合は先に動いているコードがあり、あとで型情報がやってくる 意図しない型だったとしても動いているコードが正になる 66 / 67

112.

やること / 考えること RBS ファイルをどう生成するのか どのタイミングで RBS ファイルを自動生成するといいのか 既存の RBS ファイルをコンフリクトしないように生成する仕組みを考える必要がある 『動いているコードが正となる』ことに対するギャップ 一般的な静的型付けの場合は型が先にあり、そのあとに動くコードがある `rbs-dynamic` の場合は先に動いているコードがあり、あとで型情報がやってくる 意図しない型だったとしても動いているコードが正になる 型の抽象度を上げてみる 例えば `class Sub1 < Base` と `class Sub2 < Base` がある時に `Sub1 | Sub2` を `Base` 型に寄せる事ができ ないか 66 / 67

113.

やること / 考えること RBS ファイルをどう生成するのか どのタイミングで RBS ファイルを自動生成するといいのか 既存の RBS ファイルをコンフリクトしないように生成する仕組みを考える必要がある 『動いているコードが正となる』ことに対するギャップ 一般的な静的型付けの場合は型が先にあり、そのあとに動くコードがある `rbs-dynamic` の場合は先に動いているコードがあり、あとで型情報がやってくる 意図しない型だったとしても動いているコードが正になる 型の抽象度を上げてみる 例えば `class Sub1 < Base` と `class Sub2 < Base` がある時に `Sub1 | Sub2` を `Base` 型に寄せる事ができ ないか そもそも RBS をどう活用するのか でのみ利用する? 他に RBS が利用できる場面がないか考えたい `Steep` 66 / 67

114.

最後に rbs-dynamic で生成した ActiveRecord の RBS をながめて見よう! 67 / 67