diff options
| author | Marijn Haverbeke <marijnh@gmail.com> | 2012-01-12 14:24:36 +0100 |
|---|---|---|
| committer | Marijn Haverbeke <marijnh@gmail.com> | 2012-01-12 14:24:36 +0100 |
| commit | ca72a8300b2781b8267e0afffcc5a7692c25d366 (patch) | |
| tree | 819756af77247abbe20aad3609712e5f19093a7d /doc/tutorial | |
| parent | 0f72c53fdfb912319260aa8462ae20f07599631d (diff) | |
Add tutorial chapter on interfaces
Diffstat (limited to 'doc/tutorial')
| -rw-r--r-- | doc/tutorial/iface.md | 183 | ||||
| -rw-r--r-- | doc/tutorial/order | 1 |
2 files changed, 184 insertions, 0 deletions
diff --git a/doc/tutorial/iface.md b/doc/tutorial/iface.md new file mode 100644 index 00000000000..c047cf06da5 --- /dev/null +++ b/doc/tutorial/iface.md @@ -0,0 +1,183 @@ +# Interfaces + +Interfaces are Rust's take on value polymorphism—the thing that +object-oriented languages tend to solve with methods and inheritance. +For example, writing a function that can operate on multiple types of +collections. + +NOTE: This feature is very new, and will need a few extensions to be +applicable to more advanced use cases. + +## Declaration + +An interface consists of a set of methods. A method is a function that +can be applied to a `self` value and a number of arguments, using the +dot notation: `self.foo(arg1, arg2)`. + +For example, we could declare the interface `to_str` for things that +can be converted to a string, with a single method of the same name: + + iface to_str { + fn to_str() -> str; + } + +## Implementation + +To actually implement an interface for a given type, the `impl` form +is used. This defines implementations of `to_str` for the `int` and +`str` types. + + # iface to_str { fn to_str() -> str; } + impl of to_str for int { + fn to_str() -> str { int::to_str(self, 10u) } + } + impl of to_str for str { + fn to_str() -> str { self } + } + +Given these, we may call `1.to_str()` to get `"1"`, or +`"foo".to_str()` to get `"foo"` again. This is basically a form of +static overloading—when the Rust compiler sees the `to_str` method +call, it looks for an implementation that matches the type with a +method that matches the name, and simply calls that. + +## Scoping + +Implementations are not globally visible. Resolving a method to an +implementation requires that implementation to be in scope. You can +import and export implementations using the name of the interface they +implement (multiple implementations with the same name can be in scope +without problems). Or you can give them an explicit name if you +prefer, using this syntax: + + # iface to_str { fn to_str() -> str; } + impl nil_to_str of to_str for () { + fn to_str() -> str { "()" } + } + +## Bounded type parameters + +The useful thing about value polymorphism is that it does not have to +be static. If object-oriented languages only let you call a method on +an object when they knew exactly which sub-type it had, that would not +get you very far. To be able to call methods on types that aren't +known at compile time, it is possible to specify 'bounds' for type +parameters. + + # iface to_str { fn to_str() -> str; } + fn comma_sep<T: to_str>(elts: [T]) -> str { + let result = "", first = true; + for elt in elts { + if first { first = false; } + else { result += ", "; } + result += elt.to_str(); + } + ret result; + } + +The syntax for this is similar to the syntax for specifying that a +parameter type has to be copyable (which is, in principle, another +kind of bound). By declaring `T` as conforming to the `to_str` +interface, it becomes possible to call methods from that interface on +values of that type inside the function. It will also cause a +compile-time error when anyone tries to call `comma_sep` on an array +whose element type does not have a `to_str` implementation in scope. + +## Polymorphic interfaces + +Interfaces may contain type parameters. This defines an interface for +generalized sequence types: + + iface seq<T> { + fn len() -> uint; + fn iter(block(T)); + } + impl <T> of seq<T> for [T] { + fn len() -> uint { vec::len(self) } + fn iter(b: block(T)) { + for elt in self { b(elt); } + } + } + +Note that the implementation has to explicitly declare the its +parameter `T` before using it to specify its interface type. This is +needed because it could also, for example, specify an implementation +of `seq<int>`—the `of` clause *refers* to a type, rather than defining +one. + +## Casting to an interface type + +The above allows us to define functions that polymorphically act on +values of *an* unknown type that conforms to a given interface. +However, consider this function: + + # iface drawable { fn draw(); } + fn draw_all<T: drawable>(shapes: [T]) { + for shape in shapes { shape.draw(); } + } + +You can call that on an array of circles, or an array of squares +(assuming those have suitable `drawable` interfaces defined), but not +on an array containing both circles and squares. + +When this is needed, an interface name can be used as a type, causing +the function to be written simply like this: + + # iface drawable { fn draw(); } + fn draw_all(shapes: [drawable]) { + for shape in shapes { shape.draw(); } + } + +There is no type parameter anymore (since there isn't a single type +that we're calling the function on). Instead, the `drawable` type is +used to refer to a type that is a reference-counted box containing a +value for which a `drawable` implementation exists, combined with +information on where to find the methods for this implementation. This +is very similar to the 'vtables' used in most object-oriented +languages. + +To construct such a value, you use the `as` operator to cast a value +to an interface type: + + # type circle = int; type rectangle = int; + # iface drawable { fn draw(); } + # impl of drawable for int { fn draw() {} } + # fn new_circle() -> int { 1 } + # fn new_rectangle() -> int { 2 } + # fn draw_all(shapes: [drawable]) {} + let c: circle = new_circle(); + let r: rectangle = new_rectangle(); + draw_all([c as drawable, r as drawable]); + +This will store the value into a box, along with information about the +implementation (which is looked up in the scope of the cast). The +`drawable` type simply refers to such boxes, and calling methods on it +always works, no matter what implementations are in scope. + +Note that the allocation of a box is somewhat more expensive than +simply using a type parameter and passing in the value as-is, and much +more expensive than statically resolved method calls. + +## Interface-less implementations + +If you only intend to use an implementation for static overloading, +and there is no interface available that it conforms to, you are free +to leave off the `of` clause. + + # type currency = (); + # fn mk_currency(x: int, s: str) {} + impl int_util for int { + fn times(b: block(int)) { + let i = 0; + while i < self { b(i); i += 1; } + } + fn dollars() -> currency { + mk_currency(self, "USD") + } + } + +This allows cutesy things like `send_payment(10.dollars())`. And the +nice thing is that it's fully scoped, so the uneasy feeling that +anybody with experience in object-oriented languages (with the +possible exception of Rubyists) gets at the sight of such things is +not justified. It's harmless! diff --git a/doc/tutorial/order b/doc/tutorial/order index bf621816d47..9f751663901 100644 --- a/doc/tutorial/order +++ b/doc/tutorial/order @@ -7,6 +7,7 @@ data args generic mod +iface ffi task test |
