On 13 October 2012 22:57, Brian Anderson <[email protected]> wrote:
> 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

What I was thinking is that if Rust had properties and they could be
part of traits, x and y properties could be defined as part of the
Positioned trait.

Alternatively, fields could be part of traits and there could be a
mechanism for implementations of Positioned to declare where their x
and y fields are.

I'm not sure any of this makes sense as part of the ongoing plans.
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev

Reply via email to