Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created June 24, 2021 11:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/1fef3b871551e57486f3c0a08ce8bab3 to your computer and use it in GitHub Desktop.
Save JoshCheek/1fef3b871551e57486f3c0a08ce8bab3 to your computer and use it in GitHub Desktop.
Example of how modules could keep track of what code to run on instances and what to run on classes and then generally work correctly whether extended or included
# An example to help me explain my thoughts in this tittwer convo:
# https://twitter.com/dorianmariefr/status/1407972746093641732
#
# Note that I ran this code on MRI 3.0.1, I assume it works on other versions too, but I did not try it.
# -----------------------------------------------------------------
# For simplicity, we'll only deal with keyword args in this example,
# I initially wrote it to deal with ordinals and blocks, but that got pointlessly dense
# An alternative implementation of `include` which allows args to be passed to the module
class Module
def include2(mod, **kws)
mod.send :append_features, self
mod.send :init_class, self, **kws
self
end
end
# An alternative implementation of `extend`, which allows args to be passed to the module
module Kernel
def extend2(mod, **kws)
singleton_class.include2 mod, **kws
mod.send :init_instance, self, **kws
self
end
end
# An alternative implementation of `Module`, which keeps track of how to initialize
# the class, and the instance, and does each at the appropriate time.
class Module2 < Module
def initialize(&block)
module_eval &block if block
end
# call this to define how the class should be initialized
def on_class(&block)
@on_class = block
end
# call this to define how the instance should be initialized
def on_instance(&on_instance)
@on_instance, mod = on_instance, self
define_method :initialize do |**kws|
block_kws, super_kws = mod.partition_kws **kws, &on_instance
instance_exec **block_kws, &on_instance
super **super_kws
end
end
# call this with a class to apply it to
def init_class(klass, **kws)
return unless @on_class
relevant_kws, _ = partition_kws(**kws, &@on_class)
klass.class_exec **relevant_kws, &@on_class
end
# call this with an instance to apply it to
def init_instance(obj, **kws)
return unless @on_instance
relevant_kws, _ = partition_kws(**kws, &@on_instance)
obj.instance_exec **relevant_kws, &@on_instance
end
# just a helper method
def partition_kws(**kws, &block)
kw_names = block.parameters.map(&:last)
kws.partition { |k, _| kw_names.include? k }.map(&:to_h)
end
end
# An example of how to use it, inspired by ActiveRecord
DbPersisted = Module2.new do
on_class do |table_name:|
define_singleton_method(:table_name) { table_name }
attr_accessor :id
end
on_instance do |id: nil|
self.id = id
end
def load_from_db
"select * from #{singleton_class.table_name} where id = #{id}"
end
end
# Here we include it into a class, so it uses the `on_class` definition above,
# and the `on_instance` method is handled by an `initialize` method defined by the module
class Customer
include2 DbPersisted, table_name: 'users'
attr_reader :name
def initialize(name:, **rest)
@name = name
super **rest
end
end
josh = Customer.new(id: 5, name: 'Josh')
josh.id # => 5
josh.name # => "Josh"
josh.load_from_db # => "select * from users where id = 5"
# Here we extend it onto an object, so it evaluates the `on_instance` code above
# on this object immediately, and evalutes the `on_class` block in the singleton class
Object.new.extend2(DbPersisted, table_name: 'customers', id: 123).load_from_db
# => "select * from customers where id = 123"
# `Object` was not affected by extending it like we did:
Object.ancestors # => [Object, PP::ObjectMixin, Kernel, BasicObject]
Object.new.load_from_db rescue $! # => #<NoMethodError: undefined method `load_from_db' for #<Object:0x0000000151120a50>>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment