-
Notifications
You must be signed in to change notification settings - Fork 0
Implement abstract classes #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| # typed: ignore | ||
| # frozen_string_literal: true | ||
|
|
||
| # Benchmark the time it takes to instantiate a subclass of an abstract class | ||
|
|
||
| ############################################# Results ############################################# | ||
| # | ||
| # ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23] | ||
| # | ||
| # Time to instantiate a subclass of an abstract class | ||
| # | | Interpreter | YJIT | | ||
| # |-------------------|-------------------------:|-------------------------:| | ||
| # | sorbet-runtime | (2.29x slower) 109.28 ns | (2.49x slower) 90.09 ns | | ||
| # | type_toolkit | 47.80 ns | 36.23 ns | | ||
| # | ||
| # Time to instantiate a subclass of an abstract class with a custom implementation of `new` | ||
| # | | Interpreter | YJIT | | ||
| # |-------------------|-------------------------:|-------------------------:| | ||
| # | sorbet-runtime | (1.10x slower) 132.95 ns | (1.32x slower) 104.45 ns | | ||
| # | type_toolkit | 121.12 ns | 79.33 ns | | ||
| # | ||
| #################################################################################################### | ||
|
|
||
| require "bundler" | ||
| Bundler.require(:default, :benchmark) | ||
|
|
||
| require "type_toolkit" | ||
|
|
||
| # This benchmark has pretty high variance (it depends on the GC's allocation patterns), | ||
| # so we run it for a longer time to get a more stable result. | ||
| warmup = 10 | ||
| time = 30 | ||
|
|
||
| width = ["type_toolkit", "sorbet-runtime", "manual delegation"].max_by(&:length).length | ||
|
|
||
| # module PreventConflictingAbstractPatch | ||
| # def abstract! | ||
| # # Prevent Sorbet's definition of `abstract!` from calling the TypeToolkit implementation of `abstract!` | ||
| # return if singleton_class.include?(T::Helpers) | ||
|
|
||
| # super | ||
| # end | ||
| # end | ||
|
|
||
| # Class.prepend(PreventConflictingAbstractPatch) | ||
|
|
||
| module TypeKitDemo | ||
| class Parent | ||
| TypeToolkit.make_abstract!(self) | ||
| end | ||
|
|
||
| class Child < Parent; end | ||
|
|
||
| class Child_OverridesNew < Parent | ||
| def self.new(...) = super | ||
| end | ||
| end | ||
|
|
||
| module SorbetRuntimeDemo | ||
| class Parent | ||
| extend T::Helpers | ||
|
|
||
| # binding.irb | ||
| abstract! | ||
| end | ||
|
|
||
| class Child < Parent; end | ||
|
|
||
| class Child_OverridesNew < Parent | ||
| def self.new(...) = super | ||
| end | ||
| end | ||
|
|
||
| # Run GC before each job run. | ||
| # | ||
| # Inspired by https://www.omniref.com/ruby/2.2.1/symbols/Benchmark/bm?#annotation=4095926&line=182 | ||
| class GCSuite | ||
| def warming(*) | ||
| GC.start | ||
| end | ||
|
|
||
| def running(*) | ||
| GC.start | ||
| end | ||
|
|
||
| def warmup_stats(*) | ||
| end | ||
|
|
||
| def add_report(*) | ||
| end | ||
| end | ||
|
|
||
| suite = GCSuite.new | ||
|
|
||
| [:interpreter, :yjit].each do |mode| | ||
| if mode == :yjit | ||
| puts <<~MSG | ||
|
|
||
|
|
||
| ================================================================================ | ||
| Enabling YJIT... | ||
| ================================================================================ | ||
|
|
||
|
|
||
| MSG | ||
| RubyVM::YJIT.enable | ||
| end | ||
|
|
||
| puts "\nBenchmark the time to instantiate a subclass of an abstract class..." | ||
| Benchmark.ips do |x| | ||
| x.config(warmup:, time:) | ||
|
|
||
| x.report("type_toolkit".rjust(width)) do |times| | ||
| i = 0 | ||
| while (i += 1) < times | ||
| TypeKitDemo::Child.new | ||
| end | ||
| end | ||
|
|
||
| x.report("sorbet-runtime".rjust(width)) do |times| | ||
| i = 0 | ||
| while (i += 1) < times | ||
| SorbetRuntimeDemo::Child.new | ||
| end | ||
| end | ||
|
|
||
| x.compare! | ||
| end | ||
|
|
||
| puts "\nBenchmark the time to instantiate a subclass of an abstract class with a custom implementation of `new`..." | ||
| Benchmark.ips do |x| | ||
| x.config(warmup:, time:, suite:) | ||
|
|
||
| x.report("type_toolkit".rjust(width)) do |times| | ||
| i = 0 | ||
| while (i += 1) < times | ||
| TypeKitDemo::Child_OverridesNew.new | ||
| end | ||
| end | ||
|
|
||
| x.report("sorbet-runtime".rjust(width)) do |times| | ||
| i = 0 | ||
| while (i += 1) < times | ||
| SorbetRuntimeDemo::Child_OverridesNew.new | ||
| end | ||
| end | ||
|
|
||
| x.compare! | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,92 @@ | ||||||
| # typed: true | ||||||
| # frozen_string_literal: true | ||||||
|
|
||||||
| module TypeToolkit | ||||||
| class << self | ||||||
| #: (Module[top]) -> void | ||||||
| def make_abstract!(mod) | ||||||
| case mod | ||||||
| when Class | ||||||
| # We need to save the original implementation of `new`, so we can restore it on the subclasses later. | ||||||
| mod.singleton_class.alias_method(:__original_new_impl, :new) | ||||||
|
|
||||||
| mod.extend(TypeToolkit::AbstractClass) | ||||||
| mod.extend(TypeToolkit::DSL) | ||||||
| mod.extend(TypeToolkit::MethodDefRecorder) | ||||||
| mod.extend(TypeToolkit::HasAbstractMethods) | ||||||
|
|
||||||
| mod.include(TypeToolkit::AbstractInstanceMethodReceiver) | ||||||
| when Module | ||||||
| raise NotImplementedError, "Abstract modules are not implemented yet." | ||||||
| end | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| # This module is extended onto every class marked `abstract!`. | ||||||
| # Abstract classes can't be instantiated, only subclassed. | ||||||
| # They should contain abstract methods, which must be implemented by subclasses. | ||||||
| # | ||||||
| # Example: | ||||||
| # | ||||||
| # class Widget | ||||||
| # abstract! | ||||||
| # | ||||||
| # #: -> void | ||||||
| # abstract def draw; end | ||||||
| # end | ||||||
| # | ||||||
| # class Button < Widget | ||||||
| # # @override | ||||||
| # #: -> void | ||||||
| # def draw | ||||||
| # ... | ||||||
| # end | ||||||
| # end | ||||||
| # | ||||||
| # class TextField < Widget | ||||||
| # # @override | ||||||
| # #: -> void | ||||||
| # def draw | ||||||
| # ... | ||||||
| # end | ||||||
| # end | ||||||
| # | ||||||
| module AbstractClass | ||||||
| # An override of `new` which prevents instantiation of the class. | ||||||
| # This needs to be overridden again in subclasses, to restore the real `.new` implementation. | ||||||
| def new(...) # :nodoc: | ||||||
| #: self as Class[top] | ||||||
|
|
||||||
| if respond_to?(:__original_new_impl) # This is true for the abstract classes themselves, and false for their subclasses. | ||||||
| raise CannotInstantiateAbstractClassError, "#{self.class.name} is declared as abstract; it cannot be instantiated" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's a bug here: I wonder if we should use
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True. I have to add tests for abstract class methods, and I'll verify their name is correct in the message. |
||||||
| end | ||||||
|
|
||||||
| # This is hit in the uncommon case where a subclass of an abstract class overrides `.new` and calls `super`. | ||||||
| super | ||||||
| end | ||||||
|
|
||||||
| # Restores the original `.new` implementation for the direct subclasses of an abstract class. | ||||||
| #: (Class[AbstractClass]) -> void | ||||||
| def inherited(subclass) # :nodoc: | ||||||
| superclass = subclass.singleton_class.superclass #: as !nil | ||||||
|
|
||||||
| if superclass.include?(TypeToolkit::AbstractClass) && | ||||||
| !superclass.singleton_class.include?(TypeToolkit::AbstractClass) | ||||||
| # We only need to restore the original `.new` implementation for the direct subclasses of the abstract class. | ||||||
| # That's then inherited by the indirect subclasses. | ||||||
|
|
||||||
| subclass.singleton_class.alias_method(:new, :__original_new_impl) | ||||||
|
|
||||||
| # We don't need a reference to the original implementation anymore, | ||||||
| # so let's undef it to limit namespace pollution. | ||||||
| subclass.singleton_class.undef_method(:__original_new_impl) | ||||||
| end | ||||||
|
|
||||||
| super | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| # Raised when an attempt is made to instantiate an abstract class. | ||||||
| class CannotInstantiateAbstractClassError < Exception # rubocop:disable Lint/InheritException | ||||||
| end | ||||||
| end | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class Class | ||
| def abstract! | ||
| TypeToolkit.make_abstract!(self) | ||
| end | ||
| end |
Uh oh!
There was an error while loading. Please reload this page.