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
