Added a few idioms and patterns - some are still WIP

This commit is contained in:
Nick Cameron 2015-10-23 09:53:16 +13:00
parent cf8479320a
commit 325b642a01
7 changed files with 381 additions and 7 deletions

View File

@ -14,29 +14,29 @@ language.
* [Concatenating strings with `format!`](idioms/concat-format.md)
* TODO private field to indicate extensibility
* TODO trait to separate visibility of methods from visibility of data (https://github.com/sfackler/rust-postgres/blob/master/src/lib.rs#L1400)
* TODO Deref on Vec/String to treat like a smart pointer/borrowed view of such data
* [Collections are smart pointers](idioms/deref.md)
* TODO leak amplification ("Vec::drain sets the Vec's len to 0 prematurely so that mem::forgetting Drain "only" mem::forgets more stuff. instead of exposing uninitialized memory or having to update the len on every iteration")
* TODO dtor for finally
* [Finalisation in destructors](idioms/dtor-finally.md)
* TODO interior mutability - UnsafeCell, Cell, RefCell
* TODO treating Option like a list
### Design patterns
* [Builder](patterns/builder.md)
* TODO RAII ( + borrows - mutex guard, [style guide entry]())
* [RAII guards](patterns/raii.md)
* [Newtype](patterns/newtype.md)
* TODO iterators (to safely avoid bounds checks)
* TODO closures and lifetimes (coupling to lifetime)
* TODO platform-specific sub-modules (https://github.com/rust-lang/rfcs/blob/master/text/0517-io-os-reform.md#platform-specific-opt-in)
* TODO affine types/session types
* TODO Entry API vs insert_or_update etc.
* [Entry API](patterns/entry.md)
* TODO visitor
* TODO fold
* TODO small crates and semver
* TODO extension traits
* TODO destructor bombs (ensure linear typing dynamically, e.g., https://github.com/Munksgaard/session-types/commit/0f25ccb7c3bc9f65fa8eaf538233e8fe344a189a)
* TODO convertible to Foo trait for more generic generics (e.g., http://static.rust-lang.org/doc/master/std/fs/struct.File.html#method.open)
* TODO late binding of bounds for better APIs (i.e., Mutex's don't require Send)
* [Late bound bounds](patterns/late-bounds.md)
* TODO 'shadow' borrowed version of struct - e.g., double buffering, Niko's parser generator
* TODO composition of structs to please the borrow checker
@ -49,7 +49,7 @@ language.
* TODO Deref polymorphism
* TODO Matching all fields of a struct (back compat)
* TODO wildcard matches
* TODO tkaing an enum rather than having multiple functions
* TODO taking an enum rather than having multiple functions

81
idioms/deref.md Normal file
View File

@ -0,0 +1,81 @@
# Collections are smart pointers
## Description
Use the `Deref` trait to treat collections like smart pointers, offering owning
and borrowed views of data.
## Example
```rust
struct Vec<T> {
...
}
impl<T> Deref for Vec<T> {
type Target = [T];
fn deref(&self) -> &[T] {
...
}
}
```
A `Vec<T>` is an owning collection of `T`s, a slice (`&[T]`) is a borrowed
collection of `T`s. Implementing `Deref` for `Vec` allows implicit dereferencing
from `&Vec<T>` to `&[T]` and includes the relationship in auto-derefencing
searches. Most methods you might expect to be implemented for `Vec`s are instead
implemented for slices.
See also `String` and `&str`.
## Motivation
Ownership and borrowing are key aspects of the Rust language. Data structures
must account for these semantics properly in order to give a good user
experience. When implementing a data structure which owns its data, offering a
borrowed view of that data allows for more flexible APIs.
## Advantages
Most methods can be implemented only for the borrowed view, they are then
implicitly available for the owning view.
Gives clients a choice between borrowing or taking ownership of data.
## Disadvantages
Methods and traits only available via dereferencing are not taken into account
when bounds checking, so generic programming with data structures using this
pattern can get complex (see the `Borrow` and `AsRef` traits, etc.).
## Discussion
Smart pointers and collections are analogous: a smart pointer points to a single
object, whereas a collection points to many objects. From the point of view of
the type system there is little difference between the two. A collection owns
its data if the only way to access each datum is via the collection and the
collection is responsible for deleting the data (even in cases of shared
ownership, some kind of borrowed view may be appropriate). If a collection owns
its data, it is usually useful to provide a view of the data as borrowed so that
it can be multiply referenced.
Most smart pointers (e.g., `Foo<T>`) implement `Deref<Target=T>`. However,
collections will usually dereference to a custom type. `[T]` and `str` have some
language support, but in the general case, this is not necessary. `Foo<T>` can
implement `Deref<Target=Bar<T>>` where `Bar` is a dynamically sized type and
`&Bar<T>` is a borrowed view of the data in `Foo<T>`.
Commonly, ordered collections will implement `Index` for `Range`s to provide
slicing syntax. The target will be the borrowed view.
## See also
Deref polymorphism anti-pattern.
[Documentation for `Deref` trait](https://doc.rust-lang.org/std/ops/trait.Deref.html).

96
idioms/dtor-finally.md Normal file
View File

@ -0,0 +1,96 @@
# Finalisation in destructors
## Description
Rust does not provide he equivalent to `finally` blocks - code that will be
executed no matter how a function is exited. Instead an object's destructor can
be used to run code that must be run before exit.
## Example
```rust
fn bar() -> Result<(), ()> {
// These don't need to be defined inside the function.
struct Foo;
// Implement a destructor for Foo.
impl Drop for Foo {
fn drop(&mut self) {
println!("exit");
}
}
// The dtor of _exit is run however `bar` is exited.
let _exit = Foo;
// Implicit return in try!.
try!(baz());
// Normal return.
OK(())
}
```
## Motivation
If a function has multiple return points, then executing code on exit becomes
difficult and repetitive (and thus bug-prone). This is especially the case where
return is implicit due to a macro. A common case is `try!` which returns if the
result is an `Err`, but continues if it is `Ok`. `try!` is used as an exception
handling mechanism, but unlike Java (which has `finally`), there is no way to
schedule code to run in both the normal and exceptional cases. Panicking will
also exit a function early.
## Advantages
Code in destructors will (nearly) always be run - copes with panics, early
returns, etc.
## Disadvantages
It is not guaranteed that destructors will run. For example, if there is an
infinite loop in a function or if running a function crashes before exit.
Destructors are also not run in the case of a panic in an already panicking
thread. Therefore destructors cannot be relied on as finalisers where it is
absolutely essential that finalisation happens.
This pattern introduces some hard to notice, implicit code. Reading a function
gives no clear indication of destructors to be run on exit. This can make
debugging tricky.
Requiring an object and `Drop` impl just for finalisation is heavy on boilerplate.
## Discussion
There is some subtlety about how exactly to store the object used as a
finaliser. It must be kept alive until the end of the function and must then be
destroyed. The object must always be a value or uniquely owned pointer (e.g.,
`Box<Foo>`). If a shared pointer (such as `Rc`) is used, then the finaliser can
be kept alive beyond the lifetime of the function. For similar reasons, the
finaliser should not be moved or returned.
The finaliser must be assigned into a variable, otherwise it will be destroyed
immediately, rather than when it goes out of scope. The variable name must start
with `_` if the variable is only used as a finaliser, otherwise the compiler
will warn that the finaliser is never used. However, do not call the variable
`_` with no suffix - in that case it will be again be destroyed immediately.
In Rust, destructors are run when an object goes out of scope. This happens
whether we reach the end of block, there is an early return, or the program
panics. When panicking, Rust unwinds the stack running destructors for each
object in each stack frame. So, destructors get called even if the panic happens
in a function being called.
If a destructor panics while unwinding, there is no good action to take, so Rust
aborts the thread immediately, without running further destructors. This means
that desctructors are not absolutely guaranteed to run. It also means that you
must take extra care in your destructors not to panic, since it could leave
resources in an unexpected state.
## See also
[RAII](../patterns/raii.md).

115
patterns/RAII.md Normal file
View File

@ -0,0 +1,115 @@
# RAII guards
## Description
RAII stands for "Resource allocation is initialisation" which is a terrible
name. The essence of the pattern is that resource initialisation is done in the
constructor of an object and finalisation in the destructor. This pattern is
extended in Rust by using an RAII object as a guard of some resource and relying
on the type system to ensure that access is always mediated by the guard object.
## Example
Mutex guards are the classic example of this pattern from the std library (this
is a simplified version of the real implementation):
```rust
struct Mutex<T> {
// We keep a reference to our data: T here.
...
}
struct MutexGuard<'a, T: 'a> {
data: &'a T,
...
}
// Locking the mutex is explicit.
impl<T> Mutex<T> {
fn lock(&self) -> MutexGuard<T> {
// Lock the underlying OS mutex.
...
// MutexGuard keeps a reference to self
MutexGuard { data: self, ... }
}
}
// Destructor for unlocking the mutex.
impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
// Unlock the underlying OS mutex.
...
}
}
// Implementing Deref means we can treat MutexGuard like a pointer to T.
impl<'a, T> Deref for MutexGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
self.data
}
}
fn main(x: Mutex<Foo>) {
let xx = x.lock();
xx.foo(); // foo is a method on Foo.
// The borrow checker ensures we can't store a reference to the underlying
// Foo which will outlive the guard xx.
// x is unlocked when we exit this function and xx's destructor is executed.
}
```
## Motivation
Where a resource must be finalised after use, RAII can be used to do this
finalisation. If it is an error to access that resource after finalisation, then
this pattern can be used to prevent such errors.
## Advantages
Prevents errors where a resource is not finalised and where a resource is used
after finalisation.
## Discussion
RAII is a useful pattern for ensuring resources are properly deallocated or
finalised. We can make use of the borrow checker in Rust to statically prevent
errors stemming from using resources after finalisation takes place.
The core aim of the borrow checker is to ensure that references to data do not
outlive that data. The RAII guard pattern works because the guard object
contains a reference to the underlying resource and only exposes such
references. Rust ensures that the guard cannot outlive the underlying resource
and that references to the resource mediated by the guard cannot outlive the
guard. To see how this works it is helpful to examine the signature of `deref`
without lifetime elision:
```rust
fn deref<'a>(&'a self) -> &'a T { ... }
```
The returned reference to the resource has the same lifetime as `self` (`'a`).
The borrow checker therefore ensures that the lifetime of the reference to `T`
is shorter than the lifetime of `self`.
Note that implementing `Deref` is not a core part of this pattern, it only makes
using the guard object more ergonomic. Implementing a `get` method on the guard
works just as well.
## See also
[Finalisation in destructors idiom](../idioms/dtor-finally.md)
RAII is a common pattern in C++: [cppreference.com](http://en.cppreference.com/w/cpp/language/raii),
[wikipedia](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization).
[Style guide enty](http://doc.rust-lang.org/stable/style/ownership/raii.html)
(currently just a placeholder).

38
patterns/entry.md Normal file
View File

@ -0,0 +1,38 @@
# Entry API
## Description
A short, prose description of the pattern.
## Example
```rust
// An example of the pattern in action, should be mostly code, commented
// liberally.
```
## Motivation
Why and where you should use the pattern
## Advantages
Good things about this pattern.
## Disadvantages
Bad things about this pattern. Possible contraindications.
## Discussion
TODO vs insert_or_update etc.
## See also
[RFC](https://github.com/rust-lang/rfcs/blob/master/text/0216-collection-views.md)

41
patterns/late-bounds.md Normal file
View File

@ -0,0 +1,41 @@
# Late bound bounds
## Description
TODO late binding of bounds for better APIs (i.e., Mutex's don't require Send)
## Example
```rust
// An example of the pattern in action, should be mostly code, commented
// liberally.
```
## Motivation
Why and where you should use the pattern
## Advantages
Good things about this pattern.
## Disadvantages
Bad things about this pattern. Possible contraindications.
## Discussion
A deeper discussion about this pattern. You might want to cover how this is done
in other languages, alternative approaches, why this is particularly nice in
Rust, etc.
## See also
Related patterns (link to the pattern file). Versions of this pattern in other
languages.

View File

@ -7,7 +7,10 @@ A short, prose description of the pattern.
## Example
An example of the pattern in action, should be mostly code, commented liberally.
```rust
// An example of the pattern in action, should be mostly code, commented
// liberally.
```
## Motivation