As future work we would like to ...(3) extend trait composition so that it can replace inheritance, (4) evaluate the possibility of using traits to modify the behaviour of individual instances at run-time, (5) develop a type systems for traits and identify the relationships between traits and interfaces, and (6) further explore the application of traits to the refactoring of complex class hierarchies.
So Scala already did some job on points (4)(thoughts not so much runtime modification) and (6). I'd like to focus on how trait composition could be used to replace or mask inherited slots.
class Ball {
def properties(): List[String] = List()
override def toString() = "It's a" +
properties.mkString(" ", ", ", " ") +
"ball"
}
trait Red extends Ball {
override def properties() = super.properties ::: List("red")
}
trait Shiny extends Ball {
override def properties() = super.properties ::: List("shiny")
}
object Balls {
def main(args: Array[String]) {
val myBall = new Ball with Shiny with Red
println(myBall) // It's a shiny, red ball
}
}
Scala's traits have one nice ability they are stackable and they can be added to a class in order to add certain behavior to it.
Opaque Traits
So thought occurred to me. What if we could use Traits to replace the Adapter Pattern. As a test case for this kind of traits I took List because it's API is rather verbose and all of methods needed to implement the example don't require aditional states. Plus it shows how Traits could IMHO improve Fantom's elegance a little bit.
@Opaque
trait Stack //: List
{
L? pop()
{
remove(-1)
}
L? peek()
{
last()
}
Void add(L item)
{
add(item)
}
..get/set and any other method we might need in a stack
}
//Later in program we have something like
stack := [1,2,3] with Stack
list := [1,2,3]
stack.last //Error that method doesn't exist on a stack
list.last //3
stack.pop //3
list.pop //List doesn't have pop anymore it has moved into the stack.
For the time being please ignore the syntax as it is a prototype syntax.
So what opaque traits do is that they go one step further than Scala trait and exclude all other slots from the applied class except those that they declare, effectivelly wrapping the List into a Stack. One problem these "Opaque Traits" might experience is the flattening property which states :
"The semantics of a class defined using traits is exactly the same as that of a class constructed
directly from all of the non-overridden methods of the traits."
It's obvious "Opaque Traits" breaks this property. Only solution I could come up is this - when introduced Opaque Trait would narrow down all public slots to private, allowing other traits to access the slot of the class implementing the Opaque Trait but preventing anyone else from using them.
So I'd like to hear your thoughts on this?
tcolarTue 25 Jan 2011
Don't Fantom mixins cover most of the same ground as Scala traits do ?
qualidafialTue 25 Jan 2011
If I understand you, then with:
s := [1, 2, 3] with Stack
The compiler desugars this to something like:
class Stack$Trait implements Stack
{
new make(List instance)
{
this.instance = instance
}
// default method implementations from Stack mixin
}
...
s := Stack$Trait([1, 2, 3])
Is this correct?
rfeldmanTue 25 Jan 2011
The given example doesn't seem to be any better than the adapter pattern; I'd just as soon wrap List and go stack := Stack([1,2,3]) instead of stack := [1,2,3] with Stack and avoid a language learning curve.
Could we see a different example that showcases why trait would be an improvement over the adapter pattern?
DanielFathWed 26 Jan 2011
@tcolar: Yes, they cover most of the same ground, however Scala traits can be added at construction to alter the behavior and they are stackable. In my example you can say.
val mySRBall = new Ball with Shiny with Red //Shiny Red Ball
val mySBall = new Ball with Shiny // Shiny Ball
val myRBall = new Ball with Red // Red Ball
@qualidifial: I'm not 100% sure about implementation, but opaque traits would probably look a lot like what you posted.
@rfeldman: I never said I was using optimal syntax, maybe Trait( WrappedClass ) would be better syntax. Ok, back to the list example. Even Opaque Traits are stackable. For example what if we wanted to make a queue, but for some reason we want it sortable.
queue := Int[,] with Queue //optional syntax queue := Queue(Int[,])
priorityQueue := Int[,] with Queue with Sortable //optional syntax queue := Queue(Sortable(Int[,]))
I'm guessing you could achieve same by using Decorator and Adapter pattern, but advantages are:
1) ideally you write traits only once (they only have required and provided interfaces - not like in Scala where they are defined to extend certain class) 2) you can use any composition of traits in any way you want. IIRC flattening property makes sure you implement only non-overlapping functionality.
Behind the scene compiler would probably generate required Decorator/Adapter when needed ( a single Opaque trait wouldn't need them a Decorator).
katoxWed 26 Jan 2011
There are slightly different definitions (at least for mixins) floating on the web so I'll rather sum up a small terminology clean-up.
Java interfaces were designed to overcome problems of multiple inheritance (painfully visible in C++). With interfaces single inheritance java classes can still announce the availability of certain methods but because interfaces contain no code reuse became a problem.
Mixins are somewhat in the middle between multiple inheritance and single inheritance with interfaces - mixins are components to be reused (they contain code) but they use _inheritance_ to propagate methods further in the hierarchy (so they must have restricted access to object state). The usage of inheritance sometimes makes it difficult to use a mixin "as is" and some kind of glue code might be necessary to fit everything together.
Traits are simple groups of methods which serve as units of code reuse. Unlike mixins traits can be used to simply _compose_ another trait or class. The trait composition is _flat_ - the hierarchy is only a structuring tool and it has no effect on the meaning of the resulting class. A trait :
provides a set of methods with implementations (can be implemented only partially),
requires a set of methods to parametrize the provided behavior,
must not specify state or directly reference it,
can be composed; conflicting methods are excluded from the composition,
can be nested but the result is equivalent to flattened traits.
A class can be constructed from a group of traits plus state variables and methods needed for parametrization (glue). The semantics of methods is independent of whether they are defined in a trait or in a class that uses the trait. The trait composition doesn't affect single inheritance which is an orthogonal concept. Specifically, the hierarchy navigation works the same as in the traditional single inheritance. For instance, for super keyword the referred method is to be found in the superclass of the class that uses the trait - i.e. it is the same as if the method using super was defined inline, in the class definition itself.
rfeldmanWed 26 Jan 2011
Thanks for the example - that does a good job clarifying.
I like the idea. Java has all sorts of useful Collections but there's a lot of mix-and-match going on, what with Set, SortedSet, LinkedList, LinkedHashSet, etc.
Suppose Fantom just had List and a series of traits to modify the default behavior, e.g.:
list := Int[,]
linkedList := Int[,] with Sequential
sortedList := Int[,] with Sorted
hashSet := Int[,] with Set
linkedHashSet := Int[,] with Set, Sequential
treeSet := Int[,] with Set, Sorted
This could add a good bit of data structure functionality without introducing the massive class bloat of something like the java.util package.
Not sure how type safety would work with that...I guess you could have:
Void doSomething(Int[] with Sorted someArgument)
Certainly seems useful, at any rate, as long as it were kept simple.
rfeldmanWed 26 Jan 2011
Food for thought:
Suppose instead of introducing a trait keyword, Fantom simply introduces a with keyword, which is used to compose mixins on the fly.
Is there any reason that wouldn't work? It seems that, at least at a definition level, mixins and traits share the same requirements - methods only, no state, etc. - so couldn't trait functionality be achieved by simply allowing mixins to be composed on the fly?
That sounds like it could be very useful.
(Perhaps something like @Opaque could be used to alter the mixin's behavior when composed on the fly.)
DanielFathWed 26 Jan 2011
However, do note that that you don't have direct access to the underlying data structure. Stack was an easy example because it relies only on existing list methods. Having a Sorted, Seqential, Set isn't impossible but will probably require some kind of pre/post-operation hooks in order to modify list behavior.
@Opaque (maybe @Mask is a better fitting name) simply hides all fields except those that it and other mixin/trait/whatever define. To be quite honest I'm not sure what my trait is a Trait by standard definition. I just thinks it sounds more correct than mixin.
I guess with keyword is ok. Personally I just stick to Scala operator. It can be any other free operator &, :. The syntax really isn't important.
heliumWed 26 Jan 2011
I don't get Opaque Traits. Your example Stack trait calls methods remove, last and add. Where do they come from? Do I have to somehow declare that the type I compose the trait with does have to have those methods?
Removing methods has effects on subtype relations. Foo with SomeOpaqueTrait is not a subtype of Foo as it might lack some of the methods Foo has. This is unlike normal (non-opaque) traits. To me these Opaque Traits look very confusing.
DanielFathSat 29 Jan 2011
@helium: You're right the whole Stack would look like something like this. There are two ways to do traits. One would be to specify a generic interface and let any class that has methods remove, add, last can have the Stack trait applied. Other would be to simply make Stack trait extend list and use it's methods without the need to define them as abstract.
@Opaque
trait Stack //: List
{
abstract V remove(Int index)
abstract L last()
abstract L add(V item)
V? pop()
{
remove(-1)
}
V? peek()
{
last()
}
Void add(V item)
{
add(item)
}
}
As for the second part, I'm afraid I don't have a definitive answer for that. Guess I have to go back to the drawing board :)
I hope my attempt at opaque traits doesn't discourage andy or brian from implementing traits.
DanielFath Tue 25 Jan 2011
Brief introduction to Scala traits
So Scala already did some job on points (4)(thoughts not so much runtime modification) and (6). I'd like to focus on how trait composition could be used to replace or mask inherited slots.
Scala's traits have one nice ability they are stackable and they can be added to a class in order to add certain behavior to it.
Opaque Traits
So thought occurred to me. What if we could use Traits to replace the Adapter Pattern. As a test case for this kind of traits I took List because it's API is rather verbose and all of methods needed to implement the example don't require aditional states. Plus it shows how Traits could IMHO improve Fantom's elegance a little bit.
For the time being please ignore the syntax as it is a prototype syntax.
So what opaque traits do is that they go one step further than Scala trait and exclude all other slots from the applied class except those that they declare, effectivelly wrapping the List into a Stack. One problem these "Opaque Traits" might experience is the flattening property which states :
It's obvious "Opaque Traits" breaks this property. Only solution I could come up is this - when introduced Opaque Trait would narrow down all public slots to private, allowing other traits to access the slot of the class implementing the Opaque Trait but preventing anyone else from using them.
So I'd like to hear your thoughts on this?
tcolar Tue 25 Jan 2011
Don't Fantom mixins cover most of the same ground as Scala traits do ?
qualidafial Tue 25 Jan 2011
If I understand you, then with:
The compiler desugars this to something like:
Is this correct?
rfeldman Tue 25 Jan 2011
The given example doesn't seem to be any better than the adapter pattern; I'd just as soon wrap
List
and gostack := Stack([1,2,3])
instead ofstack := [1,2,3] with Stack
and avoid a language learning curve.Could we see a different example that showcases why
trait
would be an improvement over the adapter pattern?DanielFath Wed 26 Jan 2011
@tcolar: Yes, they cover most of the same ground, however Scala traits can be added at construction to alter the behavior and they are stackable. In my example you can say.
@qualidifial: I'm not 100% sure about implementation, but opaque traits would probably look a lot like what you posted.
@rfeldman: I never said I was using optimal syntax, maybe
Trait( WrappedClass )
would be better syntax. Ok, back to the list example. Even Opaque Traits are stackable. For example what if we wanted to make a queue, but for some reason we want it sortable.I'm guessing you could achieve same by using Decorator and Adapter pattern, but advantages are:
1) ideally you write traits only once (they only have required and provided interfaces - not like in Scala where they are defined to extend certain class) 2) you can use any composition of traits in any way you want. IIRC flattening property makes sure you implement only non-overlapping functionality.
Behind the scene compiler would probably generate required Decorator/Adapter when needed ( a single Opaque trait wouldn't need them a Decorator).
katox Wed 26 Jan 2011
There are slightly different definitions (at least for mixins) floating on the web so I'll rather sum up a small terminology clean-up.
Java interfaces were designed to overcome problems of multiple inheritance (painfully visible in C++). With interfaces single inheritance java classes can still announce the availability of certain methods but because interfaces contain no code reuse became a problem.
Mixins are somewhat in the middle between multiple inheritance and single inheritance with interfaces - mixins are components to be reused (they contain code) but they use _inheritance_ to propagate methods further in the hierarchy (so they must have restricted access to object state). The usage of inheritance sometimes makes it difficult to use a mixin "as is" and some kind of glue code might be necessary to fit everything together.
Traits are simple groups of methods which serve as units of code reuse. Unlike mixins traits can be used to simply _compose_ another trait or class. The trait composition is _flat_ - the hierarchy is only a structuring tool and it has no effect on the meaning of the resulting class. A trait :
A class can be constructed from a group of traits plus state variables and methods needed for parametrization (glue). The semantics of methods is independent of whether they are defined in a trait or in a class that uses the trait. The trait composition doesn't affect single inheritance which is an orthogonal concept. Specifically, the hierarchy navigation works the same as in the traditional single inheritance. For instance, for
super
keyword the referred method is to be found in the superclass of the class that uses the trait - i.e. it is the same as if the method usingsuper
was defined inline, in the class definition itself.rfeldman Wed 26 Jan 2011
Thanks for the example - that does a good job clarifying.
I like the idea. Java has all sorts of useful
Collections
but there's a lot of mix-and-match going on, what withSet
,SortedSet
,LinkedList
,LinkedHashSet
, etc.Suppose Fantom just had
List
and a series of traits to modify the default behavior, e.g.:This could add a good bit of data structure functionality without introducing the massive class bloat of something like the
java.util
package.Not sure how type safety would work with that...I guess you could have:
Certainly seems useful, at any rate, as long as it were kept simple.
rfeldman Wed 26 Jan 2011
Food for thought:
Suppose instead of introducing a
trait
keyword, Fantom simply introduces awith
keyword, which is used to compose mixins on the fly.Is there any reason that wouldn't work? It seems that, at least at a definition level, mixins and traits share the same requirements - methods only, no state, etc. - so couldn't trait functionality be achieved by simply allowing mixins to be composed on the fly?
That sounds like it could be very useful.
(Perhaps something like
@Opaque
could be used to alter the mixin's behavior when composed on the fly.)DanielFath Wed 26 Jan 2011
However, do note that that you don't have direct access to the underlying data structure. Stack was an easy example because it relies only on existing list methods. Having a
Sorted
,Seqential
,Set
isn't impossible but will probably require some kind of pre/post-operation hooks in order to modify list behavior.@Opaque
(maybe@Mask
is a better fitting name) simply hides all fields except those that it and other mixin/trait/whatever define. To be quite honest I'm not sure what my trait is a Trait by standard definition. I just thinks it sounds more correct thanmixin
.I guess
with
keyword is ok. Personally I just stick to Scala operator. It can be any other free operator&
,:
. The syntax really isn't important.helium Wed 26 Jan 2011
I don't get Opaque Traits. Your example
Stack
trait calls methodsremove
,last
andadd
. Where do they come from? Do I have to somehow declare that the type I compose the trait with does have to have those methods?Removing methods has effects on subtype relations.
Foo with SomeOpaqueTrait
is not a subtype ofFoo
as it might lack some of the methodsFoo
has. This is unlike normal (non-opaque) traits. To me these Opaque Traits look very confusing.DanielFath Sat 29 Jan 2011
@helium: You're right the whole Stack would look like something like this. There are two ways to do traits. One would be to specify a generic interface and let any class that has methods remove, add, last can have the Stack trait applied. Other would be to simply make Stack trait extend list and use it's methods without the need to define them as abstract.
As for the second part, I'm afraid I don't have a definitive answer for that. Guess I have to go back to the drawing board :)
I hope my attempt at opaque traits doesn't discourage andy or brian from implementing traits.