We discussed this some on IRC. I made an [updated] example in Rust,
based off your version. This one uses trait inheritance and default
methods to give `Entity` the combined behavior of `Positioned`,
`Moveable` and `Renderable`. This syntax may not be exactly right, and
some of it is not implemented yet. I'm not entirely up to speed.
[updated]:
https://gist.github.com/3886118
To my eyes this looks pretty good, except for `impl Entity :
Positioned`, which is pure boilerplate. I'm not sure if there is a
planned solution for that, but it looks to me like an impl that the
compiler could derive automatically. There are plans (but no firm
design) for automatically deriving implementations for traits like Eq.
WRT embedding Point in Entity I confess to not knowing much about
that
solution. It seems like the purpose of that would be to let you get rid
of the Positioned trait and access .x and .y directly, and that would
require `trait Moveable`, etc. to know about the data layout of self.
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev
$ rust_question -vvvv
I
have some questions regarding the future of traits and their intended
usage. Let me preface this post with the information that I am a decade
old Python programmer and have little experience with static-typed
languages and language academics in general so please bare with me if I
exhibit any fundamental ignorance about the topics here.
I began my discovery of Rust while looking at Go. I
soon started to experiment with Go's structure embedding and interfaces
and attempted to draft an experiment in object composition. In the
following contrived example, we will attempt to build up a struct type
that has a two-dimensional position and some "mix-ins" that provide
functionality that work upon that position. In Go, one may embed structs
within each other and the containing or parent struct "inherits" all
fields and methods of embedded types.
First, we define a basic struct called Position that
implements an interface Positioned. You'll notice the interface
essentially covers the Position type's getter/setter methods. This is
the only way (that I know of) to enforce that a Positioned type has the
right fields, in Go.
package main
import "fmt"
type
Positioned interface {
X() int
Y() int
SetX(x int)
SetY(y int)
}
type Position struct {
x, y
int
}
func (self *Position) X() int {
return self.x }
func (self *Position) Y() int { return self.y
}
func (self *Position) SetX(x int) { self.x = x }
func
(self *Position) SetY(y int) { self.y = y }
Now
we create two more empty struct types with corresponding interfaces
called Renderer/Renderable and Mover/Movable. These represent components
that depend and work upon a Positioned type. The important thing to
note here, is that both Render() and Move() must take a reference to an
externally provided Position reference. This will be explained below.
type Renderable interface {
Positioned
Render(*Position)
}
type Renderer
struct {}
func (self *Renderer) Render (pos
*Position) {
fmt.Println("Rendered at,", pos.X(), pos.Y())
}
type
Movable interface {
Positioned
Move(pos
*Position, dx, dy int)
}
type Mover struct {}
func (self
*Mover) Move (pos *Position, x, y int) {
pos.SetX(pos.X() +
x)
pos.SetY(pos.Y() + y)
fmt.Println("Moved
to,", pos.X(), pos.Y())
}
Lastly, we define our composite type
Entity which embeds the Position, Renderer and Mover types. This grants
Entity all the fields and methods of those types such as .pos and
.Render() We show a basic main function that creates an Entity and its
embedded structs and then calls the methods now available to the Entity
type:
type Entity struct {
Position
Renderer
Mover
}
func
main() {
e := Entity {
Position { x:0,
y:0 },
Renderer{}, Mover{},
}
e.Move(&e.Position, 20, 35)
e.Render(&e.Position)
}
This is all fine and dandy. However some abrasive
qualties of this example seem readily apparent to me. For one, even
though the apparent intention of this code is to create a composite type
that is Positioned, Renderable and Movable it ends up being quite
cludgy. The biggest problem I see is that the "shared state" of the
Position which Renderer and Mover depend on must be explicitly passed
around when used with the composite type. One alternative is to wrap
these methods in overrides in Entity to hide this apparently manual
boilerplate. An updated example is here:
http://play.golang.org/p/uD5gCihUKL
Two wrapper methods are supplied for Entity:
func
(self *Entity) Move (x, y int) {
self.Mover.Move(&self.Position, x, y)
}
func (self *Entity) Render () {
self.Renderer.Render(&self.Position)
}
This
allows the external interface for Entity to be much more natural and
encapsulated:
func main() {
e :=
Entity {
Position { x:0, y:0 },
Renderer{}, Mover{},
}
e.Move(20, 35)
e.Render()
}
What is the purpose of these two wrapper
methods? They are, as far as I can tell, required invariant
boilerplate. Invariant in that nothing changes about what I'm doing. Any
sort of inter-dependency between constituent components in composed
types will require this explicit passing of the shared state. The
genesis of this problem is a detail with how methods are bound to types
in Go. We can see that in the Render method of Renderer, we define the
"method receiver" as (self *Renderer)
func (self *Renderer) Render (pos *Position) {
fmt.Println("Rendered at,", pos.X(), pos.Y())
}
Okay.
So Render() is a method that is available on Renderer pointers. But Go
tricks us, in that we can embed a Renderer inside of another struct like
Entity and Entity will gain the Render() method except that when you
call Entity.Render, you are really called Entity.Renderer.Render. When
the method is invoked the `self` receiver is still the Renderer. Once
can imagine that if methods bound to structs that are embedded have
their method reciever updated to be the parent struct instance then this
no becomes a problem. The method should still be able to work upon the
parent struct becuase it has recieved all of the fields and members of
the original type. Here is what the program looks like in this imaginary
version of Go:
http://play.golang.org/p/8MbDFj-G8m
It does not compile obviously, but the Render() and
Move() methods are now much more natural in that they work upon the
`self` name instead of an explicitly passed in Position reference.
func (self *Renderer) Render () {
fmt.Println("Rendered at,", self.X(), self.Y())
}
func
(self *Mover) Move (x, y int) {
self.SetX(self.X() + x)
self.SetY(self.Y() + y)
fmt.Println("Moved to,",
self.X(), self.Y())
}
Alarms
may now being going off, that the compiler will have no idea that the
self receiver implements the X() and Y() methods, because self here is
defined as Renderers and Movers. I understand this. One imagines that Go
would need to add some sort of interface requirements or even allow the
binding of methods to interfaces. func (self *Positioned) Render() {}
would solve this problem entirely, I think.
At this point, I put down Go and started looking
elsewhere. I came across Rust. I found that trait look more like a
proper object composition system, however Rust being so young somethings
are either not implemented or not decided upon. The rest of what I'm
going to say largely depends on Lindsey Kuper's work to unify
typeclasses and traits:
I have begun to construct an example of the same
contrived program. In current Rust, I believe this is as close as I can
get. However, you'll notice that Renderable and Movable are now just
plain-ol "interfaces" and the implmentation of Entity does all of the
work implementing Render() and Move() itself:
http://dpaste.com/hold/812966/
Assuming that I understand the little amount of
information available on train "provided" and "required" methods and how
those work, I can envision an update to the snippet. In this version,
the traits are now supplying "provided" or "default" method
implementations that work upon an explicitly passed `self` parameter.
Not only do the Renderable and Movable traits provide methods they also
declare field requirements for any type using the trait. In this
example, if you're going to implement Movable for a type, it has to have
mutable integer fields called "x" and "y":
http://dpaste.com/812972/
The one thing here I'm making up, is the embedding
of the Point struct into the Entity struct which with minimal effort
allows the Entity to satisfy the field requirements of the traits by
taking on the fields of the Point struct. Now each struct that would
like to implement Renderable and Movable do not need to define their own
individual fields but can just embed another struct that qualifies for
the traits the composed type wants to use.
If struct embedding is entirely out of the question,
then the Renderable and Movable traits can simply require a
pointer-to-a-position field which is only slightly less handy.
So in the end, I suppose I'm looking for some advice as to whether I
have interpreted the direction of traits, Lindsey's work, how idiomatic
object composition and code reuse patters are intended to work in Rust.