With most of my code becoming more and more stateless and functional, I haven’t given up leveraging Ruby’s excellent way of defining declarative APIs and DSL. Ruby is just too good at that.
However, I am missing a crucial tool in Ruby: being able to pass dynamic arguments to the inheritance, exactly the way I can pass arguments to the object constructor. In short, here’s what I would love to have.
class Memo::Render < Render, engine: Render::JSON property :title property :body end
The Render
class could then use those additional arguments in its inherited
hook, acting as an initialize
on the class level.
class Render def self.inherited(subclass, options) super subclass.engine = options[:engine] end # def self.property(name); end # def self.engine=(name); end end
This would also work for anonymous classes, of course.
Memo::Render = Class.new( Render, engine: Render::JSON)
Looks very Ruby-esque, doesn’t it?
The Problem with inherited
Currently, you need to set class variables in the inherited class to configure it.
class Memo::Render < Render def self.engine Render::JSON end end
The massive drawback here (a design flaw in Ruby?) is that this override is evaluated too late, after the inherited
hook is called. Check out the exemplary inherited
method in the superclass Render
and how it behaves.
class Render def self.engine; end def self.inherited(subclass) puts subclass #=> Memo::Render puts subclass.engine #=> nil !!! end end
As you can see, even though Memo::Render
implements engine
, it isn’t evaluated at inherited
time, making this hook more or less useless if you want to cleanly initialize and setup a subclass at compile-time.
One solution could be the proposed extension of the hook, however, the complexity of this change is completely unknown to me due to my lack of Ruby core participation.
May you use a constant from subclass instead of calling its method?
LikeLike
Cool idea, but I guess this will be the same problem: The class body is evaluated _after_ its inherited hook was called. Have you tested it?
LikeLike
Looks like yes, it has the same problem:
$ irb
irb(main):001:0> class A
irb(main):002:1> def self.inherited(cls)
irb(main):003:2> puts cls::CONSTANT
irb(main):004:2> end
irb(main):005:1> end
=> :inherited
irb(main):006:0>
irb(main):007:0* class B CONSTANT = “Here’s the constant”
irb(main):009:1> end
NameError: uninitialized constant B::CONSTANT
from (irb):3:in `inherited’
from (irb):7
from .rbenv/versions/2.4.1/bin/irb:11:in `’
LikeLike
Ruby simply evaluates the text file, and A<B will literally trigger the inherited hook.
LikeLike
I think this might do what you want: https://gist.github.com/bkudria/82e3721d4ec2b62a2a3d95050e964d84
LikeLike
Hey Ben, thanks for your snippet.
The “problem” here is that it basically does the same we do here: https://apotonick.wordpress.com/2018/01/18/dear-ruby-2-the-functional-inherited-hook/ in the “Inherit-then-tune” section. As described, I don’t want the third anonymous class in the middle, which is unavoidable in your solution. Thanks anyway, and also thanks for `define_singleton_method`, new one for me! :beers:
LikeLike
I’d like to translate the articles https://apotonick.wordpress.com/2018/01/17/dear-ruby-1-what-about-arguments-when-inheriting/ and https://apotonick.wordpress.com/2018/01/18/dear-ruby-2-the-functional-inherited-hook/ into Japanese and publish on our tech blog https://techracho.bpsinc.jp/ for sharing it. Is it OK for you?
I make sure to indicate the link to original, title, author name in the case.
Best regards,
LikeLike
I would be honored! Arrigato!!!
LikeLike
> class Memo::Render < Render, engine: Render::JSON
The engine is a customization of the the subclass. Why pass it the the superclass so that it can pass back to the subclass
LikeLike
Please ignore the previous accidental submission.
> class Memo::Render < Render, engine: Render::JSON
The engine is a customization of the the subclass. Why pass the engine to the superclass so that it can pass it back to the subclass, when you can set it on the subclass directly:
class Memo::Render < Render
@engine = Render::JSON
end
This can also be done with class factory method
Memo::Render = Render.extend engine: Render::JSON do
# Memo::Render body
end
def Render.extend(engine:, &class_def)
sub = Class.new(Render, &class_def)
sub.engine = engine # or whatever
sub
end
LikeLike