Let's collect type info during Ruby running and automaticall (en)

303 Views

September 10, 22

スライド概要

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

Currently, Ruby is introducing RBS, which defines type information, and developing TypeProf, which statically analyzes Ruby code to extract type information, as efforts to improve the Developer Experience. So I am working on a different approach than TypeProf, using TracePoint to collect information on method calls when Ruby code is executed and generate RBS files based on that information. I will explain the advantages and disadvantages of this approach compared to TypeProf, as well as how I achieved it. Let's use this session as an opportunity to get more people interested in RBS and work together to improve the future Ruby Developer Experience!

シェア

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

各ページのテキスト
1.

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

2.

Hey guys, are you writing RBS files???

3.

I don't write RBS files!

4.

But I want benefits of type!!! I want code completion in the editor!!!!

5.

Trace the Ruby process at runtime and RBS data automatically generated!!!!

6.

Profile name: osyo Twitter : @pink_bangbi https://twitter.com/pink_bangbi github : osyo-manga https://github.com/osyo-manga Blog : Secret Garden(Instrumental) http://secret-garden.hatenablog.com Rails engineer My favorite Ruby feature is Refinements My Editor is Vim :) Second time to attend RubyKaigi Use Macro all the time ~ マクロを使いまくろ ~ RubyKaigi Takeout 2021 [slides en / ja] 6 / 67

7.

Today talk

8.

Today talk created rbs-dynamic, which tracing Ruby runtime and generates RBS.

9.

Agenda 1. What is RBS? 2. Why did I decide to do it? 3. How it was implemented 4. Considerations in the automatic generation of RBS 5. Compare with TypeProf 6. Future Issues 8 / 67

10.

What is RBS? 9 / 67

11.

What is RBS? 10 / 67

12.

What is RBS? Data format for handling type information in Ruby Written in a syntax similar to Ruby Static type checking `Steep` scrutinizes based on RBS information 10 / 67

13.

What is RBS? Data format for handling type information in Ruby Written in a syntax similar to Ruby Static type checking `Steep` scrutinizes based on RBS information Types in RBS often refer to classes in Ruby. Strictly speaking, it’s a little different, but I’m OK with this understanding for now. 10 / 67

14.

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

15.

RBS Sample 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 Define method argument and return types This is a method type definition that takes a `String` object and returns a value of type `String`.

16.

RBS Sample 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 Define method argument and return types This is a method type definition that takes a `String` object and returns a value of type `String`. `Array[String]` is defined to be an `Array` with elements of type `String`.

17.

RBS Sample 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 Define method argument and return types This is a method type definition that takes a `String` object and returns a value of type `String`. `Array[String]` is defined to be an `Array` with elements of type `String`. Using `|` will result in a typedef of type `String` or `Symbol`.

18.

RBS Sample 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 class Math 12 13 14 PI: Float end 15 class MyClass 16 17 @value: Integer end Define method argument and return types This is a method type definition that takes a `String` object and returns a value of type `String`. `Array[String]` is defined to be an `Array` with elements of type `String`. Using `|` will result in a typedef of type `String` or `Symbol`. Typedefs can be defined for constants and instance variables

19.

RBS Sample 1 2 3 def replace: (String other_str) -> String 4 def chars: () -> Array[String] 5 end 6 7 class Object 8 9 10 def method: (String | Symbol name) -> Method end 11 class Math 12 11 / 67 class String PI: Float 13 14 end 15 class MyClass 16 17 @value: Integer end Define method argument and return types This is a method type definition that takes a `String` object and returns a value of type `String`. `Array[String]` is defined to be an `Array` with elements of type `String`. Using `|` will result in a typedef of type `String` or `Symbol`. Typedefs can be defined for constants and instance variables

20.

And so 12 / 67

21.

And so Let’s actually try it out! 12 / 67

22.

Demo :) 13 / 67

23.

2. Why did I decide to do it? 14 / 67

24.

I wanted to take a different approach than `TypeProf`. 15 / 67

25.

I wanted to take a different approach than `TypeProf`. I was wondering if it is possible to generate RBS automatically, because I don’t want to write types, but I want benefits from them. 15 / 67

26.

I wanted to take a different approach than `TypeProf`. I was wondering if it is possible to generate RBS automatically, because I don’t want to write types, but I want benefits from them. Static analysis of Ruby code to generate RBS is already done by `TypeProf`. 15 / 67

27.

I wanted to take a different approach than `TypeProf`. I was wondering if it is possible to generate RBS automatically, because I don’t want to write types, but I want benefits from them. Static analysis of Ruby code to generate RBS is already done by `TypeProf`. As an alternative approach, I wanted to actually collect type information at Ruby runtime and generate an RBS. 15 / 67

28.

I wanted to take a different approach than `TypeProf`. I was wondering if it is possible to generate RBS automatically, because I don’t want to write types, but I want benefits from them. Static analysis of Ruby code to generate RBS is already done by `TypeProf`. As an alternative approach, I wanted to actually collect type information at Ruby runtime and generate an RBS. I want to solve the problems that `TypeProf` can’t solve! I want to make something better than `TypeProf`! I am motivated more by "I wanted to try it" than "I want to make something better than `TypeProf`". I wanted to compare it to `TypeProf` on that basis. 15 / 67

29.

I want / need to be interested in RBS 16 / 67

30.

I want / need to be interested in RBS I have the impression that there are a certain number of people who are interested in Ruby types, but it has not spread to the general public yet. 16 / 67

31.

I want / need to be interested in RBS I have the impression that there are a certain number of people who are interested in Ruby types, but it has not spread to the general public yet. I hope that this session will trigger more discussions about RBS and types. 16 / 67

32.

I want / need to be interested in RBS I have the impression that there are a certain number of people who are interested in Ruby types, but it has not spread to the general public yet. I hope that this session will trigger more discussions about RBS and types. We do not intend to make "dynamically generating RBS" the goal of our approach this time, but would like to think together about how to handle RBS in the future. 16 / 67

33.

3. How it was implemented 17 / 67

34.

Mounting image 1. Collect Ruby method invocation information at runtime 2. Generate RBS data from collected information 3. Save the generated data as a files 18 / 67

35.

1. collect Ruby method invocation information at runtime 19 / 67

36.

Use `TracePoint` to collect information `TracePoint` library can be used to hook a process when an arbitrary event is executed 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 } def func(a) a end puts "# Result" tp.enable { func(42) func("homu") } 1 2 3 4 5 # Result call: Object#func() return: 42 call: Object#func() return: homu

37.

Use `TracePoint` to collect information `TracePoint` library can be used to hook a process when an arbitrary event is executed 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 } def func(a) a end puts "# Result" tp.enable { func(42) func("homu") } 1 2 3 4 5 # Result call: Object#func() return: 42 call: Object#func() return: homu Create an object of `TracePoint`. block is called when an event occurs

38.

Use `TracePoint` to collect information `TracePoint` library can be used to hook a process when an arbitrary event is executed 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 } def func(a) a end puts "# Result" tp.enable { func(42) func("homu") } 1 2 3 4 5 # Result call: Object#func() return: 42 call: Object#func() return: homu Create an object of `TracePoint`. block is called when an event occurs Trace processing within a block

39.

Use `TracePoint` to collect information `TracePoint` library can be used to hook a process when an arbitrary event is executed 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 } def func(a) a end puts "# Result" tp.enable { func(42) func("homu") } 1 2 3 4 5 # Result call: Object#func() return: 42 call: Object#func() return: homu Create an object of `TracePoint`. block is called when an event occurs Trace processing within a block Branch processing depending on events is a method call `return` is when you `return` from a method `call` 20 / 67

40.
[beta]
Use `TracePoint` to collect information
`TracePoint` library can be used to hook a process when an arbitrary event is executed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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
}
def func(a)
a
end
puts "# Result"
tp.enable {
func(42)
func("homu")
}

1
2
3
4
5

# Result
call: Object#func()
return: 42
call: Object#func()
return: homu

Create an object of `TracePoint`.
block is called when an event occurs

Trace processing within a block
Branch processing depending on
events

is a method call
`return` is when you `return` from a
method
`call`

20 / 67

Obtain meta-information such as
method name, return value, etc.

41.

Meta information collected by `rbs-dynamic `rbs-dynamic` also retrieves information that cannot be retrieved by `TracePoint`. Data needed to define instance variables and RBS 21 / 67

42.

Meta information collected by `rbs-dynamic `rbs-dynamic` also retrieves information that cannot be retrieved by `TracePoint`. Data needed to define instance variables and RBS The meta-information of the method invoked within its own method is also kept nested in order to check the caller of the method. Keeps information of `func2` method called in `func1` method as meta-information of `func1` method. 21 / 67

43.
[beta]
Meta information handled by `rbs-dynamic`
1
2
3
4
5
6
7
8
9

22 / 67

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

# method name
# defined class

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

10
11
12
13
14
15

:lineno=>4,
# defined method line
:path=> "/path/to/sample.rb", # defined method path
:visibility=>:public,
# method visibility
:singleton_method?=>false,
:called_lineno=>11,
# called method line
:called_path=> "/path/to/sample.rb",
# called method path

16
17
18
19
20

:called_method_id=>nil,
:called_methods=>[..],
:block?=>false,
:block=>[],
:trace_point_event=>:call,

21
22
23

:return_value_class=>String, # return value class
:return_value_rbs_type=>{:type=>String, :value=>nil, :args=>[]},
:instance_variables_class=>{}}]
# instance variables table

# called method name
# called method info in method
# block

44.

2. Generate RBS data from the collected information 23 / 67

45.

Generate RBS data using the rbs library Standard libraries for working with RBS data in Ruby are also bundled. In this case, this is used to convert the collected method meta-information into RBS data. 24 / 67

46.

Generate RBS data 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 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 ) stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string Result 1 2 3 class X def func: (Integer n) -> String end

47.

Generate RBS data 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 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 ) stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string Result 1 2 3 class X def func: (Integer n) -> String end Define argument types

48.

Generate RBS data 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 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 ) stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string Result 1 2 3 class X def func: (Integer n) -> String end Define argument types Define return types

49.

Generate RBS data 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 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 ) stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string Result 1 2 3 class X def func: (Integer n) -> String end Define argument types Define return types Define method types

50.

Generate RBS data 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 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 ) stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string Result 1 2 3 class X def func: (Integer n) -> String end Define argument types Define return types Define method types Define class types

51.

Generate RBS data 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 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 ) stdout = StringIO.new writer = RBS::Writer.new(out: stdout) writer.write(decls) puts stdout.string Result 1 2 3 class X def func: (Integer n) -> String end Define argument types Define return types Define method types Define class types Output to any IO from defined RBS data

52.

Implemented to collect meta-information of methods and generate RBS data at Ruby runtime MEMO: Minimal implementation 26 / 67

53.

Try it out for yourself 27 / 67

54.
[beta]
Generating RBS data from Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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
end
end
rbs = RBS::Dynamic.trace_to_rbs_text do
(1..20).each { FizzBuzz.new(_1).apply }
end
puts rbs

Result
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]
Generating RBS data from Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14

29 / 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

Result
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

Generate RBS data from methods called in the
block

56.
[beta]
Generating RBS data from Ruby
1
2

require "rbs/dynamic"

1
2

3
4
5
6

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

3
4
5
6

7
8
9

def value; @value end

7
8
9

10
11
12

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

13
14
15

30 / 67

Result

: value %

5 == 0 ? "Fuzz"

: value
end

16
17
18
19

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

Generate RBS data from methods called in the
block
In addition to methods, instance variables are
also typed

57.
[beta]
Generated by `rbs-dynamic` command
1
2
3
4
5
6

# sample.rb
class FizzBuzz
def initialize(value)
@value = value
end

7
8
9
10
11
12

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

13
14
15
16
17

: value
end
end

18

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

Result
1

$ rbs-dynamic trace sample.rb

2
3
4
5
6
7

# RBS dynamic trace 0.1.0

8
9
10
11
12
13

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

puts "Result"

NODE: `rbs-dynamic` command does not produce standard Ruby output when executed.
31 / 67

58.
[beta]
Other RBS types
1
2
3
4
5
6
7
8
9
10
11
12

32 / 67

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
14
15
16
17
18

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

19
20
21
22

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

Result
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

59.
[beta]
Other type definitions
1

require "rbs/dynamic"

2
3
4

class X
VALUE = 42

5
6

def self.get_value

7

33 / 67

VALUE

8
9
10

end

11
12
13

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

14
15
16

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

def func1(value:)

17

end

18
19

x = X.new

20
21
22

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

Result
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

Define constant variables

60.
[beta]
Other type definitions
1

require "rbs/dynamic"

1

2
3
4

class X
VALUE = 42

2
3
4

5
6
7
8
9
10

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

11
12
13
14
15
16

34 / 67

Result

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

17

end

18
19
20

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

21
22

}

x.func1(value: 1)

5
6
7

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

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

Define constant variables
Define class methods

61.
[beta]
Other type definitions
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

35 / 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)

Result
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

Define constant variables
Define class methods
Define method with keyword arguments

62.
[beta]
Other type definitions
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

36 / 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)
}

Result
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

Define constant variables
Define class methods
Define method with keyword arguments
Define method with block arguments and
private
`?{ (?Integer a) -> String }`

types

is block argument

63.

Type supported by rbs-dynamic class / module super class `include / prepend / extend` instance variables constant variables Method argument types return type block visibility class methods literal types (e.g. `1` `:hoge`) `String` 37 / 67 literals are not supported. Generics types `Array` `Hash` `Range` Non supported class variables Generics types `Enumerable` `Struct` Record types Tuple types

64.

`rbs-dynamic` command options 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 9 [--target-filepath-pattern=TARGET-FILEPATH-PATTERN] # Target filepath pattern. e.g. hoge\|foo\|bar. Default '.*' # Default: .* 10 [--ignore-filepath-pattern=IGNORE-FILEPATH-PATTERN] # Ignore filepath pattern. Priority over `target-filepath-pattern`. e.g. hoge\|foo\|bar. 11 [--target-classname-pattern=TARGET-CLASSNAME-PATTERN] # Target class name pattern. e.g. RBS::Dynamic. Default '.*' 12 13 [--ignore-classname-pattern=IGNORE-CLASSNAME-PATTERN] # Default: .* # Ignore class name pattern. Priority over `target-classname-pattern`. e.g. PP\|PrettyPrint. 7 # Default: /path/to/current Default '' Default '' 14 [--ignore-class-members=one two three] 15 instance_variables, singleton_methods, methods 16 [--method-defined-calsses=one two three] # Which class defines method type. Default: defined_class and receiver_class 18 [--show-method-location], [--no-show-method-location] # Show source_location and called_location in method comments. Default: no 19 20 [--use-literal-type], [--no-use-literal-type] [--with-literal-type], [--no-with-literal-type] # Integer and Symbol as literal types. e.g func(:hoge, 42). Default: no # Integer and Symbol with literal types. e.g func(Symbol | :hoge | :foo). Default: no 21 [--use-interface-method-argument], [--no-use-interface-method-argument] # 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 17 38 / 67 # Possible values: inclued_modules, prepended_modules, extended_modules, constant_variables, # Possible values: defined_class, receiver_class

65.

3. Save the generated data as a files 39 / 67

66.

Not yet implemented 40 / 67

67.

I wanted to do 41 / 67

68.

I wanted to do Trace information for each request in Rails apps and automatically generate RBS files And at that time, merge the existing RBS files to avoid conflicts. 41 / 67

69.

I wanted to do Trace information for each request in Rails apps and automatically generate RBS files And at that time, merge the existing RBS files to avoid conflicts. Automatically generate RBS files when running tests in CI Automated up to commit to repository 41 / 67

70.

I wanted to do Trace information for each request in Rails apps and automatically generate RBS files And at that time, merge the existing RBS files to avoid conflicts. Automatically generate RBS files when running tests in CI Automated up to commit to repository It is necessary to consider separately when the RBS files should be saved. 41 / 67

71.

4. Considerations for automatic RBS generation 42 / 67

72.

Interface type implementation 43 / 67

73.

What is Interface type? Types that only accept types for which specific methods are defined 44 / 67

74.

What is Interface type? Types that only accept types for which specific methods are defined Suppose you have a `my_puts` method that calls the following `#to_s` method internally 44 / 67 1 def my_puts(a) 2 3 puts a.to_s end

75.

What is Interface type? Types that only accept types for which specific methods are defined Suppose you have a `my_puts` method that calls the following `#to_s` method internally 1 def my_puts(a) 2 3 puts a.to_s end In RBS, the Interface type can be used in such cases. Interface types can be defined as types that "only accept types with specific methods. 44 / 67

76.

What is Interface type? Types that only accept types for which specific methods are defined Suppose you have a `my_puts` method that calls the following `#to_s` method internally 1 2 3 def my_puts(a) puts a.to_s end In RBS, the Interface type can be used in such cases. Interface types can be defined as types that "only accept types with specific methods. Typedef of `my_puts` using the Interface type is as follows 1 2 # Accept only objects with a to_s method that returns a String type 3 4 interface _ToS def to_s: () -> String 5 6 end 7 8 44 / 67 class Object private def my_puts: (_ToS) -> NilClass end

77.

What is Interface type? Types that only accept types for which specific methods are defined Suppose you have a `my_puts` method that calls the following `#to_s` method internally 1 2 3 def my_puts(a) puts a.to_s end In RBS, the Interface type can be used in such cases. Interface types can be defined as types that "only accept types with specific methods. Typedef of `my_puts` using the Interface type is as follows 1 class Object 2 # Accept only objects with a to_s method that returns a String type 3 4 interface _ToS def to_s: () -> String 5 6 end 7 8 private def my_puts: (_ToS) -> NilClass end 44 / 67`rbs-dynamic` implements automatic Interface type definition.

78.
[beta]
Interface type is automatically defined
Add `--use-interface-method-argument` command option to `rbs-dynamic`.
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

5
6

private def my_puts: (_Interface_have__to_s__1 a) -> NilClass

class Dog

5
6

7
8

def to_s; "Dog" end
end

7
8

interface _Interface_have__to_s__1
def to_s: () -> String

9

45 / 67

9

end

10
11

def my_puts(a)
puts a.to_s

10
11

end

12
13

end

12
13

class Cat
def to_s: () -> String

14

my_puts Cat.new

14

end

15

my_puts Dog.new

15
16

class Dog

17
18

def to_s: () -> String
end

19

$

79.

Method definition source and reference source 46 / 67

80.

Method definition source and reference source Sometimes when I’m reading code, I want to refer to the source or reference of a method definition. 47 / 67

81.

Method definition source and reference source Sometimes when I’m reading code, I want to refer to the source or reference of a method definition. However, RBS does not maintain method source location. 47 / 67

82.

Method definition source and reference source Sometimes when I’m reading code, I want to refer to the source or reference of a method definition. However, RBS does not maintain method source location. In this implementation, I tried to embed the location information in the RBS comments. 47 / 67

83.

Sample Add `--show-method-location` command option to `rbs-dynamic`. 1 2 3 4 # sample.rb class X def func1(a) end 5 6 7 8 9 def func2(a) func1("homu") end end 10 48 / 67 1 2 $ rbs-dynamic trace sample.rb --show-method-location # RBS dynamic trace 0.1.0 3 4 class X 5 # source location: sample.rb:3 6 7 # reference location: # func1(Integer a) -> NilClass sample.rb:12 8 9 # func1(String a) -> NilClass sample.rb:7 def func1: (Integer | String a) -> NilClass 10 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 $

84.

Sample Add `--show-method-location` command option to `rbs-dynamic`. 1 2 3 4 # sample.rb class X def func1(a) end 5 6 7 8 9 def func2(a) func1("homu") end end $ rbs-dynamic trace sample.rb --show-method-location # RBS dynamic trace 0.1.0 3 4 class X 5 # source location: sample.rb:3 6 7 # reference location: # func1(Integer a) -> NilClass sample.rb:12 8 9 # func1(String a) -> NilClass sample.rb:7 def func1: (Integer | String a) -> NilClass 10 11 x = X.new 10 11 # source location: sample.rb:6 12 x.func1(42) 12 # reference location: 13 x.func2("mami") 13 14 # func2(String a) -> NilClass sample.rb:13 def func2: (String a) -> NilClass `X#func1` 49 / 67 1 2 defined location 15 end 16 $

85.

Sample Add `--show-method-location` command option to `rbs-dynamic`. 1 2 # sample.rb class X $ rbs-dynamic trace sample.rb --show-method-location # RBS dynamic trace 0.1.0 class X # source location: sample.rb:3 3 4 5 def func1(a) end 3 4 5 6 7 def func2(a) func1("homu") 6 7 # reference location: # func1(Integer a) -> NilClass sample.rb:12 8 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 14 # func2(String a) -> NilClass sample.rb:13 def func2: (String a) -> NilClass `X#func1` 50 / 67 1 2 referenced location def func1: (Integer | String a) -> NilClass 15 end 16 $

86.

Treat Symbol as type or literal 51 / 67

87.

Symbol can be treated as a literal RBS can define `Symbol` literals as types Available if you want to accept only specific `Symbol` literals. Similar to `enum` types in other languages 1 2 3 # Define a method type that accepts only the literals of a specific Symbol def upcase: () -> String 4 5 | (:ascii | :lithuanian | :turkic) -> String | (:lithuanian, :turkic) -> String 6 | (:turkic, :lithuanian) -> String 7 52 / 67 class String end

88.

On the other hand, there are also methods like `#attr_accessor` that do not depend on a specific literal 1 2 3 4 53 / 67 module Module # Must accept any Symbol literal def attr_accessor: (*(Symbol | String) arg0) -> NilClass end

89.

On the other hand, there are also methods like `#attr_accessor` that do not depend on a specific literal 1 2 3 4 module Module # Must accept any Symbol literal def attr_accessor: (*(Symbol | String) arg0) -> NilClass end So `rbs-dynamic` uses the `Symbol` type instead of `:hoge`. 1 2 3 4 5 def func(a) 1 2 $ rbs-dynamic trace sample.rb # RBS dynamic trace 0.1.0 3 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)

90.

On the other hand, there are also methods like `#attr_accessor` that do not depend on a specific literal 1 2 3 4 module Module # Must accept any Symbol literal def attr_accessor: (*(Symbol | String) arg0) -> NilClass end So `rbs-dynamic` uses the `Symbol` type instead of `:hoge`. 1 2 3 4 5 # sample.rb class X def func(a) 1 2 $ rbs-dynamic trace sample.rb # RBS dynamic trace 0.1.0 3 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) Nevertheless, there are cases where it is easier to understand if the `Symbol` literal is known 53 / 67

91.

Tried to define a type by `Symbol` type + `Symbol` literal 54 / 67

92.

Tried to define a type by `Symbol` type + `Symbol` literal Define `Symbol` type + its literal as a type, like `Symbol | :hoge | :foo | :bar` instead of just `Symbol` type. 54 / 67

93.

Tried to define a type by `Symbol` type + `Symbol` literal Define `Symbol` type + its literal as a type, like `Symbol | :hoge | :foo | :bar` instead of just `Symbol` type. This way, while receiving a `Symbol` type, you can check what literal you are receiving by looking at the RBS. 54 / 67

94.

Tried to define a type by `Symbol` type + `Symbol` literal Define `Symbol` type + its literal as a type, like `Symbol | :hoge | :foo | :bar` instead of just `Symbol` type. This way, while receiving a `Symbol` type, you can check what literal you are receiving by looking at the RBS. Add `--with-literal-type` command option to `rbs-dynamic`. 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)

95.
[beta]
Hash type
With this feature enabled, `Hash` types are defined as follows
It is easy to infer what the literal of the `Hash` key is.
1
2
3

# sample.rb
class X
def func(user)

4
5

end
end

6
7

x = X.new

8
9

x.func({ id: 1, name: "homu", age: 14 })
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
$

96.

5. Compare with TypeProf 56 / 67

97.

What is TypeProf? 57 / 67

98.

What is TypeProf? TypeProf` is a tool to generate RBS data, bundled as standard since Ruby 3.0. 57 / 67

99.

What is TypeProf? TypeProf` is a tool to generate RBS data, bundled as standard since Ruby 3.0. Unlike `rbs-dynamic`, it statically parses Ruby code and generates RBS data So, unlike `rbs-dynamic`, there are no runtime side effects. 57 / 67

100.
[beta]
Comparison of generated RBS Part 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

101.
[beta]
Comparison of generated RBS Part 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

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

Different type definitions for `#initialize` and `@value`.

102.

Comparison of generated RBS Part 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

103.
[beta]
Comparison of generated RBS Part 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

is defined by the method at the source, `rbs-dynamic` by both the source and
61 / 67 receiver classes.
`TypeProf`

104.
[beta]
Comparison of generated RBS Part 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

105.
[beta]
Comparison of generated RBS Part 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`

treats `Symbol` type as a literal.

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

Record type definitions like `{id: Integer, name: String}` instead of `Hash` type.

63 / 67

106.
[beta]
Comparison of generated RBS Part 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

almost the same

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

107.

6. Future Issues 65 / 67

108.

Things to do / Things to think about 66 / 67

109.

Things to do / Things to think about How generating RBS files When it would be better the RBS files to automatically generated? Need to come up with a mechanism to generate existing RBS files without conflicts 66 / 67

110.

Things to do / Things to think about How generating RBS files When it would be better the RBS files to automatically generated? Need to come up with a mechanism to generate existing RBS files without conflicts Gap against the runtime code being positive In general static typing, the type comes first, then runtime code In the case of `rbs-dynamic`, runtime code first, and the type information comes later Even if it was an unintended type, runtime code will be positive 66 / 67

111.

Things to do / Things to think about How generating RBS files When it would be better the RBS files to automatically generated? Need to come up with a mechanism to generate existing RBS files without conflicts Gap against the runtime code being positive In general static typing, the type comes first, then runtime code In the case of `rbs-dynamic`, runtime code first, and the type information comes later Even if it was an unintended type, runtime code will be positive Try to raise the level of abstraction of the type. For example, if we have `class Sub1 < Base` and `class Sub2 < Base`, can we put `Sub1 | Sub2` into `Base` type? 66 / 67

112.

Things to do / Things to think about How generating RBS files When it would be better the RBS files to automatically generated? Need to come up with a mechanism to generate existing RBS files without conflicts Gap against the runtime code being positive In general static typing, the type comes first, then runtime code In the case of `rbs-dynamic`, runtime code first, and the type information comes later Even if it was an unintended type, runtime code will be positive Try to raise the level of abstraction of the type. For example, if we have `class Sub1 < Base` and `class Sub2 < Base`, can we put `Sub1 | Sub2` into `Base` type? How to utilize RBS in the first place Use only with `Steep`? I would like to consider if there are other situations where RBS can be used. 66 / 67

113.

Thanks every one :) Finally, `rbs-dynamic` generated Let’s take a gander at ActiveRecord’s RBS! 67 / 67