$ 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.
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev

Reply via email to