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

Reply via email to