Hands-On Functional Programming in RUST

4 (1 reviews total)
By Andrew Johnson
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Functional Programming – a Comparison

About this book

Functional Programming allows developers to divide programs into smaller, reusable components that ease the creation, testing, and maintenance of software as a whole. Combined with the power of Rust, you can develop robust and scalable applications that fulfill modern day software requirements. This book will help you discover all the Rust features that can be used to build software in a functional way.

We begin with a brief comparison of the functional and object-oriented approach to different problems and patterns. We then quickly look at the patterns of control flow, data the abstractions of these unique to Functional Programming. The next part covers how to create functional apps in Rust; mutability and ownership, which are exclusive to Rust, are also discussed. Pure functions are examined next and you'll master closures, their various types, and currying. We also look at implementing concurrency through functional design principles and metaprogramming using macros. Finally, we look at best practices for debugging and optimization.

By the end of the book, you will be familiar with the functional approach of programming and will be able to use these techniques on a daily basis.

Publication date:
May 2018
Publisher
Packt
Pages
249
ISBN
9781788839358

 

Chapter 1. Functional Programming – a Comparison

Functional programming (FP) is the second most popular programming paradigm, behind only object-oriented programming (OOP). For many years, these two paradigms have been separated into different languages, so as not to be mixed. Multi-paradigm languages have attempted to support both approaches. Rust is one such language.

As a broad definition, functional programming emphasizes the use of composable and maximally reusable functions to define program behavior. Using these techniques, we will show how functional programming has adapted clever solutions to many common yet difficult problems. This chapter will outline most of the concepts presented in this book. The remaining chapters will be dedicated to helping you master each technique.

The learning outcomes we hope to provide are as follows:

  • Being able to use functional style to reduce code weight and complexity
  • Being able to write robust safe code by utilizing safe abstractions
  • Being able to engineer complex projects using functional principles
 

Technical requirements


A recent version of Rust is necessary to run the examples provided, and can be found here:

https://www.rust-lang.org/en-US/install.html

This chapter's code is also available on GitHub, here:

https://github.com/PacktPublishing/Hands-On-Functional-Programming-in-RUST

Specific installation and build instructions are also included in each chapter's README.md file.

 

Reducing code weight and complexity


Functional programming can greatly reduce the amount and complexity of code required to accomplish tasks. Particularly in Rust, proper application of functional principles may simplify the often complex design requirements, and make programming a much more productive and rewarding experience.

Making generics more generic

Making generics more generic relates to the practice of parameterizing data structures and functions originated in functional languages. In Rust, and other languages, this is called generics. Types and functions can all be parameterized. One or more constraints may be placed on generic types to indicate requirements of a trait or lifetime.

Struct definitions can become redundant without generics. Here is a definition of three structs that define a common concept of a Point. However, the structs use different numerical types, so the singular concept is expanded into three separate PointN type definitions in intro_generics.rs:

struct PointU32 
{
    x: u32,
    y: u32
}

struct PointF32
{
    x: f32,
    y: f32
}

struct PointI32
{
    x: i32,
    y: i32
}

Instead, we can use generics to remove duplicate code and make the code more robust. Generic code is more easily adaptable to new requirements because many behaviors (and thus requirements) can be parameterized. If a change is needed, it is better to only change one line rather than a hundred.

This code snippet defines a parameterized Point struct. Now, a single definition can capture all possible numerical types for a Point in intro_generics.rs:

struct Point<T>
{
    x: T,
    y: T
}

Functions are also problematic without generics.

Here is a simple function to square a number. However, to capture possible numerical types, we define three different functions in intro_generics.rs:

fn foo_u32(x: u32) -> u32
{
    x*x
}

fn foo_f32(x: f32) -> f32
{
    x*x
}

fn foo_i32(x: i32) -> i32
{
    x*x
}

Function parameters, such as this one, may need trait bounds (a constraint specifying one or more traits) to permit any behavior on that type that is used in the function body.

Here is the foo function, redefined with a parameterized type. A single function can define the operation for all numerical types. Explicit bounds must be set for even basic operations, such as multiply or even copy, in intro_generics.rs:

fn foo<T>(x: T) -> T
where T: std::ops::Mul<Output=T> + Copy
{
    x*x
}

Even functions can be sent as parameters. We call these higher-order functions.

Here is a trivial function that accepts a function and argument, then calls the function with the argument, returning the result. Note the trait bound  Fn, indicating that the provided function is a closure. For an object to be callable, it must implement one of the fnFn, FnMut, or FnOnce traits in intro_generics.rs:

fn bar<F,T>(f: F, x: T) -> T
where F: Fn(T) -> T
{
    f(x)
}

Functions as values

Functions are nominally the big feature of functional programming. Specifically, functions as values are the keystone of the whole paradigm. Glossing over much detail, we will also introduce the term closure here for future reference. A closure is an object that acts as a function, implementing fnFn, FnMut, or FnOnce.

Simple closures can be defined with the built-in closure syntax. This syntax is also beneficial because the fnFn, FnMut, and FnOnce traits are automatically implemented if permitted. This syntax is great for shorthand manipulation of data.

Here is an iterator over the range 0 to 10, mapped to the squared value. The square operation is applied using an inline closure definition sent to the map function of the iterator. The result of this expression will be an iterator. Here is an expression in intro_functions.rs:

(0..10).map(|x| x*x);

Closures can also have complex bodies with statements if the block syntax is used.

Here is an iterator from 0 to 10, mapped with a complex equation. The closure provided to map includes a function definition and a variable binding in intro_functions.rs:

(0..10).map(|x| {
    fn f(y: u32) -> u32 {
        y*y
    }
    let z = f(x+1) * f(x+2);
    z*z
}

It is possible to define functions or methods that accept closures as arguments. To use the closure as a callable function, a bound of Fn, FnMut, or FnOnce must be specified.

Here is a HoF definition accepting a function g and an argument x. The definition constrains g and x to process u32 types, and defines some mathematical operations involving calls to g. An invocation of the f HoF is also provided, as follows, using a simple inline closure definition in intro_functions.rs:

fn f<T>(g: T, x: u32) -> u32
where T: Fn(u32) -> u32
{
    g(x+1) * g(x+2)
}

fn main()
{
   f(|x|{ x*x }, 2);
}

Many parts of the standard library, particularly iterators, encourage heavy use of functions as arguments.

Here is an iterator from 0 to 10 followed by many chained iterator combinators. The map function returns a new value from an original. inspect looks at a value, does not change it, but permits side-effects. filter omits all values that do not satisfy a predicate. filter_map filters and maps with a single function. The fold reduces all results to a single value, starting from an initial value, working left to right. Here is the expression in intro_functions.rs:

(0..10).map(|x| x*x)
       .inspect(|x|{ println!("value {}", *x) })
       .filter(|x| *x<3)
       .filter_map(|x| Some(x))
       .fold(0, |x,y| x+y);

Iterators

Iterators are a common feature of OOP languages, and Rust supports this concept well. Rust iterators are also designed with functional programming in mind, allowing programmers to write more legible code. The specific concept emphasized here is composability. When iterators can be manipulated, transformed, and combined, the mess of for loops can be replaced by individual function calls. These examples can be found in the intro_iterators.rs file. This is depicted in the following table:

Function name with description

Example

Chain concatenates two iterators: first...second

(0..10).chain(10..20);

The zip function combines two iterators into tuple pairs, iterating until the end of the shortest iterator: (a1,b1), (a2, b2), ...

(0..10).zip(10..20);

The enumerate function is a special case of zip that creates numbered tuples (0, a1),(1,a2), …

(0..10).enumerate();

The inspect function applies a function to all values in the iterator during iteration

(0..10).inspect(|x|{ println!("value {}", *x) });

The map function applies a function to each element, returning the result in place

(0..10).map(|x| x*x);

The filter function restricts elements to those satisfying a predicate

(0..10).filter(|x| *x<3);

The fold function accumulates all values into a single result

(0..10).fold(0, |x,y| x+y);

When you want to apply the iterator, you can use a for loop or call collect

for i in (0..10) {}(0..10).collect::<Vec<u64>>();

Compact legible expressions

In functional languages, all terms are expressions. There are no statements in function bodies, only a single expression. All control flow operators are then formulated as expressions with a return value. In Rust, this is almost the case; the only non-expressions are let statements and item declarations.

Both of these statements can be wrapped in blocks to create an expression along with any other term. An example for this is the following, in intro_expressions.rs:

let x = {
    fn f(x: u32) -> u32 {
        x * x
    }
    let y = f(5);
    y * 3
};

This nested format is uncommon in the wild, but it illustrates the permissive nature of Rust grammar.

Returning to the concept of functional style expressions, the emphasis should always be on writing legible literate code without much hassle or bloat. When someone else, or you at a later time, comes to read your code, it should be immediately understandable. Ideally, the  code should document itself. If you find yourself constantly writing code twice, once in code and again as comments, then you should reconsider how effective your programming practices really are.

To start with some examples of functional expressions, let's look at an expression that exists in most languages, the ternary conditional operator. In a normal if statement, the condition must occupy its own line and thus cannot be used as a sub-expression.

The following is a traditional if statement, initializing a variable in intro_expressions.rs:

let x;
if true {
    x = 1;
} else {
    x = 2;
}

With the ternary operator, this assignment can be moved to a single line, shown as follows in intro_expressions.rs:

let x = if true { 1 } else { 2 };

Almost every statement from OOP in Rust is also an expression—if, for, while, and so on. One of the more unique expressions to see in Rust that is uncommon in OOP languages is direct constructor expressions. All Rust types can be instantiated by single expressions. Constructors are only necessary in specific cases, for example, when an internal field requires complex initialization. The following is a simple struct and an equivalent tuple in intro_expressions.rs:

struct MyStruct
{
    a: u32,
    b: f32,
    c: String
}

fn main()
{
    MyStruct {
        a: 1,
        b: 1.0,
        c: "".to_string()
    };

    (1, 1.0, "".to_string());
}

Another distinctive expression from functional languages is pattern matching. Pattern matching can be thought of as a more powerful version of a switch statement. Any expression can be sent into a pattern expression and de-structured to bind internal information into local variables before executing a branch expression. Pattern expressions are uniquely suited for working with enums. The two make a perfect pair.

The following snippet defines a Term as a tagged union of expression options. In the main function, a Termt is constructed, then matched with a pattern expression. Note the syntax similarity between the definition of a tagged union and the matching inside of a pattern expression in intro_expressions.rs:

enum Term
{
    TermVal { value: String },
    TermVar { symbol: String },
    TermApp { f: Box<Term>, x: Box<Term> },
    TermAbs { arg: String, body: Box<Term> }
}

fn main()
{
    let mut t = Term::TermVar {
        symbol: "".to_string()
    };
    match t {
        Term::TermVal { value: v1 } => v1,
        Term::TermVar { symbol: v1 } => v1,
        Term::TermApp { f: ref v1, x: ref v2 } =>
            "TermApp(?,?)".to_string(),
        Term::TermAbs { arg: ref mut v1, body: ref mut v2 } =>  
            "TermAbs(?,?)".to_string()
    };
}
 

Strict abstraction means safe abstraction


Having a stricter type system does not imply that code will have more requirements or be any more complex. Rather than strict typing, consider using the term expressive typing. Expressive typing provides more information to the compiler. This extra information allows the compiler to provide extra assistance while programming. This extra information also permits a very rich metaprogramming system. This is all in addition to the obvious benefit of safer, more robust code.

Scoped data binding

Variables in Rust are treated much more strictly than in most other languages. Global variables are almost entirely disallowed. Local variables are put under close watch to ensure that allocated data structures are properly deconstructed before going out of scope, but not sooner. This concept of tracking a variable's proper scope is known as ownership and lifetime.

In a simple example, data structures that allocate memory will deconstruct automatically when they go out of scope. No manual memory management is required in intro_binding.rs:

fn scoped() {
    vec![1, 2, 3];
}

In a slightly more complex example, allocated data structures can be passed around as return values, or referenced, and so on. These exceptions to simple scoping must also be accounted for in intro_binding.rs:

fn scoped2() -> Vec<u32>
{
    vec![1, 2, 3]
}

This usage tracking can get complicated (and undecidable), so Rust has some rules that restrict when a variable can escape a context. We call this complex rules ownership. It can be explained with the following code, in intro_binding.rs:

fn scoped3()
{
    let v1 = vec![1, 2, 3];
    let v2 = v1;
    //it is now illegal to reference v1
    //ownership has been transferred to v2
}

When it is not possible or desirable to transfer ownership, the clone trait is encouraged to create a duplicate copy of whatever data is referenced in intro_binding.rs:

fn scoped4()
{
    vec![1, 2, 3].clone();
    "".to_string().clone();
}

Cloning or copying is not a perfect solution, and comes with a performance overhead. To make Rust faster, and it is pretty fast, we also have the concept of borrowing. Borrowing is a mechanism to receive a direct reference to some data with the promise that ownership will be returned by some specific point. References are indicated by an ampersand. Consider the following example, in intro_binding.rs:

fn scoped5()
{
   fn foo(v1: &Vec<u32>)
   {
       for v in v1
       {
           println!("{}", v);
       }
   }

   let v1 = vec![1, 2, 3];
   foo(&v1);

   //v1 is still valid
   //ownership has been returned
   v1;
}

Another benefit of strict ownership is safe concurrency. Each binding is owned by a particular thread, and that ownership can be transferred to new threads with the move keyword. This has been explained with the following code, in intro_binding.rs:

use std::thread;

fn thread1()
{
   let v = vec![1, 2, 3];

   let handle = thread::spawn(move || {
      println!("Here's a vector: {:?}", v);
   });

   handle.join().ok();
}

To share information between threads, programmers have two main options.

First, programmers may use the traditional combination of locks and atomic references. This is explained with the following code, in intro_binding.rs:

use std::sync::{Mutex, Arc};
use std::thread;

fn thread2()
{

   let counter = Arc::new(Mutex::new(0));
   letmut handles = vec![];

   for _ in0..10 {
      let counter = Arc::clone(&counter);
      let handle = thread::spawn(move || {
         letmut num = counter.lock().unwrap();
         *num += 1;
      });
      handles.push(handle);
   }

   for handle in handles {
      handle.join().unwrap();
   }

   println!("Result: {}", *counter.lock().unwrap());
}

Second, channels provide a nice mechanism for message passing and job queuing between threads. The send trait is also implemented automatically for most objects. Consider the following code, in intro_binding.rs:

use std::thread;
use std::sync::mpsc::channel;

fn thread3() {

    let (sender, receiver) = channel();
    let handle = thread::spawn(move ||{

        //do work
        let v = vec![1, 2, 3];
        sender.send(v).unwrap();

    });

    handle.join().ok();
    receiver.recv().unwrap();
}

All of this concurrency is type-safe and compiler-enforced. Use threads as much as you want, and if you accidentally try to create a race condition or simple deadlock, then the compiler will stop you. We call this fearless concurrency.

Algebraic datatypes

In addition to structs/objects and functions/methods, Rust functional programming includes some rich additions to definable types and structures. Tuples provide a shorthand for defining simple anonymous structs. Enums provide a type-safe approach to unions of complex data structures with the added bonus of a constructor tag to help in pattern matching. The standard library has extensive support for generic programming, from base types to collections. Even the object system traits are a hybrid cross between the OOP concept of a class and the FP concept of type classes. Functional style lurks around every corner, and even if you don't seek them in Rust, you will probably find yourself unknowingly using the features.

The type aliases can be helpful to create shorthand names for complex types. Alternatively, the newtype struct pattern can be used to create an alias with different non-equivalent types. Consider the following example, in intro_datatypes.rs:

//alias
type Name = String;

//newtype
struct NewName(String);

A struct, even when parameterized, can be repetitive when used simply to store multiple values into a single object. This can be seen in intro_datatypes.rs:

struct Data1
{
    a: i32,
    b: f64,
    c: String
}

struct Data2
{
    a: u32,
    b: String,
    c: f64
}

A tuple helps eliminate redundant struct definitions. No prior type definitions are necessary to use tuples. Consider the following example, in intro_datatypes.rs:

//alias to tuples
type Tuple1 = (i32, f64, String);
type Tuple2 = (u32, String, f64);

//named tuples
struct New1(i32, f64, String);
struct New2(u32, String, f64);

Standard operators can be implemented for any type by implementing the correct trait. Consider the following example for this, in intro_datatypes.rs:

use std::ops::Mul;

struct Point
{
    x: i32,
    y: i32
}

impl Mul for Point
{
    type Output = Point;
    fn mul(self, other: Point) -> Point
    {
        Point
        {
            x: self.x * other.x,
            y: self.y * other.y
        }
    }
}

Standard library collections and many other built-in types are generic, such as HashMap in intro_datatypes.rs:

use std::collections::HashMap;

type CustomHashMap = HashMap<i32,u32>;

Enums are a type-safe union of multiple types. Note that recursive enum definitions must wrap the inner value in a container such as Box, otherwise the size would be infinite. This is depicted as follows, in intro_datatypes.rs:

enum BTree<T>
{
Branch { val:T, left:Box<BTree<T>>, right:Box<BTree<T>> },
    Leaf { val: T }
}

Tagged unions are also used for more complex data structures. Consider the following code, in intro_datatypes.rs:

enum Term
{
    TermVal { value: String },
    TermVar { symbol: String },
    TermApp { f: Box<Term>, x: Box<Term> },
    TermAbs { arg: String, body: Box<Term> }
}

Traits are a bit like object classes (OOP), shown with the following code example, in intro_datatypes.rs:

trait Data1Trait
{
    //constructors
    fn new(a: i32, b: f64, c: String) -> Self;

    //methods
    fn get_a(&self) -> i32;
    fn get_b(&self) -> f64;
    fn get_c(&self) -> String;
}

Traits are also like type classes (FP), shown with the following code snippet, in intro_datatypes.rs:

trait BehaviorOfShow
{
    fn show(&self) -> String;
}

Mixing object-oriented programming and functional programming

As mentioned before, Rust supports much of both object-oriented and functional programming styles. Datatypes and functions are neutral to either paradigm. Traits specifically support a hybrid blend of both styles.

First, in an object-oriented style, defining a simple class with a constructor and some methods can be accomplished with a struct, trait, and impl. This is explained using the following code snippet, in intro_mixoopfp.rs:

struct MyObject
{
    a: u32,
    b: f32,
    c: String
}

trait MyObjectTrait
{
    fn new(a: u32, b: f32, c: String) -> Self;
    fn get_a(&self) -> u32;
    fn get_b(&self) -> f32;
    fn get_c(&self) -> String;
}

impl MyObjectTrait for MyObject
{
    fn new(a: u32, b: f32, c: String) -> Self
    {
        MyObject { a:a, b:b, c:c }
    }

    fn get_a(&self) -> u32
    {
        self.a
    }

    fn get_b(&self) -> f32
    {
        self.b
    }

    fn get_c(&self) -> String
    {
        self.c.clone()
    }
}

Adding support for functional programming onto an object is as simple as defining traits and methods that use functional language features. For example, accepting a closure can become a great abstraction when used appropriately. Consider the following example, in intro_mixoopfp.rs:

trait MyObjectApply
{
    fn apply<F,R>(&self, f:F) -> R
    where F: Fn(u32,f32,String) -> R;
}

impl MyObjectApply for MyObject
{
    fn apply<F,R>(&self, f:F) -> R
    where F: Fn(u32,f32,String) -> R
    {
        f(self.a, self.b, self.c.clone())
    }
}
 

Improving project architecture


Functional programs encourage good project architecture and principled design patterns. Using the building blocks of functional programming often reduces the number of design choices to be made in such a way that good options become obvious.

"There should be one - and preferably only one - obvious way to do it."

                                                                                                                         –PEP 20

File hierarchy, modules, and namespace design

Rust programs are compiled primarily in one of two ways. The first is to use rustc to compile individual files. The second is to describe an entire package for compilation using cargo. We will assume here that projects are built using cargo, as follows:

  1. To start a package, you first create a Cargo.toml file in a directory. That directory will be your package directory from now on. This is a configuration file that will tell the compiler what code, assets, and extra information should be included into the package:
[package]
name = "fp_rust"
version = "0.0.1"
  1. After this basic configuration, you can now use cargo build to compile the entire project. Where you decide to place your code files, and what to name them, is determined by how you want to refer to them in the module namespace. Each file will be given its own module mod. You can also nest modules inside files:
mod inner_module
{
    fn f1()
    {
        println!("inner module function");
    }
}
  1. After these steps, projects can then be added as cargo dependencies, and namespaces can be used inside of modules to expose public symbols. Consider the following code snippet:
extern crate package;
use package::inner_module::f1;

These are the basic building blocks of Rust modules, but what does this have to do with functional programming?

Architecting a project in functional style is a process, and lends itself to certain routines. Typically, the project architect will start by designing the core data structures and in complex cases also the physical structure (where code/services will operationally be run). Once the data layout has been outlined in sufficient detail, then core functions/routines can be planned (such as how the program behaves). Up to this point, there may be code left unimplemented if coding is happening during the architecting stage. The final stage involves replacing this mock code with correct behaviors.

Following this stage-by-stage development process, we can also see an archetypical file layout forming. It is common to see these stages written top to bottom in actual programs. Though it is unlikely the authors went through planning in these explicit stages, it still is a common pattern due to simplicity's sake. Consider the following example:

//trait definitions

//data structure and trait implementations

//functions

//main

Grouping definitions like this may be helpful to standardize file layout and improve readability. Searching back and forth through a long file for symbol definitions is a common but unpleasant part of programming. It is also a preventable problem.

Functional design patterns

Aside from file layout, there are a number of functional design patterns that help reduce code weight and redundancy. When used properly, these principles can help clarify design decisions and also enable robust architecture. Most design patterns are variants of the single responsibility principle. This can take many forms depending on the context, but the intent is the same; write code that does one thing well, then reuse that code as needed. I have explained this as follows:

  • Pure functions: These are functions with no side effects or logical dependencies other than function arguments. A side effect is a change of state that affects anything outside of the function, aside from the return value. Pure functions are useful because they can be tossed around and combined and generally used carelessly without the risk of unintended effects.

Note

The worst thing that can go wrong with a pure function is a bad return value or, in extreme cases, a stack overflow.

It is harder to cause bugs with pure functions, even when used recklessly. Consider the following example of pure functions, in intro_patterns.rs:

fn pure_function1(x: u32) -> u32
{
    x * x
}

fn impure_function(x: u32) -> u32
{
    println!("x = {}", x);
    x * x
}
  • Immutability: Immutability is a pattern that helps encourage pure functions. Rust variable bindings are immutable by default. This is Rust's not-so-subtle way of encouraging you to avoid mutable state. Don't do it. If you absolutely must, it is possible to tag variables with themut keyword to allow reassignment. This is shown with the following example, in intro_patterns.rs:
let immutable_v1 = 1;
//immutable_v1 = 2; //invalid

let mut mutable_v2 = 1;
mutable_v2 = 2;
  • Functional composition: Functional composition is a pattern where the output of one function is connected to the input of another function. In this fashion, functions can be chained together to create complex effects from simple steps. This is shown with the following code snippet, in intro_patterns.rs:
let fsin = |x: f64| x.sin();
let fabs = |x: f64| x.abs();

//feed output of one into the other
let transform = |x: f64| fabs(fsin(x));
  • Higher-order functions: These have already been mentioned before, but we haven't used the term yet. A HoF is a function that accepts a function as a parameter. Many iterator methods are HoFs. Consider the following example, in intro_patterns.rs:
fn filter<P>(self, predicate: P) -> Filter<Self, P>
where P: FnMut(&Self::Item) -> bool
{ ... }
  • Functors: If you can get past the name, these are a simple and effective design pattern. They are also very versatile. The concept is somewhat difficult to capture in its entirety, but you may think of functors as the inverse of functions. A function defines a transformation, accepts data, and returns the result of the transformation. A functor defines data, accepts a function, and returns the result of the transformation. A common example of a functor is the bound map method that frequently appears on containers, such as for a Vec. Here is an example, in intro_patterns.rs:
let mut c = 0;
for _ in vec!['a', 'b', 'c'].into_iter()
   .map(|letter| {
      c+=1; (letter, c)
   }){};

"A monad is a monoid in the category of endofunctors, what's the problem?"

                                                                                                – Philip Wadler

  • Monads: Monads are a common stumbling block for people learning FP. Monads and functors are maybe the first words that you may encounter on a journey that goes deep into theoretical mathematics. We won't go there. For our purposes, monads are simply a trait with two methods. This is shown in the following code, in intro_patterns.rs:
trait Monad<A> {
    fn return_(t: A) -> Self;
    //:: A -> Monad<A>

    fn bind<MB,B>(m: Self, f: Fn(A) -> MB) -> MB
    where MB: Monad<B>;
    //:: Monad<A> -> (A -> Monad<B>) -> Monad<B>
}

If that doesn't help clarify things (and it probably doesn't), a monad has two methods. The first method is the constructor. The second method lets you bind an operation to create another monad. Many common traits have hidden semi-monads but, by making the concept explicit, the concept becomes a strong design pattern instead of a messy anti-pattern. Don't try to reinvent what you don't have to.

  • Function currying: Function currying is a technique that may seem strange for anyone coming from a background in object-oriented or imperative languages. The reason for this confusion is that in many functional languages, functions are curried by default, whereas this is not the case for other languages. Rust functions are not curried by default.

The difference between curried and non-curried functions are that curried functions send in parameters one by one, whereas non-curried functions send in parameters all at once. Looking at a normal Rust function definition, we can see that it is not curried. Consider the following code, in intro_patterns.rs:

fn not_curried(p1: u32, p2: u32) -> u32
{
    p1 + p2
}

fn main()
{
   //and calling it
   not_curried(1, 2);
}

A curried function takes each parameter one by one, as shown in the following, in intro_patterns.rs:

fn curried(p1: u32) -> Box<Fn(u32) -> u32>
{
    Box::new(move |p2: u32| {
        p1 + p2
    })
}

fn main()
{
   //and calling it
   curried(1)(2);
}

Curried functions can be used as a function factory. The first few arguments configure how the final function should behave. The result is a pattern that allows shorthand configuration of complex operators. Currying complements all the other design patterns by converting individual functions into multiple components.

  • Lazy evaluation: Lazy evaluation is a pattern that is technically possible in other languages. However, it is uncommon to see it outside of FP, due to language barriers. The difference between a normal expression and a lazy expression is that a lazy expression will not be evaluated until accessed. Here is a simple implementation of laziness, implemented behind a function call in intro_patterns.rs:
let x = { println!("side effect"); 1 + 2 };

let y = ||{ println!("side effect"); 1 + 2 };

The second expression will not be evaluated until the function is called, at which point the code resolves. For lazy expressions, side effects happen at time of resolution instead of at initialization. This is a poor implementation of laziness, so we will go into further detail in later chapters. The pattern is fairly common, and some operators and data structures require laziness to work. A simple example of necessary laziness is a lazy list that may not otherwise be possible to create. The built-in Rust numerical iterator (lazy list) uses this well: (0..).

Memoization is the last pattern that we will introduce here. It may be considered as more of an optimization than design pattern, but due to how common it is, we should mention it here. A memoized function only computes unique results once. A simple implementation would be a function guarded by a hash table. If the parameters and result are already in the hash table, then skip the function call and directly return the result from the hash table. Otherwise, compute the result, put it in the hash table, and return. This process can be implemented manually in any language, but Rust macros allow us to write the memoization code once, and reuse that code by applying this macro. This is shown using the following code snippet, in intro_patterns.rs:

#[macro_use] extern crate cached;
#[macro_use] extern crate lazy_static;

cached! {
    FIB;
    fn fib(n: u64) -> u64 = {
        if n==0 || n==1 { return n }
        fib(n-1) + fib(n-2)
    }
}

fn main()
{
   fib(30);
}

This example makes use of two crates and many macros. We won't fully explain everything that is happening here until the very end of this book. There is much that is possible with macros and metaprogramming. Caching function results is just a start.

Metaprogramming

The term metaprogramming in Rust often overlaps with the term macros. There are two primary types of macros available in Rust:

  • Recursive
  • Procedural

Both types of macros take as input an abstract syntax tree (AST), and produce one or more AST.

A commonly used macro is println. A variable number of arguments and types are joined with the format string through the use of a macro to produce formatted output. To invoke recursive macros like this, invoke the macro just like a function with the addition of a ! before the arguments. Macro applications may alternatively be surrounded by [] or {}:

vec!["this is a macro", 1, 2];

Recursive macros are defined by macro_rules! statements. The inside of a macro_rules definition is very similar to that of a pattern-matching expression. The only difference is that macro_rules! matches syntax instead of data. We can use this format to define a reduced version of the vec macro. This is shown in the following code snippet, in intro_metaprogramming.rs:

macro_rules! my_vec_macro
{
    ( $( $x:expr ),* ) =>
    {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    }
}

This definition accepts and matches only one pattern. It expects a comma-separated list of expressions. The syntax pattern ( $( $x: expr ),* ) matches against a comma-separated list of expressions and stores the result in the plural variable $x. In the body of the expression, there is a single block. The block defines a new vec, then iterates through $x* to push each $x into the vec, and, finally, the block returns the vec as its result. The macro and its expansion are as follows, in intro_metaprogramming.rs:

//this
my_vec_macro!(1, 2, 3);

//is the same as this
{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

It is important to note that expressions are moved as code, not as values, so side effects will be moved to the evaluating context, not the defining context.

Recursive macro patterns match against token strings. It is possible to execute separate branches depending on which tokens are matched. A simple case match looks like the following, in intro_metaprogramming.rs:

macro_rules! my_macro_branch
{
    (1 $e:expr) => (println!("mode 1: {}", $e));
    (2 $e:expr) => (println!("mode 2: {}", $e));
}

fnmain()
{
    my_macro_branch!(1 "abc");
    my_macro_branch!(2 "def");
}

The name recursive macros comes from recursion in the macros, so of course we can call into the macro that we are defining. Recursive macros could be a quick way to define a domain-specific language. Consider the following code snippet, in intro_metaprogramming.rs:

enum DSLTerm {
TVar { symbol: String },
TAbs { param: String, body: Box<DSLTerm> },
TApp { f: Box<DSLTerm>, x: Box<DSLTerm> }
}

macro_rules! dsl
{
    ( ( $($e:tt)* ) ) => (dsl!( $($e)* ));
    ( $e:ident ) => (DSLTerm::TVar {
        symbol: stringify!($e).to_string()
    });
    ( fn $p:ident . $b:tt ) => (DSLTerm::TAbs {
        param: stringify!($p).to_string(),
        body: Box::new(dsl!($b))
    });
    ( $f:tt $x:tt ) => (DSLTerm::TApp {
        f: Box::new(dsl!($f)),
        x: Box::new(dsl!($x))
    });
} 

The second form of macro definitions is procedural macros. Recursive macros can be thought of as a nice syntax to help define procedural macros. Procedural macros, on the other hand, are the most general form. There are many things you can do with procedural macros that are simply impossible with the recursive form.

Here, we can grab the TypeName of a struct and use that to automatically generate a trait implementation. Here is the macro definition, in intro_metaprogramming.rs:

#![crate_type = "proc-macro"]
extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
#[proc_macro_derive(TypeName)]

pub fn type_name(input: TokenStream) -> TokenStream
{
    // Parse token stream into input AST
    let ast = syn::parse(input).unwrap();
    // Generate output AST
    impl_typename(&ast).into()
}

fn impl_typename(ast: &syn::DeriveInput) -> quote::Tokens
{
    let name = &ast.ident;
    quote!
    {
        impl TypeName for #name
        {
            fn typename() -> String
            {
                stringify!(#name).to_string()
            }
        }
    }
}

The corresponding macro invocation looks like the following, in intro_metaprogramming.rs:

#[macro_use]
extern crate metaderive;

pub trait TypeName
{
    fn typename() -> String;
}

#[derive(TypeName)]
struct MyStructA
{
    a: u32,
    b: f32
}

As you can see, procedural macros are a bit more complicated to set up. However, the benefit is then that all processing is done directly with normal Rust code. These macros permit use of any syntactic information in unstructured format to generate more code structures before compilation.

Procedural macros are handled as separate modules to be precompiled and executed during normal compiler execution. The information provided to each macro is localized, so whole program consideration is not possible. However, the available local information is sufficient to achieve some fairly complicated effects.

 

Summary


In this chapter, we briefly outlined the major concepts that will appear throughout this book. From the code examples, you should now be able to visually identify functional style. We also mentioned some of the reasons why these concepts are useful. In the remaining chapters, we will provide full context to when and why each technique would be appropriate. In that context, we will also provide the knowledge required to master the techniques and start using functional practices.

From this chapter, we learned to parameterize as much as possible, and that functions can be used as parameters, to define complex behavior by combining simple behaviors, and that it is safe to use threads however you want in Rust as long as it compiles.

This book is structured to introduce simpler concepts first, then, as the book continues, some concepts may become more abstract or technical. Also, all techniques will be introduced in the context of an ongoing project. The project will control an elevator system, and the requirements will gradually become more demanding as the book progresses.

 

Questions


  1. What is a function?
  2. What is a functor?
  3. What is a tuple?
  4. What control flow expression was designed for use with tagged unions?
  5. What is the name for a function with a function as a parameter?
  6. How many times will fib be called in memoized fib(20)?
  7. What datatypes can be sent over a channel?
  8. Why do functions need to be boxed when returned from a function?
  9. What does the move keyword do?
  10. How could two variables share ownership of a single variable?
 

Further reading


Packt has many other great resources for learning Rust:

For basic documentation and a tutorial, please refer here:

About the Author

  • Andrew Johnson

    Andrew Johnson is a software developer who has worn many hats. Professionally, he has worked on projects written in C, C++, Java, Python, Ruby, JavaScript, Haskell, OCaml, and now Rust. Most notably, he has worked as an early employee at Topsy Labs (acquired by Apple) and FiscalNote (growing rapidly). Academically, his interests are focused on the intersection between formal language processing (such as programming languages) and existing natural language programming techniques.

    Browse publications by this author

Latest Reviews

(1 reviews total)
A lire et relire; vaut le détour.

Recommended For You

Book Title
Unlock this book and the full library for only $5/m
Access now