'Create dynamically methods in a module for each constant value defined within the module
I am trying to find out a way to generate dynamically methods in module (SomeConstants) based on constants defined within this module.
what I think I would like to achieve is the situation in which I would be able to include (mixin) SomeConstants to other classes/modules so they are aware of methods returning constant values as well as being able to extend object so the object itself has an access to constants through methods
module SomeConstants
A = 'a'
B = 'b'
constants.each do |const|
define_method(const.downcase.to_sym, lambda {SomeConstants.const_get(const)})
end
C = 'c'
end
class SomeClass
include SomeConstants
end
s = SomeClass.new
p s.a
p s.b
p s.class.ancestors
p s.singleton_class.ancestors
p s.singleton_methods
o = Object.new
o.extend(SomeConstants)
p o.a
p o.b
p o.class.ancestors
p o.singleton_class.ancestors
p o.singleton_methods
Output:
"b"
[SomeClass, SomeConstants, Object, Kernel, BasicObject]
[#<Class:#<SomeClass:0x00559069504fe8>>, SomeClass, SomeConstants, Object, Kernel, BasicObject]
[]
"a"
"b"
[Object, Kernel, BasicObject]
[#<Class:#<Object:0x00559069504660>>, SomeConstants, Object, Kernel, BasicObject]
[:a, :b]
The above one seem to work fine, apart from the fact that all constants must be defined before creating methods due to the way how source code is processed.
My second approach was to use included/extended hooks, though I am not sure if this doesn't smell badly.. see below
module SomeConstants
A = 'a'
B = 'b'
def self.included(base)
constants.each do |const|
base.send(:define_method, const.downcase.to_sym, lambda { SomeConstants.const_get(const) })
end
end
def self.extended(base)
constants.each do |const|
base.send(:define_singleton_method, const.downcase.to_sym, lambda { SomeConstants.const_get(const) })
end
end
C = 'c'
end
class SomeClass
include SomeConstants
end
s = SomeClass.new
p s.a
p s.b
p s.c
p s.class.ancestors
p s.singleton_class.ancestors
p s.singleton_methods
o = Object.new
o.extend(SomeConstants)
p o.a
p o.b
p o.c
p o.class.ancestors
p o.singleton_class.ancestors
p o.singleton_methods
class SomeOtherClass
extend SomeConstants
end
s = SomeOtherClass
p s.a
p s.b
p s.c
p s.class.ancestors
p s.singleton_class.ancestors
p s.singleton_methods
Output:
"a"
"b"
"c"
[SomeClass, SomeConstants, Object, Kernel, BasicObject]
[#<Class:#<SomeClass:0x005580a51562a8>>, SomeClass, SomeConstants, Object, Kernel, BasicObject]
[]
"a"
"b"
"c"
[Object, Kernel, BasicObject]
[#<Class:#<Object:0x005580a5155448>>, SomeConstants, Object, Kernel, BasicObject]
[:a, :b, :c]
"a"
"b"
"c"
[Class, Module, Object, Kernel, BasicObject]
[#<Class:SomeOtherClass>, SomeConstants, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
[:a, :b, :c]
Are there some better approaches? Is above one asking for troubles?
Is it a common requirement that one wants to be able to extend an instance of an object
o = Object.new
o.extend SomeModule
and at the same time be able to include (mixin) such module
class A
include SomeModule
end
Can someone put some light on these?
Solution 1:[1]
I don’t know if it’s asking for trouble so much as that it might get confusing for users of an API if constants are being used not only as unique references to values in syntax, but also as config to specify mixin methods.
A lot of Ruby libraries and frameworks probably wouldn’t use constants like that, but instead, would treat them as the output of a generator.
A very common thing to see is block configuration DSL patterns used to define identifiers and values used to generate the dynamic methods:
ASCII = LookupTable.define do
uppercase_letter_a 'A'
lowercase_letter_a 'a'
end
CP_437 = LookupTable.define do
light_shade '?'
medium_shade '?'
dark_shade '?'
end
p ASCII::UPPERCASE_LETTER_A
p ASCII::LOWERCASE_LETTER_A
obj = Object.new
obj.extend(ASCII::Methods)
cls = Class.new
cls.include(ASCII::Methods)
cls_obj = cls.new
p cls_obj.uppercase_letter_a == obj.uppercase_letter_a
p cls_obj.lowercase_letter_a == obj.lowercase_letter_a
class BoxDrawing
include CP_437::Methods
end
# etc...
In some places, you might also see a module const declared in a namespace as ClassMethods, with singleton defined methods rather than instance defined. But the pattern is very similar across a lot of libraries—how you set up the particular definition of modules vs classes largely depends on the calling conventions/style you want for the generated code.
Whether consumers of a DSL or library embed the dynamically created modules in object instances or declare classes in whichever way is mostly a style question. If you want your code to be called in a particular way by consumers, be sure to design the API and document the examples so that it’s easy for people to follow your intended setup.
Implementation for the above code looks like:
class LookupTable
class << self
def define(&block)
@table = Class.new
@table.const_set(:Methods, Module.new)
instance_eval(&block)
@table
end
def method_missing(identifier, value)
@table.const_set(identifier.upcase.to_s, value)
@table.const_get(:Methods).define_method(identifier, lambda { value })
end
end
end
There’s no reason why you couldn’t do the opposite of this and use declared consts as the input to a module generator. It’s not as common to see as the DSL/instance_eval patterns.
If you do want to go ahead with what you describe above, I would still recommend designing it in a way that the instance methods are separated into a different scope/attached to a different module than the consts so that you create a clear distinction in the API between what is expected to be embedded/included and what is expected to be referenced directly. Hence, I’d be leaning towards your first approach rather than the second (the second is very good for adaptive plugin APIs—take a look at the Sequel and Roda Gems for more inspiration in this direction).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
