Skip to content

Implement interfaces#2

Open
amomchilov wants to merge 4 commits intomainfrom
Alex/interfaces
Open

Implement interfaces#2
amomchilov wants to merge 4 commits intomainfrom
Alex/interfaces

Conversation

@amomchilov
Copy link
Contributor

@amomchilov amomchilov commented Feb 19, 2026

Interfaces are modules that define abstract methods, which must be implemented by the classes that implement the interface.

Example usage:

module Notifier
  interface!
  
  #: (String) -> void
  abstract def send_notification(message); end
end

class SlackNotifier
  include Notifier
  
  #: (String) -> void
  def send_notification(message)
    # Post to Slack API
  end
end

class EmailNotifier
  include Notifier
  
  #: (String) -> void
  def send_notification(message)
    # Send an email
  end
end

@amomchilov
Copy link
Contributor Author

amomchilov commented Feb 19, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

This was referenced Feb 19, 2026
@amomchilov amomchilov changed the title Implement interface! Implement interfaces Feb 19, 2026
@amomchilov amomchilov changed the base branch from Alex/gem-infra to graphite-base/2 February 19, 2026 22:31
@amomchilov amomchilov force-pushed the Alex/interfaces branch 2 times, most recently from cf42ec7 to 998bf60 Compare February 19, 2026 23:25
@amomchilov amomchilov changed the base branch from graphite-base/2 to Alex/gem-infra February 20, 2026 00:32
Comment on lines +46 to +57
x.report("check then include") do |times|
i = 0
while (i += 1) < times
C1.include(M1) unless C1.include?(M1)
end
end

x.report("just include") do |times|
i = 0
while (i += 1) < times
C2.include(M2)
end

Choose a reason for hiding this comment

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

Is this a fair comparison when the check include will only include once across the whole run? Wouldn't it be better to create a class, include it then re-include it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, this was me toying with making it so calling abstract lazily does the correct module inclusions in the right places. You probably have multiple abstract methods per class, so I was just curious how repeated attempts to reinclude the same module would work.

Comment on lines +166 to +169
mod.extend(TypeToolkit::Interface)
mod.extend(TypeToolkit::DSL)
mod.extend(TypeToolkit::MethodDefRecorder)
mod.extend(TypeToolkit::HasAbstractMethods)
Copy link
Contributor Author

@amomchilov amomchilov Feb 20, 2026

Choose a reason for hiding this comment

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

TODO: follow-up on @vinistock's suggestion to merge these modules into one.

I only had these split because I was moving around which pieces should go where, and that just made it easier during development.

However, this raises a new question. Some people might not want the method_missing? behaviour, and are okay with just hitting NoMethodError instead. In that case, what's the best way to make this behaviour configurable?

#!/usr/bin/ruby

$config = {
  nicer_error_messages_for_abstract_methods: true
}

class YourClass; end

# Approach 1: Conditionally define method
module TypeToolKit
  # Some method needed under all configurations
  def f1; end
  def f2; end
  def f3; end
  
  if $config[:nicer_error_messages_for_abstract_methods]
    def method_missing
      # magic to raise a nicer message for abstract methods
    end
  end
end

YourClass.include(TypeToolKit)

# Approach 2: Unconditionally define method on an optional module
module TypeToolKit
  # Some method needed under all configurations
  def f1; end
  def f2; end
  def f3; end
end

module TypeToolKit::NicerErrorMessages
  def method_missing
    # magic to raise a nicer message for abstract methods
  end
end

YourClass.include(TypeToolKit)
YourClass.include(TypeToolKit::NicerErrorMessages) if $config[:nicer_error_messages_for_abstract_methods]

Copy link
Contributor Author

@amomchilov amomchilov Feb 24, 2026

Choose a reason for hiding this comment

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

Vini and I paired on this, and these modules can't me combined as much as I thought.

  1. TypeToolkit::Interface needs to standalone, because its purpose is to define an included hook that only applies to interfaces, and not any classes that use interfaces
  2. DSL and MethodDefRecorder are the only pair that could be combined, but I found it useful to be able to have MethodDefRecorder isolated for testing
  3. HasAbstractMethods needs to appear on both an interface and implementers of the interface

This separation does also happen to help with making the method_missing behaviour optional in the future, if we ever want to do that.

@amomchilov amomchilov force-pushed the Alex/interfaces branch 4 times, most recently from bbd649e to 2a1f136 Compare February 23, 2026 23:18
@amomchilov amomchilov force-pushed the Alex/gem-infra branch 2 times, most recently from 276e63a to 738e534 Compare February 23, 2026 23:20
@amomchilov amomchilov changed the base branch from Alex/gem-infra to graphite-base/2 February 23, 2026 23:29
@amomchilov amomchilov changed the base branch from graphite-base/2 to main February 23, 2026 23:53
@amomchilov amomchilov changed the base branch from main to graphite-base/2 February 24, 2026 23:32
@amomchilov amomchilov changed the base branch from graphite-base/2 to Alex/initial-readme February 24, 2026 23:32
@amomchilov amomchilov mentioned this pull request Feb 24, 2026
@amomchilov amomchilov marked this pull request as ready for review February 25, 2026 14:40
@amomchilov amomchilov requested a review from a team February 25, 2026 15:07
# an unimplemented abstract method would usually raise a `NoMethodError`. This module uses `method_missing` to
# to raise `AbstractMethodNotImplementedError` instead.
# @requires_ancestor: Kernel
module AbstractClassMethodReceiver
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Contributor Author

@amomchilov amomchilov Feb 25, 2026

Choose a reason for hiding this comment

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

Abstract class methods in the up-stack PR

... which I realize aren't tested.

c = self.class #: as Class[top] & HasAbstractMethods

if c.abstract_method_declared?(method_name)
raise AbstractMethodNotImplementedError, "Abstract method ##{method_name.inspect} was never implemented."
Copy link
Contributor

Choose a reason for hiding this comment

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

The instance method error uses method_name.inspect here, but the class method version (line 23) doesn't. With .inspect, Symbol output includes a : prefix — so you'd get Abstract method #:send_notification was never implemented. which looks odd. Should we be consistent and drop the .inspect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good eye, I intended to drop the .inspect.

Should test this message, too.

# - 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

# frozen_string_literal: true

class Module
def interface!
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be under DSL?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah okay I see how DSL gets appended later. Unfortunate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I guess there's two parts to the DSL. I don't want to pollute your class with the .abstract macro if you don't opt-in with abstract! or interface!

Base automatically changed from Alex/initial-readme to main February 25, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants