Created
June 24, 2021 11:19
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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