type_toolkit 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +43 -0
- data/benchmark/.rubocop.yml +7 -0
- data/benchmark/abstract_methods_benchmark.rb +221 -0
- data/benchmark/interface_startup_performance.rb +128 -0
- data/benchmark/module_benchmark.rb +62 -0
- data/config/default.yml +5 -0
- data/docs/design/inherited_implementation_problem.md +119 -0
- data/docs/design/self_dot_methods.md +79 -0
- data/lib/rubocop/cop/type_toolkit/plugin.rb +1 -1
- data/lib/rubocop/cop/type_toolkit/prefer_not_nil.rb +96 -0
- data/lib/rubocop-type_toolkit.rb +1 -0
- data/lib/type_toolkit/abstract_method_receiver.rb +39 -0
- data/lib/type_toolkit/dsl.rb +64 -0
- data/lib/type_toolkit/ext/method.rb +11 -0
- data/lib/type_toolkit/ext/module.rb +7 -0
- data/lib/type_toolkit/ext/nil_assertions.rb +3 -1
- data/lib/type_toolkit/has_abstract_methods.rb +108 -0
- data/lib/type_toolkit/interface.rb +38 -0
- data/lib/type_toolkit/method_def_recorder.rb +53 -0
- data/lib/type_toolkit/method_patch.rb +16 -0
- data/lib/type_toolkit/version.rb +1 -1
- data/lib/type_toolkit.rb +3 -0
- data/sorbet/config +3 -1
- data/sorbet/rbi/gems/benchmark-ips@2.14.0.rbi +981 -0
- data/sorbet/rbi/gems/{erb@6.0.1.rbi → erb@6.0.1.1.rbi} +2 -2
- data/sorbet/rbi/gems/{json@2.18.1.rbi → json@2.19.9.rbi} +115 -161
- data/sorbet/rbi/gems/minitest@5.27.0.rbi +707 -0
- data/sorbet/rbi/gems/rexml@3.4.4.rbi +0 -167
- data/sorbet/rbi/gems/rubocop-ast@1.49.0.rbi +6 -0
- data/sorbet/rbi/gems/rubocop@1.84.2.rbi +7 -0
- data/sorbet/rbi/gems/tapioca@0.17.10.rbi +1 -0
- data/sorbet/rbi/gems/{yard@0.9.38.rbi → yard@0.9.44.rbi} +1206 -241
- data/sorbet/rbi/shims/core.rbi +19 -0
- data/sorbet/rbi/shims/minitest.rbi +5 -2
- data/spec/interface_spec.rb +405 -0
- data/spec/rubocop/cop/type_toolkit/prefer_not_nil_spec.rb +229 -0
- data/spec/spec_helper.rb +14 -0
- metadata +23 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4227c717f707bc4d21aea4b6026e4ec48c62bf48044518b7545b4a12d499662
|
|
4
|
+
data.tar.gz: afbee5d26bf77e402c0425dc60cfb53b6d8c80ba8af6f69af39bad034459a3bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a251f77a0cc924ae2b621f1eda192d24d862406f076c875227ada2f7d16883a8c98e8c8b7310843a00a7d08b2fcab902e0f7bf6bcc5c4262da519a2e7a2721d8
|
|
7
|
+
data.tar.gz: a93074b2e21d1ea1b7dac91b9cf8e52690f68d1a2ae12e2c898cbd87b3ea71367a4d956d13933faef666e6683d868e7360a5b66a2039c8ccd735bdeefa82ae09
|
data/README.md
CHANGED
|
@@ -70,6 +70,49 @@ last_delivery = user.not_nil!
|
|
|
70
70
|
.deliveries.last.not_nil!
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### Interfaces
|
|
74
|
+
|
|
75
|
+
Interfaces are modules with abstract methods which a conforming class must implement. They help make duck-typing easier to use in Ruby, by validating that your conforming classes do actually provide the correct methods needed of them.
|
|
76
|
+
|
|
77
|
+
The Type Toolkit provides runtime support for interfaces (marked with `interface!`) and abstract methods (marked with `abstract` before the `def` keyword).
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
module Notifier
|
|
83
|
+
interface!
|
|
84
|
+
|
|
85
|
+
#: (String) -> void
|
|
86
|
+
abstract def send_notification(message); end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class SlackNotifier
|
|
90
|
+
include Notifier
|
|
91
|
+
|
|
92
|
+
# @override
|
|
93
|
+
#: (String) -> void
|
|
94
|
+
def send_notification(message)
|
|
95
|
+
puts "Posting to Slack API: #{message.inspect}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
SlackNotifier.new.send_notification("Hello, world!") # ✅
|
|
100
|
+
# => Posting to Slack API: "Hello, world!"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Unimplemented abstract methods cannot be called, and the Type Toolkit runtime will raise an error if you try to do so:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class EmailNotifier
|
|
107
|
+
include Notifier
|
|
108
|
+
|
|
109
|
+
# Oops, forgot to implement `#send_notification`!
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
EmailNotifier.new.send_notification("Hello, world!") # ❌ TypeToolkit::AbstractMethodNotImplementedError
|
|
113
|
+
# => Abstract method #send_notification was never implemented.
|
|
114
|
+
```
|
|
115
|
+
|
|
73
116
|
## Guiding Principles
|
|
74
117
|
|
|
75
118
|
### Blazingly fast™
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# typed: ignore
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Benchmark the performance overhead of calling:
|
|
5
|
+
# - A concrete implementation of an abstract method
|
|
6
|
+
# - An inherited concrete implementation of an abstract method
|
|
7
|
+
# - The error case of calling an unimplemented abstract method
|
|
8
|
+
|
|
9
|
+
############################################# Results #############################################
|
|
10
|
+
#
|
|
11
|
+
# ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
|
|
12
|
+
#
|
|
13
|
+
# ## Interpreter
|
|
14
|
+
#
|
|
15
|
+
# | Call to... | Regular impl | Inherited impl | Missing impl |
|
|
16
|
+
# |-------------------|--------------------:|------------------------:|--------------------------:|
|
|
17
|
+
# | sorbet-runtime | (same-ish) 23.02 ns | (2.70x slower) 57.30 ns | (1.13x slower) 472.86 ns |
|
|
18
|
+
# | manual delegation | (same-ish) 22.18 ns | (2.07x slower) 44.90 ns | *415.36 ns* |
|
|
19
|
+
# | type_toolkit | (same-ish) 22.56 ns | *22.03 ns* | (2.11x slower) 890.38 ns |
|
|
20
|
+
#
|
|
21
|
+
# ## YJIT#
|
|
22
|
+
# | Call to... | Regular impl | Inherited impl | Missing impl |
|
|
23
|
+
# |-------------------|--------------------:|-------------------------:|--------------------------:|
|
|
24
|
+
# | sorbet-runtime | (same-ish) 1.63 ns | (21.41x slower) 34.91 ns | (1.10x slower) 447.59 ns |
|
|
25
|
+
# | manual delegation | (same-ish) 1.63 ns | (7.15x slower) 11.66 ns | *405.84 ns* |
|
|
26
|
+
# | type_toolkit | (same-ish) 1.67 ns | *1.63 ns* | (1.91x slower) 774.91 ns |
|
|
27
|
+
#
|
|
28
|
+
####################################################################################################
|
|
29
|
+
|
|
30
|
+
require "bundler"
|
|
31
|
+
Bundler.require(:default, :benchmark)
|
|
32
|
+
|
|
33
|
+
require "type_toolkit"
|
|
34
|
+
|
|
35
|
+
module TypeKitDemo
|
|
36
|
+
# Provides the concrete implementation of `m`
|
|
37
|
+
class Parent
|
|
38
|
+
def m1 = "Parent#m1"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module I
|
|
42
|
+
interface!
|
|
43
|
+
|
|
44
|
+
abstract def m1; end
|
|
45
|
+
abstract def m2; end
|
|
46
|
+
abstract def not_implemented; end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Inherits the concrete implementation of `m` from DemoParentClass.
|
|
50
|
+
class Child < Parent
|
|
51
|
+
include I
|
|
52
|
+
|
|
53
|
+
def m2 = "Child#m2"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module SorbetRuntimeDemo
|
|
58
|
+
# Provides the concrete implementation of `m`
|
|
59
|
+
class Parent
|
|
60
|
+
def m1 = "Parent#m1"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module I
|
|
64
|
+
extend T::Sig
|
|
65
|
+
extend T::Helpers
|
|
66
|
+
|
|
67
|
+
interface!
|
|
68
|
+
|
|
69
|
+
sig { abstract.returns(String) }
|
|
70
|
+
def m1; end
|
|
71
|
+
|
|
72
|
+
sig { abstract.returns(String) }
|
|
73
|
+
def m2; end
|
|
74
|
+
|
|
75
|
+
sig { abstract.returns(String) }
|
|
76
|
+
def not_implemented; end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Inherits the concrete implementation of `m` from DemoParentClass.
|
|
80
|
+
class Child < Parent
|
|
81
|
+
include I
|
|
82
|
+
|
|
83
|
+
def m2 = "Child#m2"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module ManualDelegationDemo
|
|
88
|
+
class Parent
|
|
89
|
+
def m1 = "Parent#m1"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
module I
|
|
93
|
+
def m1 = defined?(super) ? super : raise
|
|
94
|
+
def m2 = defined?(super) ? super : raise
|
|
95
|
+
def not_implemented = defined?(super) ? super : raise
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Inherits the concrete implementation of `m` from DemoParentClass.
|
|
99
|
+
class Child < Parent
|
|
100
|
+
include I
|
|
101
|
+
|
|
102
|
+
def m2 = "Child#m2"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
type_toolkit_object = TypeKitDemo::Child.new
|
|
107
|
+
manual_delegation_object = ManualDelegationDemo::Child.new
|
|
108
|
+
sorbet_runtime_object = SorbetRuntimeDemo::Child.new
|
|
109
|
+
|
|
110
|
+
[:interpreter, :yjit].each do |mode|
|
|
111
|
+
if mode == :yjit
|
|
112
|
+
puts <<~MSG
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
================================================================================
|
|
116
|
+
Enabling YJIT...
|
|
117
|
+
================================================================================
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
MSG
|
|
121
|
+
RubyVM::YJIT.enable
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
warmup = 5
|
|
125
|
+
time = 10
|
|
126
|
+
|
|
127
|
+
width = ["type_toolkit", "sorbet-runtime", "manual delegation"].max_by(&:length).length
|
|
128
|
+
|
|
129
|
+
puts "Benchmark the performance of calling the concrete implementation directly..."
|
|
130
|
+
Benchmark.ips do |x|
|
|
131
|
+
x.config(warmup:, time:)
|
|
132
|
+
|
|
133
|
+
x.report("type_toolkit".rjust(width)) do |times|
|
|
134
|
+
i = 0
|
|
135
|
+
while (i += 1) < times
|
|
136
|
+
type_toolkit_object.m2
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
x.report("sorbet-runtime".rjust(width)) do |times|
|
|
141
|
+
i = 0
|
|
142
|
+
while (i += 1) < times
|
|
143
|
+
sorbet_runtime_object.m2
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
x.report("manual delegation".rjust(width)) do |times|
|
|
148
|
+
i = 0
|
|
149
|
+
while (i += 1) < times
|
|
150
|
+
manual_delegation_object.m2
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
x.compare!
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
puts "\n\nBenchmark the performance of calling the inherited concrete implementation..."
|
|
158
|
+
Benchmark.ips do |x|
|
|
159
|
+
x.config(warmup:, time:)
|
|
160
|
+
|
|
161
|
+
x.report("type_toolkit".rjust(width)) do |times|
|
|
162
|
+
i = 0
|
|
163
|
+
while (i += 1) < times
|
|
164
|
+
type_toolkit_object.m1
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
x.report("sorbet-runtime".rjust(width)) do |times|
|
|
169
|
+
i = 0
|
|
170
|
+
while (i += 1) < times
|
|
171
|
+
sorbet_runtime_object.m1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
x.report("manual delegation".rjust(width)) do |times|
|
|
176
|
+
i = 0
|
|
177
|
+
while (i += 1) < times
|
|
178
|
+
manual_delegation_object.m1
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
x.compare!
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
puts "\n\nTest the performance of calling an unimplemented abstract method..."
|
|
186
|
+
Benchmark.ips do |x|
|
|
187
|
+
x.config(warmup:, time:)
|
|
188
|
+
|
|
189
|
+
x.report("type_toolkit".rjust(width)) do |times|
|
|
190
|
+
i = 0
|
|
191
|
+
while (i += 1) < times
|
|
192
|
+
begin
|
|
193
|
+
type_toolkit_object.not_implemented
|
|
194
|
+
rescue AbstractMethodNotImplementedError # rubocop:disable Lint/SuppressedException
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
x.report("sorbet-runtime".rjust(width)) do |times|
|
|
200
|
+
i = 0
|
|
201
|
+
while (i += 1) < times
|
|
202
|
+
begin
|
|
203
|
+
sorbet_runtime_object.not_implemented
|
|
204
|
+
rescue NotImplementedError # rubocop:disable Lint/SuppressedException
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
x.report("manual delegation".rjust(width)) do |times|
|
|
210
|
+
i = 0
|
|
211
|
+
while (i += 1) < times
|
|
212
|
+
begin
|
|
213
|
+
manual_delegation_object.not_implemented
|
|
214
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
x.compare!
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# typed: ignore
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Benchmark the startup performance of declaring modules/interfaces in 3 different styles:
|
|
5
|
+
# - TypeToolkit (abstract gem)
|
|
6
|
+
# - Sorbet runtime
|
|
7
|
+
# - Manual delegation (defined?(super) pattern)
|
|
8
|
+
|
|
9
|
+
############################################# Results #############################################
|
|
10
|
+
#
|
|
11
|
+
# ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
|
|
12
|
+
#
|
|
13
|
+
# | | Interpreter | YJIT |
|
|
14
|
+
# |-------------------|-------------------------:|---------------------------:|
|
|
15
|
+
# | sorbet-runtime | (21.34x slower) 50.79 μs | (152.34x slower) 377.27 μs |
|
|
16
|
+
# | type_toolkit | (4.18x slower) 9.95 μs | (4.18x slower) 10.35 μs |
|
|
17
|
+
# | manual delegation | 2.38 μs | 2.48 μs |
|
|
18
|
+
#
|
|
19
|
+
####################################################################################################
|
|
20
|
+
|
|
21
|
+
require "bundler"
|
|
22
|
+
Bundler.require(:default, :benchmark)
|
|
23
|
+
|
|
24
|
+
require "type_toolkit"
|
|
25
|
+
|
|
26
|
+
warmup = 5
|
|
27
|
+
time = 10
|
|
28
|
+
|
|
29
|
+
width = ["type_toolkit", "sorbet-runtime", "manual delegation"].max_by(&:length).length
|
|
30
|
+
|
|
31
|
+
puts "Benchmark the time to declare an interface module with abstract methods..."
|
|
32
|
+
|
|
33
|
+
[:interpreter, :yjit].each do |mode|
|
|
34
|
+
if mode == :yjit
|
|
35
|
+
puts <<~MSG
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
================================================================================
|
|
39
|
+
Enabling YJIT...
|
|
40
|
+
================================================================================
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MSG
|
|
44
|
+
RubyVM::YJIT.enable
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Benchmark.ips do |x|
|
|
48
|
+
x.config(warmup:, time:)
|
|
49
|
+
|
|
50
|
+
x.report("type_toolkit".rjust(width)) do |times|
|
|
51
|
+
i = 0
|
|
52
|
+
while (i += 1) < times
|
|
53
|
+
interface = Module.new do
|
|
54
|
+
interface!
|
|
55
|
+
|
|
56
|
+
abstract def m1; end
|
|
57
|
+
abstract def m2; end
|
|
58
|
+
abstract def m3; end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Class.new do
|
|
62
|
+
include interface
|
|
63
|
+
|
|
64
|
+
def m1 = "m1"
|
|
65
|
+
def m2 = "m2"
|
|
66
|
+
def m3 = "m3"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
x.report("sorbet-runtime".rjust(width)) do |times|
|
|
72
|
+
i = 0
|
|
73
|
+
while (i += 1) < times
|
|
74
|
+
interface = Module.new do
|
|
75
|
+
extend T::Sig
|
|
76
|
+
extend T::Helpers
|
|
77
|
+
|
|
78
|
+
interface!
|
|
79
|
+
|
|
80
|
+
sig { abstract.returns(String) }
|
|
81
|
+
def m1; end
|
|
82
|
+
|
|
83
|
+
sig { abstract.returns(String) }
|
|
84
|
+
def m2; end
|
|
85
|
+
|
|
86
|
+
sig { abstract.returns(String) }
|
|
87
|
+
def m3; end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Class.new do
|
|
91
|
+
extend T::Sig
|
|
92
|
+
|
|
93
|
+
include interface
|
|
94
|
+
|
|
95
|
+
sig { override.returns(String) }
|
|
96
|
+
def m1 = "m1"
|
|
97
|
+
|
|
98
|
+
sig { override.returns(String) }
|
|
99
|
+
def m2 = "m2"
|
|
100
|
+
|
|
101
|
+
sig { override.returns(String) }
|
|
102
|
+
def m3 = "m3"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
x.report("manual delegation".rjust(width)) do |times|
|
|
108
|
+
i = 0
|
|
109
|
+
while (i += 1) < times
|
|
110
|
+
interface = Module.new do
|
|
111
|
+
def m1 = defined?(super) ? super : raise
|
|
112
|
+
def m2 = defined?(super) ? super : raise
|
|
113
|
+
def m3 = defined?(super) ? super : raise
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Class.new do
|
|
117
|
+
include interface
|
|
118
|
+
|
|
119
|
+
def m1 = "m1"
|
|
120
|
+
def m2 = "m2"
|
|
121
|
+
def m3 = "m3"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
x.compare!
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Benchmark if it's worth checking `Module.include?` before calling`Module.include`
|
|
4
|
+
# ... spoiler: meh, not really.
|
|
5
|
+
|
|
6
|
+
############################################# Results #############################################
|
|
7
|
+
#
|
|
8
|
+
# ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
|
|
9
|
+
#
|
|
10
|
+
# | | Interpreter | YJIT |
|
|
11
|
+
# |-------------------:|------------------------:|------------------------:|
|
|
12
|
+
# | just include | (2.39x slower) 54.35 ns | (8.53x slower) 35.29 ns |
|
|
13
|
+
# | check then include | 22.72 ns | 4.14 ns |
|
|
14
|
+
#
|
|
15
|
+
####################################################################################################
|
|
16
|
+
|
|
17
|
+
require "bundler"
|
|
18
|
+
Bundler.require(:default, :benchmark)
|
|
19
|
+
|
|
20
|
+
module M1; end
|
|
21
|
+
module M2; end
|
|
22
|
+
class C1; end
|
|
23
|
+
class C2; end
|
|
24
|
+
|
|
25
|
+
[:interpreter, :yjit].each do |mode|
|
|
26
|
+
if mode == :yjit
|
|
27
|
+
puts <<~MSG
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
================================================================================
|
|
31
|
+
Enabling YJIT...
|
|
32
|
+
================================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
MSG
|
|
36
|
+
RubyVM::YJIT.enable
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
warmup = 5
|
|
40
|
+
time = 10
|
|
41
|
+
|
|
42
|
+
puts "Benchmark the performance of calling the concrete implementation directly..."
|
|
43
|
+
Benchmark.ips do |x|
|
|
44
|
+
x.config(warmup:, time:)
|
|
45
|
+
|
|
46
|
+
x.report("check then include") do |times|
|
|
47
|
+
i = 0
|
|
48
|
+
while (i += 1) < times
|
|
49
|
+
C1.include(M1) unless C1.include?(M1)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
x.report("just include") do |times|
|
|
54
|
+
i = 0
|
|
55
|
+
while (i += 1) < times
|
|
56
|
+
C2.include(M2)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
x.compare!
|
|
61
|
+
end
|
|
62
|
+
end
|
data/config/default.yml
CHANGED
|
@@ -2,3 +2,8 @@ TypeToolkit/DontExpectUnexpectedNil:
|
|
|
2
2
|
Description: "Detects misuse of UnexpectedNilError (rescuing, raising, or asserting it)."
|
|
3
3
|
Enabled: true
|
|
4
4
|
VersionAdded: "0.1.0"
|
|
5
|
+
|
|
6
|
+
TypeToolkit/PreferNotNil:
|
|
7
|
+
Description: "Replaces T.must assertions with Type Toolkit's not_nil! assertion."
|
|
8
|
+
Enabled: true
|
|
9
|
+
VersionAdded: "0.1.0"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The inherited implementation problem
|
|
2
|
+
|
|
3
|
+
Take this example:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Parent
|
|
7
|
+
def m = "Parent#m"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module I
|
|
11
|
+
interface!
|
|
12
|
+
|
|
13
|
+
abstract def m = raise "Abstract method `#m` not implemented"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Child < Parent
|
|
17
|
+
include I
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Child.new.m
|
|
21
|
+
# => Vanilla Ruby: raises (from `I#m`)
|
|
22
|
+
# => With this gem: `Parent#m`
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
There are two challenges here:
|
|
26
|
+
|
|
27
|
+
1. We need to not let the `I#m` stub implementation "get in the way", so that we can find real implementation that get inherited from further ancestors (like `Parent#m`).
|
|
28
|
+
2. *but* we still want something to raise an error if you attempt to call an unimplemented abstract method.
|
|
29
|
+
|
|
30
|
+
If you lookup `m` on an instance of `Child`, you would usually hit the empty stub `I#m` instead of the inherited implementation `Parent#m`:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Child.ancestors
|
|
34
|
+
|
|
35
|
+
# => [Child, Interface, Parent, Object, Kernel, BasicObject]
|
|
36
|
+
# ^ ^ ^
|
|
37
|
+
# | | Provides the implementation for Child to inherit
|
|
38
|
+
# | Its stub abstract method "gets in the way" and needs to be side-stepped
|
|
39
|
+
# We want Child to inherit the implementation from Parent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Child.instance_method(:m).owner
|
|
43
|
+
# => Vanilla Ruby: `I`
|
|
44
|
+
# => With this gem: `Parent`
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
# Solution
|
|
48
|
+
|
|
49
|
+
Solve the inheritance problem by just yoinking the abstract method stub out of the ancestor chain, by just using `remove_method()`.
|
|
50
|
+
|
|
51
|
+
Now if you send `#m` to a child, there's no `I#m` implementation to hit, so it just jumps over to `Parent#m`. This is a direct method call with absolutely no runtime overhead.
|
|
52
|
+
|
|
53
|
+
This introduces a new problem, that now calling unimplemented abstract methods just raises `NoMethodError`, as if the name never existed. For a better developer experience, we'd like to give a more helpful message.
|
|
54
|
+
|
|
55
|
+
To do this, we implement `#method_missing` and check if the missing method name is one of the abstract methods (based on a list we append to every time you call `abstract`). If it is, we can raise our nicer error, otherwise we just delegate up the rest of the `#method_missing` chain (ultimately triggering a `NoMethodError`, like normal). This part isn't strictly necessary, but it's a nice-to-have. We can make it configurable.
|
|
56
|
+
|
|
57
|
+
Syntax:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
module I
|
|
61
|
+
interface!
|
|
62
|
+
|
|
63
|
+
abstract def m; end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
That's it. That simple!
|
|
68
|
+
|
|
69
|
+
Pros:
|
|
70
|
+
- Good DX when calling an abstract method that you forgot to implement
|
|
71
|
+
- Really nice syntax
|
|
72
|
+
|
|
73
|
+
Cons:
|
|
74
|
+
- Some implementation complexity (but all tucked into this gem, with less than 130 lines of implementation code)
|
|
75
|
+
- Calls to implemented abstract methods are faster, at the expense of unimplemented ones
|
|
76
|
+
- 1.9x slower than the hand-written alternative
|
|
77
|
+
- 1.1x slower than sorbet-runtime alternative
|
|
78
|
+
- but that's totally acceptable, because this should never happen in a completed program. The performance of calls to actually implemented methods is what matters, since that's what the real code will be doing at a huge volume.
|
|
79
|
+
|
|
80
|
+
# Alternative 1: Hand-written delegation
|
|
81
|
+
|
|
82
|
+
You duplicate this delegation logic in every abstract method:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
module I
|
|
86
|
+
interface!
|
|
87
|
+
|
|
88
|
+
# @abstract
|
|
89
|
+
def m = defined?(super) ? super : raise("Abstract method `#m` not implemented")
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pros:
|
|
94
|
+
- There's no magic, no runtime needed.
|
|
95
|
+
|
|
96
|
+
Cons:
|
|
97
|
+
- Repetitive
|
|
98
|
+
- Can be forgotten
|
|
99
|
+
- There's more overhead on every method call (but only to abstract methods with an inherited implementation):
|
|
100
|
+
- Checking `defined?(super)`
|
|
101
|
+
- Making the `super` call
|
|
102
|
+
- It adds an extra frame to your backtrace which will be seen in debuggers and exception backtraces (unless you configure them to filter it out)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Alternative 2: Sorbet runtime's solution
|
|
106
|
+
|
|
107
|
+
Here's a Sorbet version of the example: [Sorbet.run](https://sorbet.run/#%23%20typed%3A%20true%0A%0Aclass%20Parent%0A%20%20extend%20T%3A%3ASig%0A%0A%20%20sig%20%7B%20returns%28String%29%20%7D%0A%20%20def%20m%20%3D%20%22Parent%23m%22%0Aend%0A%20%20%0Amodule%20I%0A%20%20extend%20T%3A%3ASig%0A%20%20extend%20T%3A%3AHelpers%0A%0A%20%20interface!%0A%0A%20%20sig%20%7B%20abstract.returns%28String%29%20%7D%0A%20%20def%20m%3B%20end%0Aend%0A%0Aclass%20Child%20%3C%20Parent%3B%20end).
|
|
108
|
+
|
|
109
|
+
Sorbet runtime basically automates the hand-written solution above, by wrapping abstract methods:
|
|
110
|
+
|
|
111
|
+
https://github.com/sorbet/sorbet/blob/703498a0dcddbe7ec4b87ec6cc5d7d55cfa9b270/gems/sorbet-runtime/lib/types/private/methods/call_validation.rb#L48-L68
|
|
112
|
+
|
|
113
|
+
Pros:
|
|
114
|
+
- "just works"
|
|
115
|
+
|
|
116
|
+
Cons:
|
|
117
|
+
- All the downsides of doing this the hand-written way.
|
|
118
|
+
- To determine if a method is abstract or not, every `sig` needs to have its block evaluated, to see if it calls `abstract`.
|
|
119
|
+
- This used to be slower because it was defined via `defined_methods` with a block body. This produces a slower kind of method (`VM_METHOD_TYPE_BMETHOD`), than the equivalent code via `def` (`VM_METHOD_TYPE_ISEQ`). This was fixed in [this PR](https://github.com/sorbet/sorbet/pull/8238), which switch to using `module_eval` to define the method via `def`.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# The `def self.` problem
|
|
2
|
+
|
|
3
|
+
Ruby method definitions evaluate to the name of the method that was defined. This fact is used by the `public`/`protected`/`private` methods:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class
|
|
7
|
+
puts def demo; end
|
|
8
|
+
# Equivalent to `puts(:demo)`
|
|
9
|
+
end
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
However, there's no way to distinguish whether the method was an instance method, or a singleton method:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class C
|
|
16
|
+
puts def demo; end # prints ":demo"
|
|
17
|
+
puts def self.demo; end # *also* prints ":demo"
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
# Solution
|
|
22
|
+
|
|
23
|
+
Track into whether the last method call was an instance method or a singleton method, by hooking into `method_added` and `singleton_method_added`. See the `MethodDefRecorder` for details.
|
|
24
|
+
|
|
25
|
+
This way, both of these "just work"
|
|
26
|
+
|
|
27
|
+
```rb
|
|
28
|
+
class C
|
|
29
|
+
# Correctly defines an abstract instance method
|
|
30
|
+
abstract def demo; end
|
|
31
|
+
|
|
32
|
+
# Correctly defines an abstract "class method"
|
|
33
|
+
abstract def self.demo; end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
# Alternative 1: Do nothing
|
|
38
|
+
|
|
39
|
+
This is already a problem for access level modifiers, which don't do anything to handle it:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class C
|
|
43
|
+
private def self.demo; end
|
|
44
|
+
# => ❌ undefined method 'demo' for class 'C' (NameError)
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
We can just follow suit. However, there's a risk that if an instance method called `demo` _actually_ existed, we inadvertently make it abstract without intending. Again, the access level modifier methods already have this issue, but it's a sharp edge that we don't need to have.
|
|
49
|
+
|
|
50
|
+
If we choose to do nothing, we could encourage users to enable the [`Style/ClassMethodsDefinitions`](https://docs.rubocop.org/rubocop/cops_style.html#styleclassmethodsdefinitions) on the `EnforcedStyle: self_class` mode, so that their code bases don't contain `def self.foo` methods at all.
|
|
51
|
+
|
|
52
|
+
Pros:
|
|
53
|
+
- Simpler implementation (none!)
|
|
54
|
+
|
|
55
|
+
Cons:
|
|
56
|
+
- Sharp edge
|
|
57
|
+
- More complex mental model for users of RBS
|
|
58
|
+
|
|
59
|
+
# Alternative 2: separate macro
|
|
60
|
+
|
|
61
|
+
E.g.
|
|
62
|
+
|
|
63
|
+
```rb
|
|
64
|
+
class C
|
|
65
|
+
# Correctly defines an abstract instance method
|
|
66
|
+
abstract_instance_method def demo; end
|
|
67
|
+
|
|
68
|
+
# Correctly defines an abstract "class method"
|
|
69
|
+
abstract_class_method def self.demo; end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Pros:
|
|
74
|
+
- Simple implementation
|
|
75
|
+
|
|
76
|
+
Cons:
|
|
77
|
+
- Correct usage can't be enforced at runtime.
|
|
78
|
+
- Rubocop cop could enforce it, but that would need to be written
|
|
79
|
+
- Still a sharp edge, has the same cognitive complexity as alternative 1.
|
|
@@ -13,7 +13,7 @@ module RuboCop
|
|
|
13
13
|
name: "rubocop-type_toolkit",
|
|
14
14
|
version: ::TypeToolkit::VERSION,
|
|
15
15
|
homepage: "https://github.com/Shopify/type_toolkit",
|
|
16
|
-
description: "
|
|
16
|
+
description: "RuboCop rules for Type Toolkit.",
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
19
|
|