tutorial: swap order of "Traits" and "Modules and Crates" sections.

This puts "Traits" next to "Generics", as requested by issue #3397.
Closes #3397.
This commit is contained in:
Lindsey Kuper 2012-09-07 20:12:16 -04:00
parent 53ce42dc4f
commit 62ab9d70f4

View File

@ -1605,6 +1605,203 @@ resource type. Rust has several kinds that can be used as type bounds:
> kinds will actually be traits that the compiler has special
> knowledge about.
# Traits
Traits 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
A trait 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 trait `to_str` for things that
can be converted to a string, with a single method of the same name:
~~~~
trait to_str {
fn to_str() -> ~str;
}
~~~~
## Implementation
To actually implement a trait for a given type, the `impl` form
is used. This defines implementations of `to_str` for the `int` and
`~str` types.
~~~~
# trait to_str { fn to_str() -> ~str; }
impl int: to_str {
fn to_str() -> ~str { int::to_str(self, 10u) }
}
impl ~str: to_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.
## 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.
~~~~
# trait to_str { fn to_str() -> ~str; }
fn comma_sep<T: to_str>(elts: ~[T]) -> ~str {
let mut result = ~"", first = true;
for elts.each |elt| {
if first { first = false; }
else { result += ~", "; }
result += elt.to_str();
}
return 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`
trait, it becomes possible to call methods from that trait 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 traits
Traits may contain type parameters. A trait for
generalized sequence types is:
~~~~
trait seq<T> {
fn len() -> uint;
fn iter(fn(T));
}
impl<T> ~[T]: seq<T> {
fn len() -> uint { vec::len(self) }
fn iter(b: fn(T)) {
for self.each |elt| { b(elt); }
}
}
~~~~
The implementation has to explicitly declare the type
parameter that it binds, `T`, before using it to specify its trait type. Rust requires this declaration because the `impl` could also, for example, specify an implementation of `seq<int>`. The trait type -- appearing after the colon in the `impl` -- *refers* to a type, rather than defining one.
The type parameters bound by a trait are in scope in each of the
method declarations. So, re-declaring the type parameter
`T` as an explicit type parameter for `len` -- in either the trait or
the impl -- would be a compile-time error.
## The `self` type in traits
In a trait, `self` is a special type that you can think of as a
type parameter. An implementation of the trait for any given type
`T` replaces the `self` type parameter with `T`. The following
trait describes types that support an equality operation:
~~~~
trait eq {
fn equals(&&other: self) -> bool;
}
impl int: eq {
fn equals(&&other: int) -> bool { other == self }
}
~~~~
Notice that `equals` takes an `int` argument, rather than a `self` argument, in
an implementation for type `int`.
## Casting to a trait type
The above allows us to define functions that polymorphically act on
values of *an* unknown type that conforms to a given trait.
However, consider this function:
~~~~
# type circle = int; type rectangle = int;
# trait drawable { fn draw(); }
# impl int: drawable { fn draw() {} }
# fn new_circle() -> int { 1 }
fn draw_all<T: drawable>(shapes: ~[T]) {
for shapes.each |shape| { shape.draw(); }
}
# let c: circle = new_circle();
# draw_all(~[c]);
~~~~
You can call that on an array of circles, or an array of squares
(assuming those have suitable `drawable` traits defined), but not
on an array containing both circles and squares.
When this is needed, a trait name can be used as a type, causing
the function to be written simply like this:
~~~~
# trait drawable { fn draw(); }
fn draw_all(shapes: ~[drawable]) {
for shapes.each |shape| { 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 a trait type:
~~~~
# type circle = int; type rectangle = int;
# trait drawable { fn draw(); }
# impl int: drawable { 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.
## Trait-less implementations
If you only intend to use an implementation for static overloading,
and there is no trait available that it conforms to, you are free
to leave off the type after the colon. However, this is only possible when you
are defining an implementation in the same module as the receiver
type, and the receiver type is a named type (i.e., an enum or a
class); [single-variant enums](#single_variant_enum) are a common
choice.
# Modules and crates
The Rust namespace is divided into modules. Each source file starts
@ -1872,203 +2069,6 @@ This makes it possible to rebind a variable without actually mutating
it, which is mostly useful for destructuring (which can rebind, but
not assign).
# Traits
Traits 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
A trait 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 trait `to_str` for things that
can be converted to a string, with a single method of the same name:
~~~~
trait to_str {
fn to_str() -> ~str;
}
~~~~
## Implementation
To actually implement a trait for a given type, the `impl` form
is used. This defines implementations of `to_str` for the `int` and
`~str` types.
~~~~
# trait to_str { fn to_str() -> ~str; }
impl int: to_str {
fn to_str() -> ~str { int::to_str(self, 10u) }
}
impl ~str: to_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.
## 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.
~~~~
# trait to_str { fn to_str() -> ~str; }
fn comma_sep<T: to_str>(elts: ~[T]) -> ~str {
let mut result = ~"", first = true;
for elts.each |elt| {
if first { first = false; }
else { result += ~", "; }
result += elt.to_str();
}
return 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`
trait, it becomes possible to call methods from that trait 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 traits
Traits may contain type parameters. A trait for
generalized sequence types is:
~~~~
trait seq<T> {
fn len() -> uint;
fn iter(fn(T));
}
impl<T> ~[T]: seq<T> {
fn len() -> uint { vec::len(self) }
fn iter(b: fn(T)) {
for self.each |elt| { b(elt); }
}
}
~~~~
The implementation has to explicitly declare the type
parameter that it binds, `T`, before using it to specify its trait type. Rust requires this declaration because the `impl` could also, for example, specify an implementation of `seq<int>`. The trait type -- appearing after the colon in the `impl` -- *refers* to a type, rather than defining one.
The type parameters bound by a trait are in scope in each of the
method declarations. So, re-declaring the type parameter
`T` as an explicit type parameter for `len` -- in either the trait or
the impl -- would be a compile-time error.
## The `self` type in traits
In a trait, `self` is a special type that you can think of as a
type parameter. An implementation of the trait for any given type
`T` replaces the `self` type parameter with `T`. The following
trait describes types that support an equality operation:
~~~~
trait eq {
fn equals(&&other: self) -> bool;
}
impl int: eq {
fn equals(&&other: int) -> bool { other == self }
}
~~~~
Notice that `equals` takes an `int` argument, rather than a `self` argument, in
an implementation for type `int`.
## Casting to a trait type
The above allows us to define functions that polymorphically act on
values of *an* unknown type that conforms to a given trait.
However, consider this function:
~~~~
# type circle = int; type rectangle = int;
# trait drawable { fn draw(); }
# impl int: drawable { fn draw() {} }
# fn new_circle() -> int { 1 }
fn draw_all<T: drawable>(shapes: ~[T]) {
for shapes.each |shape| { shape.draw(); }
}
# let c: circle = new_circle();
# draw_all(~[c]);
~~~~
You can call that on an array of circles, or an array of squares
(assuming those have suitable `drawable` traits defined), but not
on an array containing both circles and squares.
When this is needed, a trait name can be used as a type, causing
the function to be written simply like this:
~~~~
# trait drawable { fn draw(); }
fn draw_all(shapes: ~[drawable]) {
for shapes.each |shape| { 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 a trait type:
~~~~
# type circle = int; type rectangle = int;
# trait drawable { fn draw(); }
# impl int: drawable { 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.
## Trait-less implementations
If you only intend to use an implementation for static overloading,
and there is no trait available that it conforms to, you are free
to leave off the type after the colon. However, this is only possible when you
are defining an implementation in the same module as the receiver
type, and the receiver type is a named type (i.e., an enum or a
class); [single-variant enums](#single_variant_enum) are a common
choice.
# Tasks
Rust supports a system of lightweight tasks, similar to what is found