Skip to content

Instantly share code, notes, and snippets.

@estum
Created November 2, 2020 12:06
Show Gist options
  • Save estum/11d22b5fa3ab8eead6a4ffcdd237f6bd to your computer and use it in GitHub Desktop.
Save estum/11d22b5fa3ab8eead6a4ffcdd237f6bd to your computer and use it in GitHub Desktop.
Yieldable v2
# frozen_string_literal: true
# = Yieldable
#
# This is meta-mixin, which makes your module, class or it's instance to respond
# to method +to_proc+, for example, to easily build instances during iteration:
#
# ["1", "2", "3"].map(&SomeObject)
#
# It is 2 different ways to use this module: +extend+ or +prepend+ it to your class.
#
# == <tt>extend Yieldable</tt>
#
# class SomeObject
# extend Yieldable # => singleton_class.to_proc = SomeObject.method(:call).to_proc
# end
#
# This way defines an attribute reader +to_proc+ in a singleton class.
# It sets a proc only once for a class, so it will not generate a new +Proc+ object
# every time, like if you would use just a <tt>method(:call).to_proc</tt>.
# By default it uses the +call+ class method as a source method for a proc. You
# can change the source method by using brackets:
#
# class SomeObject
# extend Yieldable[:new] # => singleton_class.to_proc = SomeObject.method(:new).to_proc
# end
#
# It's better to ensure that a source method is defined _before_ extending a class with +Yieldable+.
# In the other way it will add the +singletone_method_added+ hook and wait. After a source method
# will be added, the hook method will be removed.
#
# You can avoid to removing the hook with an optional <tt>once: false</tt> passed into the brackets:
#
# extend Yieldable[:new, once: false]
#
# == <tt>prepend Yieldable</tt>
#
# This way defines a +to_proc+ instance method once for a class, but a proc will be generated once for
# each object. You can also use a brackets like within an +extend+ way.
module Yieldable
# Creates configured mixin.
#
# @param method_name [Symbol] A source method's name
# @param once [Boolean] Will be a proc defined only once? (by default: +true+)
# @return [Module] the configured mixin
def self.[](method_name = :call, once: true)
Mixin.new(method_name, once: once)
end
class Mixin < Module
attr_reader :method_name
def initialize(method_name = :call, once: true)
@method_name = method_name
@once = once
@proc = nil
super()
end
def method_added(base, id)
set_proc(base)
cleanup(base) if @once # self-remove
end
def extend_object(base)
unless base.singleton_class.method_defined?(:to_proc)
base.singleton_class.attr_reader(:to_proc)
end
if base.singleton_class.method_defined?(@method_name)
set_proc(base)
end
if wait_method_definition?
mixin = self
base.define_method :singleton_method_added do |id|
mixin.method_added(self, id) if id == mixin.method_name
super(id)
end
end
super
end
def prepend_features(base)
base.class_eval <<~RUBY, __FILE__, __LINE__ + 1
def to_proc
@to_proc ||= method(:#{@method_name}).to_proc
end
RUBY
super
end
private
def wait_method_definition?
@proc.nil? || !@once
end
def set_proc(base)
@proc = find_method_on(base).to_proc
base.instance_variable_set(:@to_proc, @proc)
base
end
def cleanup(base)
base.singleton_class.remove_method(:singleton_method_added)
end
def find_method_on(base)
if base.respond_to?(@method_name)
base.method(@method_name)
else
base.singleton_method(@method_name)
end
end
end
CallMixin = Mixin.new(:call)
private_constant :Mixin
private_constant :CallMixin
class << self
private
def extend_object(base)
CallMixin.extend_object(base)
end
def prepend_features(base)
CallMixin.prepend_features(base)
end
end
end
@estum
Copy link
Author

estum commented Nov 2, 2020

Yieldable

This is meta-mixin, which makes your module, class or it's instance to respond
to method to_proc, for example, to easily build instances during iteration:

  ["1", "2", "3"].map(&SomeObject)

It is 2 different ways to use this module: extend or prepend it to your class.

extend Yieldable

  class SomeObject
    extend Yieldable # => singleton_class.to_proc = SomeObject.method(:call).to_proc
  end

This way defines an attribute reader to_proc in a singleton class.
It sets a proc only once for a class, so it will not generate a new Proc object
every time, like if you would use just a method(:call).to_proc.
By default it uses the call class method as a source method for a proc. You
can change the source method by using brackets:

  class SomeObject
    extend Yieldable[:new] # => singleton_class.to_proc = SomeObject.method(:new).to_proc
  end

It's better to ensure that a source method is defined before extending a class with +Yieldable+.
In the other way it will add the singletone_method_added hook and wait. After a source method
will be added, the hook method will be removed.

You can avoid to removing the hook with an optional once: false passed into the brackets:

  extend Yieldable[:new, once: false]

prepend Yieldable

This way defines a to_proc instance method once for a class, but a proc will be generated once for
each object. You can also use a brackets like within an +extend+ way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment