Hi list
after some very interesting input I decided to rewrite Ruby-Traits
completely.
http://rubyforge.org/frs/?group_id=4642&release_id=15737
I have decided to implement two different kinds of traits:
- ruby-traits
and - pure-traits.
In detail:
-
ruby-traits are mimicking some of the properties of traits but violate
the
flattening property. They implement some of the more dynamic features
of modules and are in fact module based. The code is heavily influenced
by Thomas S.‘s traits from Facets. Many thanks to Thomas for the
permission to use his code. Note that Thomas’ traits of Facets is yet
another
different beast. -
pure-traits implement traits in the strict sense of their definition
here:
http://portal.acm.org/ft_gateway.cfm?id=1028771&type=pdf&coll=GUIDE&dl=GUIDE&CFID=7912496&CFTOKEN=77115102
They obey the flattening property, using a trait is exactly as defining
its methods inline, no modules are used and inheritance is unchanged.
All trait combination rules are obeyed (associative, communative, e.g.)
The main purpose of pure-traits is to play around with them and to see
the
benefits and shortcomings of traits based design.
The syntax has changed, again inspired by Factes, trait aliasing is done
via #* now, thus traits aliased and combined can be expressed more
naturally:
t1 * { :t1_a => :a } + t2 * { :t2_a => :a }
works now as expected.
In addition to the traditional trait operations +, - and *, I have
implemented & to allow of selective usage of methods.
I believe that the usage of this is normally a bad sign for the design
but sometimes it might be a handy feature.
Pure Traits
Traits are Composable Units of Behavior. they are described here in
detail
The interested can look here
for a detailed description.
For the Rubyist Traits can be explained in some short phrases quite
effectively:
* Traits are a Collection of Composable Behavior (e.g. methods)
They ressemble a lot to modules.
t1 = trait{ def a; 42 end }
assert_kind_of Trait, t1
t2 = Trait::new { def b; 46 end }
* Traits can be composed by operations as + (union), &
(intersection), * (aliasing)
and - (substraction). The result of such a trait composition is
always a new trait.
As a matter of fact traits are immutable( with the exception of
metaprogramming techniques ).
t3 = t1 + t2
assert_kind_of Trait, t3
c1 = Class::new {
use t3
}
assert_equal 42, c1.new.a
assert_equal 46, c1.new.b
* When a trait is composed and only in that case conflicts might
occur. Conflicts
can only occur during trait unification (that is +) in case the two
unified
traits contain methods with the same name, these methods are
replaced by conflict
raising methods. It is the responsability of the using entity
(Module, Trait or Class)
to resolve the conflict.
t4 = Trait::new do
def a; :t4 end
end
t5 = Trait::new do
def a; :t5 end
end
c1 = Class::new do
use t4, t5 ### this is exactly the same as "use t4 + t5"
end
c2 = Class::new do
use t4
use t5 ### This is *not* the same as the above
end
assert_raise TraitsConflict do
c1.new.a
end
assert_equal :t5, c2.new.a
The second case is distinct to the first in that a “use” of a trait is
a
transparent operation in that the trait’s methods are just defined as
if they
were defined in the using entity (c2 in our example above).
Almost as if they were macroexpanded
As mentioned above it is up to the using entity to resolve such
conflicts, sometime
such conflict resolution needs access to the conflicting methods, in
these cases
aliasing is our friend.
t6 = trait do
def the_answer; 22 end
end
t7 = trait do
def the_answer; 20 end
end
c3 = Class::new {
use t6 * {:first_answer => :the_answer} +
t7 * {:second_answer=> :the_answer}
def the_answer; first_answer + second_answer end
}
assert_equal 42, c3.new.the_answer
Sometimes of course we do not want to resolve the conflict
explicitly but we just
want to avoid it by choising from which trait we want to use the
method, again
trait composition shall be used to achieve that task.
m1 = Module::new do
use t6 + ( t7 - :the_answer )
end
assert_equal 22, Class::new{ extend m1 }.the_answer
* When traits are used they obey to the Flattening Property, that is
the using entity
does not inherit from the trait but just get its methods defined.
Again one can very nicely understand what is going on when
thinking of macro expansion.
t= trait {
def a; 1 end
}
c = Class::new {
use t
}
o = c.new
assert_equal 1, o.a
t.send :add_method, :a do 2 end
assert_equal 1, o.a
assert_equal 1, c.new.a
assert_equal 2, t.to_class.new.a
* Calls to super in trait methods are dynamically resolved at
runtime, as is the case
for modules.
t8 = trait { def a
super * 7 end }
c4 = Class::new { def a; 3 end }
c5 = Class::new( c4 ) {
use t8
}
assert_equal 21, c5.new.a
Again the flattening property can be tested with the semantics of
super, the usage of the trait
just defines methods, they can be replaced with methods in the class
later, but they are not
accessible via super, as they were if they had been included via
modules.
c5.module_eval do
def a; super.succ end
end
assert_equal 4, c5.new.a
Ruby Traits
Traits are Composable Units of Behavior. they are described here in
detail
The interested can look here
for a detailed description.
For the Rubyist Traits can be explained in some short phrases quite
effectively:
* Traits are a Collection of Composable Behavior (e.g. methods)
The ressemble a lot to modules.
t1 = trait{ def a; 42 end }
assert_kind_of Trait, t1
t2 = Trait::new { def b; 46 end }
* Traits can be composed by operations as + (union), &
(intersection), * (aliasing)
and - (substraction). The result of such a trait composition is
always a new trait.
As a matter of fact traits are immutable( with the exception of
metaprogramming techniques ).
t3 = t1 + t2
assert_kind_of Trait, t3
c1 = Class::new {
use t3
}
assert_equal 42, c1.new.a
assert_equal 46, c1.new.b
* When a trait is composed and only in that case conflicts might
occur. Conflicts
can only occur during trait unification (that is +) if and only if
the two unified
traits contain methods with the same name, these methods are
replaced by conflict
raising methods. It is the responsability of the using entity
(Module, Trait or Class)
to resolve the conflict.
t4 = Trait::new do
def a; :t4 end
end
t5 = Trait::new do
def a; :t5 end
end
c1 = Class::new do
use t4, t5 ### this is exactly the same as "use t4 + t5"
end
c2 = Class::new do
use t4
use t5 ### This is *not* the same as the above
end
assert_raise TraitsConflict do
c1.new.a
end
assert_equal :t5, c2.new.a
The second case is distinct to the first in that a “use” of a trait is
a
transparent operation in which the trait’s methods are just defined as
if they
were defined in the using entity (c2 in our example above).
As mentioned above it is up to the using entity to resolve such
conflicts, sometime
such conflict resolution needs access to the conflicting methods, in
these cases
aliasing is our friend.
t6 = trait do
def the_answer; 22 end
end
t7 = trait do
def the_answer; 20 end
end
c3 = Class::new {
use t6 * {:the_answer=>:first_answer} +
t7 * {:the_answer=>:second_answer}
def the_answer; first_answer + second_answer end
}
assert_equal 42, c3.new.the_answer
Sometimes of course we do not want to resolve the conflict
explicitly but we just
want to avoid it by choising from which trait we want to use the
method, again
trait composition shall be used to achieve that task.
m1 = Module::new do
use t6 + ( t7 - :the_answer )
def the_answer; super.to_s.to_i(20) end
end
assert_equal 42, Class::new{ extend m1 }.the_answer
* When traits are used they obey to the Flattening Property, that is
the using entity
does not inherit from the trait but just get its methods defined
exactly as if the defs
of the trait were macro expanded into the using entity.
This however is something I have not implemented in ruby-traits
(only in pure-traits).
As one can notice in the super example above the method
t6#the_answer can be accessed via
super in the method m1#the_answer
There are two reasons for this, (a) I wanted to use Thomas’
elegant and simple approach
and (b) this is a behaviour Rubyists are rather fond of, so I
really thaught that
it might be a good idea to do this. The price to pay is that
traits still behave a little
bit too much as Modules, the following example shows the down side
of this approach
t= trait {
def a; 1 end
}
c = Class::new {
use t
}
assert c < t
o = c.new
assert_equal 1, o.a
t.send :add_method, :a do 2 end
assert_equal 2, o.a
Another downside of this is that Trait#define_method,
Trait#remove_method and friends
cannot be undefined, using them on traits directly would however
endanger correct
conflict detection or trigger false conflicts!
In order to do correct traits metaprogramming Traits#add_method and
Traits#del_method,
both are private, shall be used.
* Calls to super in trait methods are dynamically resolved at
runtime, as is the case
for modules.
This even works together with the usage of super as shown above
t8 = trait { def a; super * 7 end }
c4 = Class::new { def a; 3 end }
c5 = Class::new( c4 ) {
use t8
def a; super * 2 end
}
assert_equal 42, c5.new.a
# Trait#to_class simply is a convenience method that
# simply creates an anonymous class from this trait.
“t.to_class(parent=Object)” is short for
# “Class::new(parent)”{ use t }
t9 = trait { def a; :a9 end; def b; :b9 end }
t10 = trait { def b; :b10 end; def c; :c10 end }
c6 = ( t9 + t10 ).to_class
assert_equal :a9, c6.new.a
assert_equal :c10, c6.new.c
assert_raise TraitsConflict do c6.new.b end