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

Mapping code changes and additions

Now that we have organized our concepts, data structures, and logic into files, we can now proceed with the normal process to transform requirements into code. For each module, we will look at the required elements and produce code to satisfy those requirements.

Here, we break down all code development steps by module. Different modules have different organizations, so pay attention for patterns regarding organization and code development.

Developing code by type

These files will be organized using the by type method.

Writing the motor_controllers.rs module

The new motor_controller module serves as an adapter to all of the linked motor drivers and their interfaces, and provides a single uniform interface. Let's see how:

  1. First, let's link all the drivers from the software provided into our program:
use libc::c_int;

#[link(name = "motor1")]
extern {
pub fn motor1_adjust_motor(target_force: c_int) -> c_int;
}

#[link(name = "motor2")]
extern {
pub fn motor2_adjust_motor(target_force: c_int) -> c_int;
}

#[link(name = "motor3")]
extern {
pub fn motor3_adjust_motor(target_force: c_int) -> c_int;
}

This section tells our program to link to statically compiled libraries named something like libmotor1.a, libmotor2.a, and libmotor3.a. Our example chapter also contains the source and build script for these libraries, so you can inspect each one. In a full project, there are many ways to link to an external binary library, this being only one of many options.

  1. Next, we should make a trait for MotorInput and a generic MotorDriver interface, including implementations for each motor. The code is as follows:
#[derive(Clone,Serialize,Deserialize,Debug)]
pub enum MotorInput
{
Motor1 { target_force: f64 },
Motor2 { target_force: f64 },
Motor3 { target_force: f64 },
}

pub trait MotorDriver
{
fn adjust_motor(&self, input: MotorInput);
}

struct Motor1;
impl MotorDriver for Motor1 { ... }

//Motor 2

//Motor 3
  1. Next, we should implement the motor controller trait and implementations. The motor controller should wrap motor information and drivers into a uniform interface. The MotorDriver and MotorController trait here are coerced into a simple upward/downward force model. Therefore, the relation between driver and controller is one-to-one and cannot be completely abstracted into a common trait. The code for it is as follows:
pub trait MotorController
{
fn adjust_motor(&self, f: f64);
fn max_force(&self) -> f64;
}

pub struct MotorController1
{
motor: Motor1
}

impl MotorController for MotorController1 { ... }

//Motor Controller 2 ...

//Motor Controller 3 ...

The entire code for these is present in the GitHub repository at: https://github.com/PacktPublishing/Hands-On-Functional-Programming-in-RUST.

Writing the buildings.rs module

The building module is again grouped by type. There should be a common trait interface that is implemented by the three buildings. The building traits and structures should additionally wrap and expose interfaces to appropriate elevator drivers and motor controllers. The code is as follows:

  1. First, we define the Building trait:
pub trait Building
{
fn get_elevator_driver(&self) -> Box<ElevatorDriver>;
fn get_motor_controller(&self) -> Box<MotorController>;
fn get_floor_heights(&self) -> Vec<f64>;
fn get_carriage_weight(&self) -> f64;
fn clone(&self) ->
Box<Building>;
fn serialize(&self) -> u64;
}
  1. Then, we define a deserialize helper function:
pub fn deserialize(n: u64) -> Box<Building>
{
if n==1 {
Box::new(Building1)
} else if n==2 {
Box::new(Building2)
} else {
Box::new(Building3)
}
}
  1. Then, we define some miscellaneous helper functions:
pub fn getCarriageFloor(floorHeights: Vec<f64>, height: f64) -> u64
{
let mut c = 0.0;
for (fi, fht) in floorHeights.iter().enumerate() {
c += fht;
if height <= c {
return (fi as u64)
}
}
(floorHeights.len()-1) as u64
}

pub fn getCumulativeFloorHeight(heights: Vec<f64>, floor: u64) -> f64
{
heights.iter().take(floor as usize).sum()
}
  1. Finally, we define the buildings and their trait implementations:
pub struct Building1;
impl Building for Building1 { ... }

//Building 2

//Building 3

Developing code by purpose

These files will be organized using the by purpose method.

Writing the motion_controllers.rs module

The old logic from motor_controllers.rs for dynamically adjusting motor force will be moved to this module. The SmoothMotionController does not change much and the code becomes as follows:

pub trait MotionController
{
fn init(&mut self, esp: Box<Building>, est: ElevatorState);
fn adjust(&mut self, est: &ElevatorState, dst: u64) -> f64;
}

pub struct SmoothMotionController
{
pub esp: Box<Building>,
pub timestamp: f64
}

impl MotionController for SmoothMotionController
{
...
}

Writing the trip_planning.rs module

The trip planner should work in static and dynamic modes. The basic structure is a FIFO queue, pushing requests into the queue, and popping the oldest element. We may be able to unify both static and dynamic modes into a single implementation, which would look like the following.

Trip planning will be organized by purpose. The planner should work in two modes—static and dynamic. For static mode, the planner should accept a list of floor requests to process. For dynamic mode, the planner should accept floor requests as they come dynamically and add them to the queue. The planner module should contain the following:

use std::collections::VecDeque;

pub struct FloorRequests
{
pub requests: VecDeque<u64>
}

pub trait RequestQueue
{
fn add_request(&mut self, req: u64);
fn add_requests(&mut self, reqs: &Vec<u64>);
fn pop_request(&mut self) -> Option<u64>;
}

impl RequestQueue for FloorRequests
{
fn add_request(&mut self, req: u64)
{
self.requests.push_back(req);
}
fn add_requests(&mut self, reqs: &Vec<u64>)
{
for req in reqs
{
self.requests.push_back(*req);
}
}
fn pop_request(&mut self) -> Option<u64>
{
self.requests.pop_front()
}
}

Writing the elevator_drivers.rs module

The elevator drivers module should interface with the static libraries provided and additionally provide a common interface to all elevator drivers. The code looks like the following:

use libc::c_int;

#[link(name = "elevator1")]
extern {
pub fn elevator1_poll_floor_request() -> c_int;
}

#[link(name = "elevator2")]
extern {
pub fn elevator2_poll_floor_request() -> c_int;
}

#[link(name = "elevator3")]
extern {
pub fn elevator3_poll_floor_request() -> c_int;
}

pub trait ElevatorDriver
{
fn poll_floor_request(&self) -> Option<u64>;
}

pub struct ElevatorDriver1;
impl ElevatorDriver for ElevatorDriver1
{
fn poll_floor_request(&self) -> Option<u64>
{
unsafe {
let req = elevator1_poll_floor_request();
if req > 0 {
Some(req as u64)
} else {
None
}
}
}
}

//Elevator Driver 2

//Elevator Driver 3

Developing code by layer

These files will be organized using the by layer method.

Writing the physics.rs module

The physics module has become much smaller. It now contains a few struct definitions and constants and the central simulate_elevator method. The result is as follows:

#[derive(Clone,Debug,Serialize,Deserialize)]
pub struct ElevatorState {
pub timestamp: f64,
pub location: f64,
pub velocity: f64,
pub acceleration: f64,
pub motor_input: f64
}

pub const MAX_JERK: f64 = 0.2;
pub const MAX_ACCELERATION: f64 = 2.0;
pub const MAX_VELOCITY: f64 = 5.0;

pub fn simulate_elevator(esp: Box<Building>, est: ElevatorState, floor_requests: &mut Box<RequestQueue>,
mc: &mut Box<MotionController>, dr: &mut Box<DataRecorder>)
{
//immutable input becomes mutable local state
let mut esp = esp.clone();
let mut est = est.clone();

//initialize MotorController and DataController
mc.init(esp.clone(), est.clone());
dr.init(esp.clone(), est.clone());

//5. Loop while there are remaining floor requests
let original_ts = Instant::now();
thread::sleep(time::Duration::from_millis(1));
let mut next_floor = floor_requests.pop_request();
while let Some(dst) = next_floor
{
//5.1. Update location, velocity, and acceleration
let now = Instant::now();
let ts = now.duration_since(original_ts)
.as_fractional_secs();
let dt = ts - est.timestamp;
est.timestamp = ts;

est.location = est.location + est.velocity * dt;
est.velocity = est.velocity + est.acceleration * dt;
est.acceleration = {
let F = est.motor_input;
let m = esp.get_carriage_weight();
-9.8 + F/m
};

//5.2. If next floor request in queue is satisfied, then remove from queue
if (est.location - getCumulativeFloorHeight(esp.get_floor_heights(), dst)).abs() < 0.01 &&
est.velocity.abs() < 0.01
{
est.velocity = 0.0;
next_floor = floor_requests.pop_request();
}

//5.4. Print realtime statistics
dr.poll(est.clone(), dst);

//5.3. Adjust motor control to process next floor request
est.motor_input = mc.poll(est.clone(), dst);

thread::sleep(time::Duration::from_millis(1));
}
}

Writing the data_recorders.rs module

To separate responsibilities and not let individual modules get too big, we should move the data recorder implementation out of the simulation and into its own module. The result is as follows:

  1. Define the DataRecorder trait:
pub trait DataRecorder
{
fn init(&mut self, esp: Box<Building>, est: ElevatorState);
fn record(&mut self, est: ElevatorState, dst: u64);
fn summary(&mut self);
}
  1. Define the SimpleDataRecorder struct:
struct SimpleDataRecorder<W: Write>
{
esp: Box<Building>,
termwidth: u64,
termheight: u64,
stdout: raw::RawTerminal<W>,
log: File,
record_location: Vec<f64>,
record_velocity: Vec<f64>,
record_acceleration: Vec<f64>,
record_force: Vec<f64>,
}
  1. Define the SimpleDataRecorder constructor:
pub fn newSimpleDataRecorder(esp: Box<Building>) -> Box<DataRecorder>
{
let termsize = termion::terminal_size().ok();
Box::new(SimpleDataRecorder {
esp: esp.clone(),
termwidth: termsize.map(|(w,_)| w-2).expect("termwidth") as u64,
termheight: termsize.map(|(_,h)| h-2).expect("termheight") as u64,
stdout: io::stdout().into_raw_mode().unwrap(),
log: File::create("simulation.log").expect("log file"),
record_location: Vec::new(),
record_velocity: Vec::new(),
record_acceleration: Vec::new(),
record_force: Vec::new()
})
}
  1. Define the SimpleDataRecorder implementation of the DataRecorder trait:
impl<W: Write> DataRecorder for SimpleDataRecorder<W>
{
fn init(&mut self, esp: Box<Building>, est: ElevatorState)
{
...
}
fn record(&mut self, est: ElevatorState, dst: u64)
...
}
fn summary(&mut self)
{
...
}
}
  1. Define the miscellaneous helper functions:
fn variable_summary<W: Write>(stdout: &mut raw::RawTerminal<W>, vname: String, data: &Vec<f64>) {
let (avg, dev) = variable_summary_stats(data);
variable_summary_print(stdout, vname, avg, dev);
}

fn variable_summary_stats(data: &Vec<f64>) -> (f64, f64)
{
//calculate statistics
let N = data.len();
let sum = data.iter().sum::<f64>();
let avg = sum / (N as f64);

let dev = (
data.clone().into_iter()
.map(|v| (v - avg).powi(2))
.sum::<f64>()
/ (N as f64)
).sqrt();
(avg, dev)
}


fn variable_summary_print<W: Write>(stdout: &mut raw::RawTerminal<W>, vname: String, avg: f64, dev: f64)
{
//print formatted output
writeln!(stdout, "Average of {:25}{:.6}", vname, avg);
writeln!(stdout, "Standard deviation of {:14}{:.6}", vname, dev);
writeln!(stdout, "");
}

Developing code by convenience

These files will be organized using the by convenience method.

Writing the simulate_trip.rs executable

The simulate trip changes quite a bit because the DataRecorder logic has been removed. The initialization of the simulation is also very different from before. The end result is as follows:

  1. Initialize ElevatorState:
//1. Store location, velocity, and acceleration state
//2. Store motor input target force
let mut est = ElevatorState {
timestamp: 0.0,
location: 0.0,
velocity: 0.0,
acceleration: 0.0,
motor_input: 0.0
};
  1. Initialize the building description and floor requests:
//3. Store input building description and floor requests
let mut esp: Box<Building> = Box::new(Building1);
let mut floor_requests: Box<RequestQueue> = Box::new(FloorRequests {
requests: Vec::new()
});
  1. Parse the input and store it as building description and floor requests:
//4. Parse input and store as building description and floor requests
match env::args().nth(1) {
Some(ref fp) if *fp == "-".to_string() => {
...
},
None => {
...
},
Some(fp) => {
...
}
}
  1. Initialize the data recorder and motion controller:
let mut dr: Box<DataRecorder> = newSimpleDataRecorder(esp.clone());
let mut mc: Box<MotionController> = Box::new(SmoothMotionController {
timestamp: 0.0,
esp: esp.clone()
});
  1. Run the elevator simulation:
simulate_elevator(esp, est, &mut floor_requests, &mut mc, &mut dr);
  1. Print the simulation summary:
dr.summary();

Writing the analyze_trip.rs executable

The analyze trip executable will only change a little bit, but only to accommodate symbols that have been moved and types that are now serializable with SerDe. The result is as follows:

  1. Define the Trip data structure:
#[derive(Clone)]
struct Trip {
dst: u64,
up: f64,
down: f64
}
  1. Initialize the variables:
let simlog = File::open("simulation.log").expect("read simulation log");
let mut simlog = BufReader::new(&simlog);
let mut jerk = 0.0;
let mut prev_est: Option<ElevatorState> = None;
let mut dst_timing: Vec<Trip> = Vec::new();
let mut start_location = 0.0;
  1. Iterate over log lines and initialize the elevator specification:
let mut first_line = String::new();
let len = simlog.read_line(&mut first_line).unwrap();
let spec: u64 = serde_json::from_str(&first_line).unwrap();
let esp: Box<Building> = buildings::deserialize(spec);

for line in simlog.lines() {
let l = line.unwrap();
//Check elevator state records
}
  1. Check the elevator state records:
let (est, dst): (ElevatorState,u64) = serde_json::from_str(&l).unwrap();
let dl = dst_timing.len();
if dst_timing.len()==0 || dst_timing[dl-1].dst != dst {
dst_timing.push(Trip { dst:dst, up:0.0, down:0.0 });
}

if let Some(prev_est) = prev_est {
let dt = est.timestamp - prev_est.timestamp;
if est.velocity > 0.0 {
dst_timing[dl-1].up += dt;
} else {
dst_timing[dl-1].down += dt;
}
let da = (est.acceleration - prev_est.acceleration).abs();
jerk = (jerk * (1.0 - dt)) + (da * dt);
if jerk.abs() > 0.22 {
panic!("jerk is outside of acceptable limits: {} {:?}", jerk, est)
}
} else {
start_location = est.location;
}

if est.acceleration.abs() > 2.2 {
panic!("acceleration is outside of acceptable limits: {:?}", est)
}

if est.velocity.abs() > 5.5 {
panic!("velocity is outside of acceptable limits: {:?}", est)
}

prev_est = Some(est);
  1. Check that the elevator does not backup:
//elevator should not backup
let mut total_time = 0.0;
let mut total_direct = 0.0;
for trip in dst_timing.clone()
{
total_time += (trip.up + trip.down);
if trip.up > trip.down {
total_direct += trip.up;
} else {
total_direct += trip.down;
}
}

if (total_direct / total_time) < 0.9 {
panic!("elevator back up is too common: {}", total_direct / total_time)
}
  1. Check that the trips finish within 20% of their theoretical limit:
let mut trip_start_location = start_location;
let mut theoretical_time = 0.0;
let floor_heights = esp.get_floor_heights();
for trip in dst_timing.clone()
{
let next_floor = getCumulativeFloorHeight(floor_heights.clone(), trip.dst);
let d = (trip_start_location - next_floor).abs();
theoretical_time += (
2.0*(MAX_ACCELERATION / MAX_JERK) +
2.0*(MAX_JERK / MAX_ACCELERATION) +
d / MAX_VELOCITY
);
trip_start_location = next_floor;
}

if total_time > (theoretical_time * 1.2) {
panic!("elevator moves to slow {} {}", total_time, theoretical_time * 1.2)
}

Writing the operate_elevator.rs executable

The operate elevator is very similar to the simulate_trip.rs and physics run_simulation code. The most significant difference is the ability to continue running while dynamically accepting new requests and adjusting motor control using the linked libraries. In the main executable, we follow the same logical process as before, adjusted for new names and type signatures:

  1. Initialize ElevatorState:
//1. Store location, velocity, and acceleration state
//2. Store motor input target force
let mut est = ElevatorState {
timestamp: 0.0,
location: 0.0,
velocity: 0.0,
acceleration: 0.0,
motor_input: 0.0
};
  1. Initialize MotionController:
let mut mc: Box<MotionController> = Box::new(SmoothMotionController {
timestamp: 0.0,
esp: esp.clone()
});
mc.init(esp.clone(), est.clone());
  1. Start the operating loop to process incoming floor requests:
//5. Loop continuously checking for new floor requests
let original_ts = Instant::now();
thread::sleep(time::Duration::from_millis(1));
let mut next_floor = floor_requests.pop_request();
while true
{
if let Some(dst) = next_floor {
//process floor request
}

//check for dynamic floor requests
if let Some(dst) = esp.get_elevator_driver().poll_floor_request()
{
floor_requests.add_request(dst);
}
}
  1. In the processing loop, update the physics approximations:
//5.1. Update location, velocity, and acceleration
let now = Instant::now();
let ts = now.duration_since(original_ts)
.as_fractional_secs();
let dt = ts - est.timestamp;
est.timestamp = ts;

est.location = est.location + est.velocity * dt;
est.velocity = est.velocity + est.acceleration * dt;
est.acceleration = {
let F = est.motor_input;
let m = esp.get_carriage_weight();
-9.8 + F/m
};
  1. If the current floor request is satisfied, remove it from the queue:
//5.2. If next floor request in queue is satisfied, then remove from queue
if (est.location - getCumulativeFloorHeight(esp.get_floor_heights(), dst)).abs() < 0.01 && est.velocity.abs() < 0.01
{
est.velocity = 0.0;
next_floor = floor_requests.pop_request();
}
  1. Adjust the motor control:
//5.3. Adjust motor control to process next floor request
est.motor_input = mc.poll(est.clone(), dst);

//Adjust motor
esp.get_motor_controller().adjust_motor(est.motor_input);

      Reflecting on the project structure

      Now that we have developed code to organize and connect different elevator functions, as well as three executables to simulate, analyze, and operate the elevators, let's ask ourselves this—how does it all fit together, and have we done a good job architecting this project thus far?

      Reviewing this chapter, we can quickly see that we have made use of four different code organization techniques. At a more casual level, the code seems to fall into categories, as follows:

      • Luggage: Like drivers that need to be connected, but may be difficult to work with
      • Nuts, bolts, and gears: Like structs and traits, we have a lot of control of how to design
      • Deliverables: Like executables, these must fulfill a specific requirement

      We have organized all deliverables by convenience; all luggage by type or by purpose; and nuts, bolts, and gears have been organized by type, by purpose, or by layer. The result could be worse, and organizing by a different standard does not imply that the code will change significantly. Overall, the deliverables are supported by fairly maintainable code and the project is going in a good direction.

      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