On 10/12/2012 12:05 PM, Dustin Lacewell wrote:
$ 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)
}
A runnable example of this is available here:
http://play.golang.org/p/gCu91h9BqQ
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:
https://github.com/mozilla/rust/wiki/Proposal-for-unifying-traits-and-interfaces
https://mail.mozilla.org/pipermail/rust-dev/2012-August/002276.html
https://air.mozilla.org/rust-typeclasses/
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.
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