A Dabbler's Adventures in Rust

Tony Aldridge - @angry_lawyer

Functional Brighton

21st January 2014

Presenter Notes

What is Rust?

(Baby don't hurt me, don't hurt me, no more)

  • New programming language developed by Mozilla
  • Multi-paradigm. Functional, imperative, object-oriented, whenever it makes sense.
  • Low-level. Targets the same problem-space as C and C++
  • Safe. Lovely, lovely types and pointer lifetimes guard against a lot of errors.

rust

Presenter Notes

Where can I get it?

Presenter Notes

The traditional Hello World

1 use std::io::stdio::println;
2 
3 fn main() {
4     println("Hello!")
5 }
  • It's very C-like. Curly braced. Imported packages at the top of the page.
  • Semicolons aren't needed in a lot of cases. They imply 'new statement' rather than end of line, so if you want your function to return a value, you leave it off (or use the return keyword).

Presenter Notes

The basics

Rust is staticly typed, but uses type inference to make sure we don't end up with Java-style mouthfuls like 'Integer myInteger = new Integer(5)'

1     let a = 5 + 4;
2     let b = a + 2;

You can specify the type if you want, which comes in handy sometimes.

1     let c: int = 7;

Presenter Notes

No automatic type casts

Rust, in order to protect ourselves from accidental casts, makes it so you have to manually convert it

1     let a = 2;
2     /*
3     let b = a * 0.1; //Won't compile
4     */

As you can see, if we want to multiply an int by a float, we have to make sure that it typechecks.

1     let b = a as f64 * 0.1;

Presenter Notes

Immutability

As Rust is designed with concurrency as a goal, everything's immutable by default.

1     let a = 1.0;
2     /*
3     a += 1.0 // Won't compile
4     */

We have to explicitly make things mutable if we want to change their value.

1     let mut a = 5;
2     a += 1;

Variables can be shadowed by defining them again

1     let a = 1;
2     let a = 5;

Presenter Notes

Expressions

Everything's an expression in Rust. If you put a semicolon after it, it'll return void instead. This makes for some fun, like returning values from 'if' statements.

1     let a = if (b > 0) {
2         "Positive"
3     } else {
4         "Negative"
5     };

We also get lovely, lovely pattern matching.

1     let c = match b {
2         3 | 5 => "Lol",
3         _ => "Not lol"
4     };

Presenter Notes

Functions

Functions are one of the places you have to be explicit with types

1     //Functions must specify the types they use
2     fn increment (a: uint) -> uint {
3         a + 1
4     };
5 
6     let a = 1;
7     let b = increment(a);

As you can see, the result of the last expresison is returned. You can force a return with the 'return' keyword, too.

Presenter Notes

Functions

Functions are first-class, so we can pass them in to others, and return them. Here, we're taking a tuple of three uints and a function that acts on a uint, and returning a three-tuple with that function applied to all its members

1     fn munge_triple((a, b, c): (uint, uint, uint), 
2         munger: |a: uint| -> uint) -> (uint, uint, uint) {
3         (munger(a), munger(b), munger(c))
4     };
5 
6     let c = munge_triple((1, 2, 3), increment);
7     //There's also shorthand
8     let c = munge_triple((1, 2, 3), |a| a+1);

Presenter Notes

Generics

Our previous function was a bit naff - we can only operate on three-tuples made of uints, and they always output the same type. There must be a better way...

In step generics to save the day!

1     fn scrunge_triple<T, U>((a, b, c): (T, T, T),
2         munger: |a: T| -> U) -> (U, U, U) {
3        (munger(a), munger(b), munger(c))
4     };

Here, we provide type parameters for the input and output, so they can be different, and we don't really care what they are as long as they match what the function we're passing wants. The compler works out what we need.

Presenter Notes

Generics

So, lets see it in action!

 1     let a = scrunge_triple((1, 2, 3), |a| a + 1);
 2     //Success, because T and U are both substituted with uint
 3 
 4     let b = scrunge_triple((1, 2, 3), |a| a as f64 * 0.1);
 5     //Success, because T is uint, and U is f64
 6 
 7     //let c = munge_triple(1, 2.0, 3), |a| a * 0.1);
 8 
 9     //Fails the typecheck as all the tuple elements
10     //were marked as the same

Presenter Notes

Basic data types

Rust gives us lots of lovely ways of structuring data. Enter types.

 1     //We have structs, which are like their C equivalent.
 2     struct Llama {
 3         hairiness: uint
 4     };
 5 
 6     let jose = Llama{hairiness: 5};
 7 
 8     //We also have tuples
 9     let tuple = (5, "lol", 7);
10 
11     //And a typed list-like structure called vectors
12     let my_list = ["first", "second", "third"];
13 
14     //Finally, we have enums, which we'll take a closer look at later
15     enum Colors {
16         Red,
17         Blue,
18         Green
19     };
20 
21     let myColor = Red;

Presenter Notes

Pointers

Pointers are a key part of understanding Rust. By default, everything goes on the stack, and therefore is freed after it falls out of scope.

1     struct Kitty {
2         fluffiness: uint
3     };
4 
5     fn new_kitty(fluffiness: uint) -> Kitty {
6         Kitty{fluffiness: fluffiness}
7     };
8 
9     let buffcat = new_kitty(2); //On the stack

beanbag

Presenter Notes

Pointers

We can define Owned pointers allocated on the heap, and freed when the pointer goes out of scope

1     let tom = ~new_kitty(3);
2     //They're guaranteed unique - you can move them
3     let moved_tom = tom;
4     //The following won't compile, as it no longer lives in that slot
5     //let fluff = tom.fluffiness;

We can also create borrowed pointers, which can only legally exist while the original does, making it impossible to accidentally dereference a cleaned-up object. The compiler makes sure this is all valid.

1     let borrowed_kitty = &buffcat;
2     //We can still use the original
3     let is_the_same = borrowed_kitty.fluffiness == buffcat.fluffiness;

Presenter Notes

Algebraic data types

Algebraic data types are one of the most useful features in Rust.

1     enum MaybeInt {
2         SomeInt(int),
3         NoneInt
4     };
5 
6     let maybe_number = SomeInt(5);
7     let no_number = NoneInt;

Here, we have a type that can either be boxing an int, or representing nothing. We can also use generics to make it a bit more useful.

1     enum Option<T> {
2         Some(T),
3         None
4     };

Presenter Notes

No Nulls!

Rust doesn't allow us to use Nulls at all; they're a common source of frustration and error in languages that have them.

Instead, we use the Option type, as we've defined it (it's also a built-in, so real Rust code doesn't need to bother defining it)

 1     fn safe_divide(a: int, b: int) -> Option<f64> {
 2         if b == 0 {
 3             None
 4         } else {
 5             Some(a as f64 / b as f64)
 6         }
 7     }
 8 
 9     //We can then unpack them with pattern matching
10     match safe_divide(5, 0) {
11         Some(x) => "Successfully divided",
12         None => "Divide by zero"
13     };

Presenter Notes

Functional fun

So, let's do the classic of Functional Programming. Define a list type.

1     enum List<T> {
2         Node(T,~List<T>),
3         Terminal
4     };

It's recursively defined as data plus an owned pointer to another List, or as a terminal

catlist

Presenter Notes

Functional fun

So, now we can define a list!

 1     let list = ~Node(1, ~Node(2, ~Node(3, ~Terminal)));
 2 
 3     fn double_list(item: &List<uint>) -> ~List<uint> {
 4         match item {
 5             &Node(ref value, ref next) => {
 6                 ~Node(value*2, double_list(*next))
 7             },
 8             &Terminal => {
 9                 ~Terminal
10             }
11         }
12     }
13     let squared = double_list(list);

This function borrows the original list, so the original remains useful. In the pattern match, we have to specify that we're borrowing rather than moving once again, otherwise no compile.

We then have to dereference the borrowed pointer with a * otherwise it's a double borrow

Presenter Notes

Functional fun

But why would we hardcode what this can do? We can take a first class function, and also make it generic across all types!

 1     fn map<T, U>(item: &List<T>, action: |&T| -> U) -> ~List<U> {
 2         match item {
 3             &Node(ref value, ref next) => {
 4                 ~Node(action(value), map(*next, action))
 5             },
 6             &Terminal => {
 7                 ~Terminal
 8             }
 9         }
10     }
11 
12     //And now we can use it as normal
13     let squared = map(list, |value: &uint| { *value * *value });

Of course, Rust's standard library has already defined all of these useful things.

Presenter Notes

What I've not covered

Rust has a lot of other awesome features that I barely understand.

  • Defining lifetimes, so you can return borrowed pointers
  • Traits, for object oriented programming, and attaching methods to any type you want.
  • Traits as predicates on types
  • Concurrency

Presenter Notes

Any questions?

Presenter Notes