Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ gem "minitest", "~> 5.16"
gem "rubocop-shopify", require: false
gem "rubocop-minitest", require: false
gem "rubocop-rake", require: false

group :benchmark do
gem "benchmark-ips"
gem "sorbet-runtime"
end
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ GEM
specs:
ast (2.4.3)
benchmark (0.5.0)
benchmark-ips (2.14.0)
date (3.5.1)
erb (6.0.1)
erubi (1.13.1)
Expand Down Expand Up @@ -123,19 +124,22 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
benchmark-ips
irb
minitest (~> 5.16)
rake (~> 13.0)
rubocop-minitest
rubocop-rake
rubocop-shopify
sorbet
sorbet-runtime
tapioca (>= 0.17)
type_toolkit!

CHECKSUMS
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
benchmark-ips (2.14.0) sha256=b72bc8a65d525d5906f8cd94270dccf73452ee3257a32b89fbd6684d3e8a9b1d
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,49 @@ last_delivery = user.not_nil!
.deliveries.last.not_nil!
```

### Interfaces

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.

The Type Toolkit provides runtime support for interfaces (marked with `interface!`) and abstract methods (marked with `abstract` before the `def` keyword).

Example:

```ruby
module Notifier
interface!

#: (String) -> void
abstract def send_notification(message); end
end

class SlackNotifier
include Notifier

# @override
#: (String) -> void
def send_notification(message)
puts "Posting to Slack API: #{message.inspect}"
end
end

SlackNotifier.new.send_notification("Hello, world!") # ✅
# => Posting to Slack API: "Hello, world!"
```

Unimplemented abstract methods cannot be called, and the Type Toolkit runtime will raise an error if you try to do so:

```ruby
class EmailNotifier
include Notifier

# Oops, forgot to implement `#send_notification`!
end

EmailNotifier.new.send_notification("Hello, world!") # ❌ TypeToolkit::AbstractMethodNotImplementedError
# => Abstract method #send_notification was never implemented.
```

## Guiding Principles

### Blazingly fast™
Expand Down
7 changes: 7 additions & 0 deletions benchmark/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
inherit_from: ../.rubocop.yml

Naming/ClassAndModuleCamelCase:
Enabled: false # Sometimes underscores are useful, m'kay?

Style/ClassMethodsDefinitions:
Enabled: false # We need to be able to compare `class << self` and `def self.`
221 changes: 221 additions & 0 deletions benchmark/abstract_methods_benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# typed: ignore
# frozen_string_literal: true

# Benchmark the performance overhead of calling:
# - A concrete implementation of an abstract method
# - An inherited concrete implementation of an abstract method
# - The error case of calling an unimplemented abstract method

############################################# Results #############################################
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will go stale

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, I'd rather have the results checked in here than pasted around some random Slack conversations or GitHub gists

#
# ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
#
# ## Interpreter
#
# | Call to... | Regular impl | Inherited impl | Missing impl |
# |-------------------|--------------------:|------------------------:|--------------------------:|
# | sorbet-runtime | (same-ish) 23.02 ns | (2.70x slower) 57.30 ns | (1.13x slower) 472.86 ns |
# | manual delegation | (same-ish) 22.18 ns | (2.07x slower) 44.90 ns | *415.36 ns* |
# | type_toolkit | (same-ish) 22.56 ns | *22.03 ns* | (2.11x slower) 890.38 ns |
#
# ## YJIT#
# | Call to... | Regular impl | Inherited impl | Missing impl |
# |-------------------|--------------------:|-------------------------:|--------------------------:|
# | sorbet-runtime | (same-ish) 1.63 ns | (21.41x slower) 34.91 ns | (1.10x slower) 447.59 ns |
# | manual delegation | (same-ish) 1.63 ns | (7.15x slower) 11.66 ns | *405.84 ns* |
# | type_toolkit | (same-ish) 1.67 ns | *1.63 ns* | (1.91x slower) 774.91 ns |
#
####################################################################################################

require "bundler"
Bundler.require(:default, :benchmark)

require "type_toolkit"

module TypeKitDemo
# Provides the concrete implementation of `m`
class Parent
def m1 = "Parent#m1"
end

module I
interface!

abstract def m1; end
abstract def m2; end
abstract def not_implemented; end
end

# Inherits the concrete implementation of `m` from DemoParentClass.
class Child < Parent
include I

def m2 = "Child#m2"
end
end

module SorbetRuntimeDemo
# Provides the concrete implementation of `m`
class Parent
def m1 = "Parent#m1"
end

module I
extend T::Sig
extend T::Helpers

interface!

sig { abstract.returns(String) }
def m1; end

sig { abstract.returns(String) }
def m2; end

sig { abstract.returns(String) }
def not_implemented; end
end

# Inherits the concrete implementation of `m` from DemoParentClass.
class Child < Parent
include I

def m2 = "Child#m2"
end
end

module ManualDelegationDemo
class Parent
def m1 = "Parent#m1"
end

module I
def m1 = defined?(super) ? super : raise
def m2 = defined?(super) ? super : raise
def not_implemented = defined?(super) ? super : raise
end

# Inherits the concrete implementation of `m` from DemoParentClass.
class Child < Parent
include I

def m2 = "Child#m2"
end
end

type_toolkit_object = TypeKitDemo::Child.new
manual_delegation_object = ManualDelegationDemo::Child.new
sorbet_runtime_object = SorbetRuntimeDemo::Child.new

[:interpreter, :yjit].each do |mode|
if mode == :yjit
puts <<~MSG


================================================================================
Enabling YJIT...
================================================================================


MSG
RubyVM::YJIT.enable
end

warmup = 5
time = 10

width = ["type_toolkit", "sorbet-runtime", "manual delegation"].max_by(&:length).length

puts "Benchmark the performance of calling the concrete implementation directly..."
Benchmark.ips do |x|
x.config(warmup:, time:)

x.report("type_toolkit".rjust(width)) do |times|
i = 0
while (i += 1) < times
type_toolkit_object.m2
end
end

x.report("sorbet-runtime".rjust(width)) do |times|
i = 0
while (i += 1) < times
sorbet_runtime_object.m2
end
end

x.report("manual delegation".rjust(width)) do |times|
i = 0
while (i += 1) < times
manual_delegation_object.m2
end
end

x.compare!
end

puts "\n\nBenchmark the performance of calling the inherited concrete implementation..."
Benchmark.ips do |x|
x.config(warmup:, time:)

x.report("type_toolkit".rjust(width)) do |times|
i = 0
while (i += 1) < times
type_toolkit_object.m1
end
end

x.report("sorbet-runtime".rjust(width)) do |times|
i = 0
while (i += 1) < times
sorbet_runtime_object.m1
end
end

x.report("manual delegation".rjust(width)) do |times|
i = 0
while (i += 1) < times
manual_delegation_object.m1
end
end

x.compare!
end

puts "\n\nTest the performance of calling an unimplemented abstract method..."
Benchmark.ips do |x|
x.config(warmup:, time:)

x.report("type_toolkit".rjust(width)) do |times|
i = 0
while (i += 1) < times
begin
type_toolkit_object.not_implemented
rescue AbstractMethodNotImplementedError # rubocop:disable Lint/SuppressedException
end
end
end

x.report("sorbet-runtime".rjust(width)) do |times|
i = 0
while (i += 1) < times
begin
sorbet_runtime_object.not_implemented
rescue NotImplementedError # rubocop:disable Lint/SuppressedException
end
end
end

x.report("manual delegation".rjust(width)) do |times|
i = 0
while (i += 1) < times
begin
manual_delegation_object.not_implemented
rescue StandardError # rubocop:disable Lint/SuppressedException
end
end
end

x.compare!
end
end
Loading
Loading