Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Hands-On Functional Programming in Rust

You're reading from   Hands-On Functional Programming in Rust Build modular and reactive applications with functional programming techniques in Rust 2018

Arrow left icon
Product type Paperback
Published in May 2018
Publisher Packt
ISBN-13 9781788839358
Length 249 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Andrew Johnson Andrew Johnson
Author Profile Icon Andrew Johnson
Andrew Johnson
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Functional Programming – a Comparison 2. Functional Control Flow FREE CHAPTER 3. Functional Data Structures 4. Generics and Polymorphism 5. Code Organization and Application Architecture 6. Mutability, Ownership, and Pure Functions 7. Design Patterns 8. Implementing Concurrency 9. Performance, Debugging, and Metaprogramming 10. Assessments 11. Other Books You May Enjoy

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));
let mut handles = vec![];

for
_ in 0..10 {
let
counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut 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())
}
}
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image