Functional Programming in Rust

Plan

  • Basics of Rust

  • Traits

  • Generics

  • Error handling

  • Closures

  • Iterators

Rust

  • Designed by Mozilla, released in 2010

  • Multi-paradigm (imperative, functional, concurrent)

  • Static and strongly typed (with type inference)

  • Safe

  • Low-level

  • Automatic memory management (without garbage collector)

  • Servo web engine

Strengths of Rust

  • Zero-cost abstractions

  • Concurrent programming

  • Safe:

    • No null pointers

    • No use after free

    • No use before initialization

    • Immutable variables by default

    • No memory leaks

Hello World!

fn main() {
    println!("Hello World!");     (1)
}
  1. println! is a macro.

Variable

let number: i32 = 42;     (1)
let number = 42;          (2)
let mut number = 24;      (3)
  1. 32-bit signed integer variable

  2. specifying the type is optional thanks to type inference

  3. mutable variable

Structure

struct Person {
    firstname: String,
    lastname: String,
    age: u8,                                   (1)
}

impl Person {
    fn full_name(&self) -> String {            (2) (3)
        format!("{} {}", self.firstname, self.lastname)
    }
}
  1. 8-bit unsigned integer

  2. self is the current instance

  3. & specify that self is passed by reference

Use of structures

let lastname = "Hoare".to_string();
let firstname = "Graydon".to_string();
let person = Person {
    age: 42,
    lastname: lastname,
    firstname,
};

let full_name = person.full_name();

Attribute

#[derive(Clone)]
struct Person {
    firstname: String,
    lastname: String,
    age: u8,
}

Pattern matching

match number {
    1 => println!("one"),
    2 => println!("two"),
    3 | 4 => println!("three or four"),
    5 ... 10 => println!("between 5 and 10"),
    _ => println!("other"),
}

Pattern matching (destructuring)

enum List {
    Empty,
    Cons(i32, Box<List>),
}

fn sum_list(list: &List) -> i32 {
    match *list {
        List::Empty => 0,
        List::Cons(element, ref rest) =>
            element + sum_list(rest),
    }
}

Pattern matching (if)

fn is_empty(list: &List) -> bool {
    if let List::Empty = *list {
        true
    }
    else {
        false
    }
}

Pattern matching (for loop)

let nums = 10..20;
for (index, num) in nums.enumerate() {
    println!("Num {} at index {}", num, index);
}

let nums = 10..20;
for tuple in nums.enumerate() {
    println!("Num {} at index {}", tuple.1, tuple.0);
}

Expressions

let text =
    if number > 42 {                (1)
        "greater than 42"           (2)
    }
    else {
        "lesser than or equal to 42"
    };
  1. if is an expression

  2. no semicolon

Function

fn max(number1: i32, number2: i32) -> i32 {
    if number1 > number2 {
        number1
    }
    else {
        number2
    }
}

Traits

  • Ad-hoc polymorphism (function/operator overloading)

  • A set of methods and types that can be defined for a type

  • Useful for generic functions

  • Very similar to typeclasses in Haskell

  • Similar to interfaces in OO languages

Example of traits

  • Display

  • Debug

  • Eq

  • Ord

  • Add

  • Sub

  • Mul

The Debug trait

#[derive(Debug)]
enum List {
    Empty,
    Cons(Point, Box<List>),
}

use List::{Cons, Empty};
let p1 = Point::new(1, 2);
let p2 = Point::new(2, 3);
let p3 = Point::new(3, 4);
let p4 = Point::new(4, 5);
println!("{:?}", Cons(p1, Box::new(Cons(p2, Box::new(Cons(p3,
    Box::new(Cons(p4, Box::new(Empty)))))))));
Cons(Point { x: 1, y: 2 }, Cons(Point { x: 2, y: 3 }, Cons(Point { x: 3, y: 4 }, Cons(Point { x: 4, y: 5 }, Empty))))

The Debug Trait (continued)

println!("{:#?}", Cons(p1, Box::new(Cons(p2, Box::new(Cons(p3, (1)
    Box::new(Cons(p4, Box::new(Empty)))))))));
  1. Usage of {:#?} instead of {:?}

Cons(
    Point {
        x: 1,
        y: 2
    },
    Cons(
        Point {
            x: 2,
            y: 3
        },
        Cons(
            Point {
                x: 3,
                y: 4
            },
            Cons(
                Point {
                    x: 4,
                    y: 5
                },
                Empty
            )
        )
    )
)

Rules of trait implementations

  • The trait must be in scope to use its methods

  • Either the type or the trait must be defined in the same crate as the implementation

Example

trait Read {
    fn read(string: &str) -> Self;
}

impl Read for i32 {
    fn read(string: &str) -> Self {
        string.parse().unwrap()
    }
}

More about traits

trait Equal {
    fn equal(&self, other: &Self) -> bool;

    fn not_equal(&self, other: &Self) -> bool {       (1)
        !self.equal(other)
    }
}

trait Ordered : Equal {                               (2)
    fn lesser_than_or_equal(&self, other: &Self) -> bool;

    fn lesser_than(&self, other: &Self) -> bool {
        !self.greater_than_or_equal(other)
    }

    fn greater_than(&self, other: &Self) -> bool {
        !self.lesser_than_or_equal(other)
    }

    fn greater_than_or_equal(&self, other: &Self) -> bool {
        self.greater_than(other) || self.equal(other)
    }
}
  1. Default implementation of the method

  2. The types implementing Ordered must also implement Equal

Generics

  • Parametric polymorphism (generic function or type)

  • Way to avoid code duplication

  • Static dispatch when using trait bounds

Attempt at creating a generic function

fn min<T>(a: T, b: T) -> T {
    if a < b {
        a
    }
    else {
        b
    }
}
error[E0369]: binary operation `<` cannot be applied to type `T`
   --> src/main.rs:227:8
    |
227 |     if a < b {
    |        ^^^^^
    |
    = note: an implementation of `std::cmp::PartialOrd` might be missing for `T`

Example

fn min<T: PartialOrd>(a: T, b: T) -> T {       (1)
    if a < b {
        a
    }
    else {
        b
    }
}

println!("{}", min(24, 42));
  1. The type T must implement PartialOrd

Generic type

enum List<T> {
    Empty,
    Cons(T, Box<List<T>>),
}

enum List<T: Eq> {
    Empty,
    Cons(T, Box<List<T>>),
}

enum List<T>
where T: Eq
{
    Empty,
    Cons(T, Box<List<T>>),
}

Setting the generic type

let list = Cons::<u8>(42, Box::new(Empty::<u8>));       (1)

let list: List<u8> = Cons(42, Box::new(Empty));

let tail = Empty;                                       (2)
let list = Cons(42u8, Box::new(tail));
  1. This looks like a fish ::<>

  2. The compiler knows tail has type List<u8> because it is used with an u8 afterwards

Error Handling

  • Option

  • Result

  • Macros

  • No null

  • No exceptions

Option

  • Encode the absence of value in the type itself

  • Cannot access the value directly

  • Like the Maybe type in Haskell

enum Option<T> {
    Some(T),
    None,
}

Example

let vector = vec![1, 2, 3];
let value =
    match vector.first() {
        Some(value) => value + 1,
        None => 0,
    };

Methods

optional.unwrap_or(0)

optional.unwrap_or_default()

optional.map(i32::abs)

optional.map_or(0, i32::abs)

optional.and_then(parse_int)

optional.cloned()

Result

  • Encode the error in the type

  • #[must_use]

  • Like the Either type in Haskell

  • Methods similar to Option

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Example

match "42".parse::<i32>() {
    Ok(value) => println!("{}", value),
    Err(error) => println!("Error: {}", error),
}

The ? operator

  • Get the result or return the error if any

  • Shortcut for pattern matching

  • Useful when you have multiple Result s, especially of different types.

Example

fn sum_string(str1: &str, str2: &str) -> Result<i32, ParseIntError> {
    let num1: i32 = str1.parse()?;
    let num2: i32 = str2.parse()?;
    Ok(num1 + num2)
}
fn sum_string(str1: &str, str2: &str) -> Result<i32, ParseIntError> {
    let num1: i32 =
        match str1.parse() {
            Ok(num) => num,
            Err(error) => return Err(error),
        };
    let num2: i32 =
        match str2.parse() {
            Ok(num) => num,
            Err(error) => return Err(error),
        };
    Ok(num1 + num2)
}

No Exceptions

Panics

  • Panic the thread

    • Unwind the stack (default)

    • Abort

panic!("don't panic");

let string = "rebar";
if string.parse::<i32>().is_err() {
    panic!("Cannot parse: {}", string);
}

Other macros

unimplemented!()

unreachable!()

assert!()

assert_eq!()

assert_ne!()

// (and their `debug_*` variants)

Backtrace (RUST_BACKTRACE=1)

thread 'main' panicked at 'Cannot parse: rebar', src/main.rs:136:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
# with RUST_BACKTRACE=1
thread 'main' panicked at 'Cannot parse: rebar', src/main.rs:136:8
stack backtrace:
# […]
   7: functional_rust::main
             at src/main.rs:136
   8: __rust_maybe_catch_panic
             at /checkout/src/libpanic_unwind/lib.rs:98
   9: std::rt::lang_start
             at /checkout/src/libstd/panicking.rs:459
             at /checkout/src/libstd/panic.rs:361
             at /checkout/src/libstd/rt.rs:61
  10: main
# […]

Closures

  • Function without name

  • Type inference

  • Can have an environment

  • Allocated on the stack by default

Example

let inc_opt = optional.map(|num| num + 1);             (1)

fn sum_string(str1: &str, str2: &str) -> Result<i32, ParseIntError> {
    str1.parse()
        .and_then(|num1: i32| -> Result<i32, _> {      (2) (3)
            let num2: i32 = str2.parse()?;
            Ok(num1 + num2)
        })
}
  1. Closure incrementing a number

  2. Argument and return types

  3. Block of code

Function as a parameter

trait Functor {
    fn fmap<F, O>(&self, func: F) -> O
    where F: Fn(&Self) -> O;                              (1)
}

let two = 2;
println!("{:?}", 40.fmap(|num| (num + two).to_string())); (2)
  1. Generic parameter that must implement this Fn trait

  2. Capture the variable two

Return a closure

fn compose<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C (1)
where F: Fn(A) -> B,
      G: Fn(B) -> C,
{
    move |value| g(f(value))                             (2)
}
  1. This returns the closure without allocating it on the heap.

  2. The keyword move indicates that the environment is moved into the closure

Iterators

  • High-level, but zero cost

  • Adapters, like filter and map

  • Used in for loops

  • Immutability

  • Iterator trait

Example

let string = b"RustConf, August 18-19 | Portland";
let string: String =                                (1) (2)
    string.iter()                                   (3)
        .map(|byte| byte.to_ascii_uppercase())
        .take_while(|&byte| byte != b'|')           (4)
        .map(|byte| byte as char)
        .filter(|&ch| ch.is_alphanumeric())
        .collect();
  1. Immutable variable

  2. Type annotation for collect()

  3. Get an iterator

  4. Pattern match against &byte

Example with for loop

for (&ch, index) in string.iter().zip(0..).skip(5) {    (1)
    if index < 20 {
        print!("{}", ch as char);
    }
}
  1. 0.. is an infinite Range

cover

ISBN: 9781788390637

Questions?