What about using just modules without provides block?
alex-fedorov opened this issue · 10 comments
like this
module Cat
def talk; "meow" end
end
module Animal
def eat; "nomnom" end
end
class Joe
extend Fabrik::Composer
compose Cat, Animal
end
If you already considered and/or tried it, then what was the problem with implementing that?
Hi Alex,
As best I can remember (I wrote this library quite quickly, some months ago) there were a handful of reasons.
The first is that I was working towards a solution that did as little binding and unbinding of methods as possible. By defining traits as classes that contain a dictionary (Fabrik::Dictionary
), I was hoping to instantiate UnboundMethod
s only once each and store them in the dictionary, rather than every time a trait is used in a composing class. I never quite got there but I'm sure it's possible and it would probably be nicer that way.
Secondly, one of the features of traits is that they too are composable. So in order to add the composition behaviour, they had to be implemented as classes. Otherwise the only way to "compose" traits would be to include
them within each other, which is effectively inheritance under a different name.
The last reason is a bit more academic. Since one of the features of traits in the Ducasse paper is that two identical methods (same method bodies and names) from different traits should not cause a conflict when composed, I had to do some backflips to actually create a situation where that was possible in Ruby. If traits were implemented as modules and used as intended, there would never be a situation in which they could provide identical methods.
Hi,
But you can always hide this behind simple modules. I.e.:
- User of library defines some modules with methods,
- User defines a class with
compose
statement compose
method deals with what you feed to it and generates required classes and dictionaries on the fly. Of course, if such trait class already was generated it will just use it.
This approach basically allows to leave current provides
mechanism as it is, but hide it with more cleaner syntax.
Consider the second point about traits themselves being composable though. Currently you can do this:
class Foo
extend Fabrik::Trait
provides do
def a; end
end
end
class Bar
extend Fabrik::Trait
compose Foo
end
How would that be possible if the traits were just modules?
Hmm,
Again compose can detect that it is currently used on module (trait in our case) and generate a trait class for it and then apply compose to this generated class.
If you want I can code up a proof of concept on this weekend. It should be relatively easy actually.
Like I say, my motivation was partly academic – I wanted to get as close as possible to the traits described in the Ducasse paper, and that involves traits being standalone entities that are "flattenable" when composed without conflict. In the real world that might not be such a valuable property for most use cases.
It would be fun to see an implementation. My only issue with it would be that you'd be doing an awful lot of work at composition time and the only benefit would be that you can pretend that plain old modules are traits. One of the nice things about traits is that they reduce a lot of the implicit behaviour that Ruby's pseudo-composition brings. If something is intended as a trait, it's kind of nice to be explicit about it. Sometimes a nominally cleaner syntax actually results in a bunch of hidden intentions. Although I completely accept that there's something a bit clumsy about provides
, it seems like a small price to pay. :)
What about even more explicit syntax then:
Cat = Fabrik::Trait.new do
def say; "meow" end
end
?
And if you want to compose traits into other traits, something along these lines:
Cat = Fabrik::Trait.new(Animal, Alive, Lively[exclude: :act]) do
def say; "meow" end
end
should be as easy as:
module Trait
def self.new(*args, &blk)
Class.new do
include Trait
compose(*args) if args.count > 0
provides(&blk) if blk
end
end
end
And probably better use :build
instead of :new