joecorcoran/fabrik

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 UnboundMethods 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