about
RustStory Operations
9/15/2022
Rust Story Code

Chapter 3. - Rust Operations

Functions, function ptrs, methods, closures

3.0 Prologue

Rust provides a rich set of operations on program data: functions, methods, closures, error handling, iterators, and enumeration matching, that support an expressive design language for building fast and robust software.
Operations Syntax
Function Syntax
fn f(arg1:A1, arg2:A2) -> R {
  /* do something with args to implement r:R */
  r
}


Function arguments may be passed by value or reference, determined by the types A1 and A2. Often the argument types and return type can be deduced by the Rust compiler and may be omitted. Functions are invoked with syntax:
let x = f(a, b)
Function Example
fn carginals<const N: usize>() -> [usize; N] { 
  let mut array = [0; N];
  for i in 0..N {
    array[i] = i;
  }
  array
}

Function cardinals builds array of N cardinals. N is a generic parameter, so the function is invoked with syntax:
let arr = cardinals<5>
It first builds array of N integers with value zero, then fills with cardinal numbers using iterator 0..N. Rust functions return value of the last expression in the function body.
Closure Syntax
|arg1:A1, arg2:A2, ...| -> R { 
  /* 
    build R using args and may use values      
    from local context 
  */             
};


Closures are anonymous structs that create invokable objects and are built within some local context, e.g., a function body. They may use values or make references to variables in that context. Rust closures are first class functions that may be passed into or returned from another function. If an implementation does that, any local variables used must be captured by value, either by copying or moving them into the closure.
Closure Example
let n = 5;

let cardinals_closure = || {
  (0..n).collect::<Vec<usize>>()               
};

let arr = cardinals_closure();


cardinals_closure captures the value of n from its local context, i.e., it is not passed as an argument. Essentially n is a global variable within the local stack frame. It is invoked with the syntax:
let arr = cardinals_closure()
Its functionality is the same as cardinals<N>(). Note that, as is frequently the case, the Rust compiler is able to deduce the return type so it need not be explicitly declared.
Method Syntax
struct S {
  item: I,
}
impl S {
  fn show(&self) -> I {                        
    &self.item
  }
}
                
Rust uses structs to define types the way some other languages use classes. They define data and methods that operate on the data to provide initialization, access, and transformations consistent with the mission of the type.
All methods, e.g., functions that have access to the structs data, are implemented in an impl block. Non-static methods must take self or &self as their first argument.
Method Example
use std::sync::atomic::{ AtomicI32, Ordering };

#[derive(Debug)]
pub struct Counter {
  value: AtomicI32,
}
impl Counter {
  pub fn new() -> Counter {
    Counter {
      value:0.into(),
    }
  }
  pub fn incr(&mut self) {
    self.value.fetch_add(1, Ordering::SeqCst);
  }
  pub fn decr(&mut self) {
    self.value.fetch_sub(1, Ordering::SeqCst);
  }
  pub fn count(&self) -> i32 {
    self.value.load(Ordering::SeqCst)
  }
}
This example provides a thread-safe counter with methods that increment and decrement an AtomicI32, and provide access to the count.
The first method is a static function that creates a new instance of the Counter type with an initial count of 0. The second and third methods mutate the count by 1 on each call. The third method returns the count without mutation.
Counter and its methods are all declared pub, providing access to code That imports these definitions. It has a #[define(Debug)] declaration That requests the compiler to implement the Debug trait so that it can be displayed with debug format "{:?}".
The atomic operations used here are described in more detail in the Synchron Rust Bite.
Function Object Syntax
pub struct FO {
  name: String
}
impl FO {
  pub fn get_function_object(&self) -> impl Fn()
  {
    let rname = &self.name;
    move || {
      /* do something with rname */
    }
  }
}

Function objects are closures that capture data from an associated object.
The FO method get_function_object(&self) returns a closure that implements the Fn trait so it can be invoked.
A closure is an anonymous struct that is invokeable and can capture data from the local scope.
The move keyword ensures that the capture moves rname into the closure, so it can be safely passed to other functions as long as its lifetime is no greater than that of FO.
Since rname is a reference to self.name it does not consume self.name.
Function Object Example
pub struct DemoFO {
  name : String,
}
impl DemoFO {
  pub fn new() -> Self {
    Self {
      name:"no_name".to_string(),
    }
  }
  pub fn change_name(&mut self, s:&str) {
    self.name = s.to_string();
  }
  pub fn get_function_object<'a>(&'a self)      
    -> impl Fn() + 'a
  {
    let name = &self.name;
    move || { 
      println!("{:?}", name) 
    }
  }
}


                
pub fn demo_function_object() {
  println!();
  show_label("demo function objects");
  let mut demo = DemoFO::new();

  /* immutable borrow of DemoFO here */   
  let fo = demo.get_function_object();
  fo();
  drop(fo);
  /* 
    mutable borrow of DemoFO below,
    O.K. because fo dropped
  */
  demo.change_name("Arnold");
  let fo = demo.get_function_object();
  fo();
}
  
 demo function objects
-----------------------                   
"no_name"
"Arnold"  
Function Pointer Syntax
/* demonstration function */
fn this_function_has_a_very_long_name() {
  println!("action of function with long name");
}

pub fn demo_function_pointer() {
  println!();
  show_label("demo function pointer");
  let tfh = this_function_has_a_very_long_name;
  tfh();
}
 demo function pointer
------------------------
action of function with long name         







  
Function Pointer Example
/* demonstration function */
pub fn u(s:&str) {
  println!("{:?}", s);
}
/* function accepts function pointer */         
pub fn v(fp:impl Fn(&str) -> ()) {
  fp("using function pointer");
}
pub fn demo_function_pointer() {
  println!();
  show_label("demo function pointer");
  v(u);
}
 demo function pointer
------------------------                  
"using function pointer"
  








                  
In this chapter we discuss all of these and explore their consequences with example code.

3.1 Functions

The syntax for Rust functions looks like this:
fn f(a1:A1, a2:A2, ...) -> R { ... }
Often the return type is omitted when the compiler can infer the type by the return expression. Statements are units of execution and end with a "semicolon". Expressions compile to some value and do not have a terminating semicolon.
The returned value of a function is the value of the last expression. If the function code body ends with a statement, the function returns "()", called the unit, signifying nothing (Null in some other languages). Rust functions allow a return statement but that is not required and is almost never used. Function arguments play a role similar to other languages, but with some interesting differences. Primitive types, e.g. int, float, ..., are passed by value or reference. The primitives and aggregates of primitives are copied when passed by value. when passed by reference a reference is created on the function's stackframe pointing back to the caller's value, and if the data is mutable any changes made to it within the function body will be visible to the caller. So far, this is just like C++, C#, and many others. Non-primitive data like strings and vectors are move types, so when passed by value they are not copied but moved, which is efficient - usually only copying a few bytes - but the caller's value will become invalid. Passing by reference places a reference in the function's stackframe pointing back to the caller's value, so no move and the caller value will remain valid after the call.
Pass by Value /*----------------------------------------------- Pass by value moves the argument into the function's stack frame, so invalid after call */ fn pass_by_value_str(s:String) { // move /* use s */ } /* s now invalid - statements using s will fail to compile */ Pass by Reference /*------------------------------------- Pass by ref borrows the argument. Borrow reference created in the function's stack frame. Borrow ends at end of function call, so param rs is valid after call */ fn pass_by_ref_str(rs:&String) { // borrow /* use rs */ } /* rs still valid, o.k. to use */
There are four combinations of argument type and passing type, each combination with its own behavior:
Function SignatureBehavior
 1. Pass copy type by value
 Example: e(d:f64)
 No side effects, argument remains valid after call
 Example: let d = 3.1415927; e(d);
 2. Pass copy type by ref
 Example: f(rd:&f64);
 Side effects: user sees change of value, argument remains valid
 Example: let d = 3.1415927; f(&d);
 3. Pass move type by value
 Example: g(v:Vec<i32>);
 Side effects: argument becomes invalid after call
 Example: let v = vec![1, 2, 3]; g(v):
 4. Pass move type by ref
 Example: h(rv:&Vec<i32>)
 Side effects: caller sees both change in value and change in instance1.
 Example: let v = vec![2, 1, 0]; h(rv:&Vec);
  1. To change instance, function needs to accept reference to Box<T>, e.g., l(rb:&Box<T>) { ... }
Argument Passing Examples: 

Pass Copy Type argument by Value

Pass by Value, Caller sees no Change
pub fn pass_by_value<T>(mut t:T)
  where T:Debug + Default 
{
  show_op("in pass_by_value");
  show_type(&t);
  show_value(&t);
  /*-- demonstrate side effects --*/
  show_op("t = T::default()");
  t = T::default();
  show_value(&t);
  show_op("leaving function");
}
Pass_by_value<T> copies or moves argument into function's stack frame if the type is copy or move.
  • function displays type and value of the argument
  • it then changes value to T::default() to illustrate when side effects occur
  • copy types will be valid after the call, but move types will not
 
Demonstration Code
Output
show_label("pass copy type by value, caller doesn't see any changes");
let d = 3.1415927;  // literal is copy type
pass_by_value(d);
print!("  value of d is now {d:?}\n");
assert_eq!(d, 3.1415927);
shows("  no side-effects, passing copy type by value\n");
putln();
 pass copy type by value, caller doesn't see any changes
---------------------------------------------------------
--- in pass_by_value ---
  TypeId: f64, size: 8
  value: 3.1415927
--- t = T::default() ---
  value: 0.0
--- leaving function ---
  value of d is now 3.1415927
  no side-effects, passing copy type by value

Pass Copy Type Argument by Reference

Pass by Ref, Caller sees change of Value
pub fn pass_by_ref<T>
  where T: std::default::Default + Debug>(rt:&mut T) 
{
  show_op("in pass_by_ref");
  show_type(&rt);
  show_value(&rt);
  /*-- demonstrate side effects --*/
  show_op("*rt = T::default()");
  *rt = T::default();
  show_value(&rt);
  show_op("leaving function");
}
Pass_by_ref<T> borrows argument. Borrow moved into the function's stack frame. Borrow ends at end of function call, so param is valid after call
  • function displays type and value of the argument
  • it then changes value to T::default() to illustrate when side effects occur
  • all types will be valid after the call
 
Demonstration Code
Output
  show_label("pass copy type by ref, caller sees any changes");
  let mut s = "a string";  // literal is copy type
  pass_by_ref(&mut s);
  print!("  value of s is now {s:?}\n");
  assert_eq!(s, "");
  shows("  side-effects, passing copy type by ref\n");
  putln();
 pass copy type by ref, caller sees any changes
------------------------------------------------
--- in pass_by_ref ---
  TypeId: &mut &str, size: 8
  value: "a string"
--- *rt = T::default() ---
  value: ""
--- leaving function ---
value of s is now ""
side-effects, passing copy type by ref

Pass Move Type by Value

Pass by Value, Arg becomes Invalid
pub fn pass_by_value<T>(mut t:T) where T:Debug + Default {
  show_op("in pass_by_value");
  show_type(&t);
  show_value(&t);
  /*-- demonstrate side effects --*/
  show_op("t = T::default()");
  t = T::default();
  show_value(&t);
  show_op("leaving function");
}
Pass_by_value<T> copies or moves argument into function's stack frame if the type is copy or move.
  • function displays type and value of the argument
  • it then changes value to T::default() to illustrate when side effects occur
  • copy types will be valid after the call, but move types will not
 
Demonstration Code
Output
  show_label("pass move type by value, caller sees invalidation");
  let s = "a string".to_string();  // move type
  pass_by_value(s);
  // statement below fails to compile: s moved
  // print!("{s}\n");
  shows("  can't access s, been moved\n");
  shows("  side-effects, passing move type by value\n");
  putln();
 pass move type by value, caller sees invalidation
---------------------------------------------------
--- in pass_by_value ---
  TypeId: alloc::string::String, size: 24
  value: "a string"
--- t = T::default() ---
  value: ""
--- leaving function ---
  can't access s, been moved
  side-effects, passing move type by value

Pass Move Type by Reference

Pass by Reference, Caller sees change of Value
pub fn pass_by_ref<T>(rt:&mut T)
  where: T: std::default::Default + Debug 
{
  show_op("in pass_by_ref");
  show_type(&rt);
  show_value(&rt);
  /*-- demonstrate side effects --*/
  show_op("*rt = T::default()");
  *rt = T::default();
  show_value(&rt);
  show_op("leaving function");
}
Pass_by_ref<T> borrows argument. Borrow moved into the function's stack frame. Borrow ends at end of function call, so param is valid after call
  • function displays type and value of the argument
  • it then changes value to T::default() to illustrate when side effects occur
  • all types will be valid after the call
 
Demonstration Code
Output
  show_label("pass move type by ref, caller sees change of value");
  let mut v = vec![1, 2, 3];  // move type
  pass_by_ref(&mut v);
  assert_ne!(v, vec![1, 2, 3]);
  print!("  v now has value: {v:?}\n");
  shows("  side-effects, pass move type by ref and change value\n");
  putln();
 pass move type by ref, caller sees change of value
----------------------------------------------------
--- in pass_by_ref ---
  TypeId: &mut alloc::vec::Vec<i32>, size: 8
  value: [1, 2, 3]
--- *rt = T::default() ---
  value: []
--- leaving function ---
  v now has value: []
  side-effects, pass move type by ref and change value

Pass Move Type by Reference

Pass by Reference, Caller sees change of Instance
>pub fn pass_move_by_ref_heap_instance<T>(rh:&mut Box<T>)
  where T: From<String> + Default + Debug
{
  show_op("in pass_move_by_ref_heap_instance<T>");
  show_type(&rh);
  show_value(&rh);
  print!("  address of rh is: {:p}\n", *rh);
  /*---------------------------------------
    demonstrate side effects by changing
    referenced object
  */
  show_op("*rh = Box::new(t)");
  let t:T = "a new string".to_string().into();
  *rh = Box::new(t);  // change of instance
  print!("  address of rh is: {:p}\n", *rh);
  show_value(&rh);
  show_op("leaving function");
}
Pass_move_by_ref_heap_instance<T> mutably borrows argument. Borrow ends at end of function call, so rh is valid after call.
  • This function displays type and value of its argument.
  • It then changes value to a T coerced from a String to illustrate when side effects occur.
  • That is always possible for a type that implements Trait From<String> as compiler generates Into<String> from From<String>, e.g., it converts a String into a T type.
  • The function then sets the contents of rh to a Box<T>
  • That has reset the reference to a new heap instance. That works only because Box is a smart pointer, something more than just a reference.
  • The change of instance is sandwiched between two displays of the address associated with the argument, just to show that the instance really was changed.
As you see in the panels below, the "smrt_ptr_heap" argument is still valid after the function call. That is always the case when passing arguments by reference.
 
Demonstration Code
Output
show_label("pass move type by ref, caller sees change of instance");
let s = "a string".to_string();       // move type
let mut smrt_ptr_heap = Box::new(s);  // s moved into Box
pass_move_by_ref_heap_instance(&mut smrt_ptr_heap);
assert_ne!(*smrt_ptr_heap, "a string".to_string());
print!("  smrt_ptr_heap now has value: {:?}\n", *smrt_ptr_heap);
print!("  address of smrt_pt_heap is: {:p}\n", smrt_ptr_heap);
shows("  side-effects, pass move type by ref and change instance");
putln();
 pass move type by ref, caller sees change of instance
-------------------------------------------------------
--- in pass_move_by_ref_heap_instance<T> ---
  TypeId: &mut alloc::boxed::Box<alloc::string::String>, size: 8
  value: "a string"
  address of rh is: 0x1f767974810
--- *rh = Box::new(t) ---
  address of rh is: 0x1f767974870
  value: "a new string"
--- leaving function ---
  smrt_ptr_heap now has value: "a new string"
  address of smrt_pt_heap is: 0x1f767974870
  side-effects, pass move type by ref and change instance
Rust functions look similar to JavaScript functions with added type specifications. However, their use is more complicated due to Rust's rules for mutation and borrowing. The rules affect:
  1. How you supply function arguments, e.g., the type signatures used.
  2. What you are allowed to do inside a function body.
  3. What signatures you use for return types.
The bad news is this can get complicated. The good news is the Rust compiler's borrow checker does a great job telling you about problems and often how to fix them.
In the details dropdown, below, you will find simple demo examples of supplying arguments to functions, returning values and references, and processing input data in function bodies. There are examples of type specific functions and generic functions. Most are simple demos, only valuable for the usage examples provided. There are, however, a few functions with lasting value. One example shows how to gracefully accept either String or str instances. Another provides a facility to run test functions and report their results. This "Executor" uses the std::panic library to simulate a try-catch block. That is, if a tested function panics, the stack unwind process is intercepted and a simple error message is displayed. So, a tested function's panic is not an Executor panic. It is simply an event to report.
Function Code Examples:
Function Code Using Code Output
/*----------------------------------------------- Accepts String by value, moves argument s - s invalid after invocation - could use s.clone() as argument to avoid invalidating s */ fn show_str1(s:String) { // move print!("{}",&s); } show_str1("\n first test".to_string()); first test
/*----------------------------------------------- Accepts String by reference, borrows argument s - won't accept string literal */ fn show_str2(s:&String) { // borrow print!("{}",&s); } show_str2(&"\n second test".to_string()); second test
/*----------------------------------------------- Accepts str by reference - won't accept String instance st - will accept &st */ fn show_str3(s:&str) { // borrow print!("{}",&s); } show_str3("\n third test"); third test
/*----------------------------------------------- Returns String, moves internally created String to caller's scope */ fn return_str1() -> String { let s = "test string 1".to_string(); s // moves s } let mut s = return_str1(); s.push_str(" with more stuff"); let mut s1 = "\n ".to_string(); s1.push_str(&s); shows(s1); test string 1 with more stuff
/*----------------------------------------------- Returns str reference - it only makes sense to return a reference to a passed in ref or a static ref - compiler should prevent anything else */ fn return_str2() -> &'static str { let s = "test string 2"; s } let s = return_str2(); let mut st = s.to_string(); st.push_str(" with more stuff"); let mut s1 = "\n ".to_string(); s1.push_str(&st); shows(s1); test string 2 with more stuff
/*----------------------------------------------- Returns String reference - it only makes sense to return a reference to a passed in ref or a static ref - compiler should prevent anything else */ fn return_str3(s:&mut String) -> &mut String { s.push('Z'); s } let mut s = String::from("test string 4"); let rs = return_str3(&mut s); rs.push_str(" with more stuff"); let mut s1 = "\n ".to_string(); s1.push_str(&rs); shows(s1); test string 4 with more stuff
/*----------------------------------------------- Pass by value moves the argument into the function's stack frame, so invalid after call */ fn pass_by_value_str(s:String) { // move show_type(&s); // defined in display lib show_value(&s); // defined in display lib } let mut s = "xyz".to_string(); let s1 = s.clone(); pass_by_value_str(s1); // moves s1 //////////////////////////////////////////////// // next statement fails to compile - s1 moved // pass_by_value(s1); TypeId: alloc::string::String, size: 12 value: "xyz"
/*----------------------------------------------- Pass by ref borrows the argument into the function's stack frame. Borrow ends at end of function stack frame, so param is valid after call */ fn pass_by_ref_str(rs:&String) { // borrow show_type(&rs); // defined in display lib show_value(&rs); // defined in display lib } pass_by_ref_str(&s); s.push('a'); pass_by_ref(&s); // borrows s s.push('b'); show("\n after pushing a and b, s = ",&s); TypeId: &alloc::string::String, size: 4 value: "xyz" TypeId: &alloc::string::String, size: 4 value: "xyza" after pushing a and b, s = "xyzab"
/*----------------------------------------------- Generic pass by value moves argument into the function's stack frame, so invalid after call */ fn pass_by_value<T>(t:T) where T:Debug { show_type(&t); show_value(&t); } let mut s = "xyz".to_string(); let s1 = s.clone(); pass_by_value(s1); //////////////////////////////////////////////// // next statement fails to compile - s1 moved // pass_by_value(s1); TypeId: alloc::string::String, size: 12 value: "xyz"
/*----------------------------------------------- Generic pass by ref borrows argument into the function's stack frame. Borrow ends at end of function stack frame, so param is valid after call */ fn pass_by_ref<T>(rt:&T) where T:Debug { show_type(&rt); show_value(&rt); } pass_by_ref(&s); s.push('a'); pass_by_ref(&s); TypeId: &alloc::string::String, size: 4 value: "xyz" TypeId: &alloc::string::String, size: 4 value: "xyza"
/*----------------------------------------------- Illustrates lifetime - type specific so no annotation needed */ fn lifetime(rs:&String) -> String { show("\n rs = ",rs); show_type(rs); // replace doesn't attempt to mutate rs let s = rs.replace("z","a"); show("\n s = ",&s); show_type(&s); shows("\n returning string s by value (a move)"); s // return by value moves string to destination } let mut s = "xyz".to_string(); show("\n s = ",&s); shows("\n calling lifetime"); separator(30); let r = &mut lifetime(&mut s); separator(30); shows("\n returned from lifetime"); show("\n s = ",&s); show("\n r = ",&r); show_type(&r); r.push('b'); show("\n after pushing b, r = ",&r); s.push('b'); show("\n after pushing b, s = ",&s); s = "xyz" calling lifetime ------------------------------- rs = "xyz" TypeId: alloc::string::String, size: 12 s = "xya" TypeId: alloc::string::String, size: 12 returning string s by value (a move) ------------------------------- returned from lifetime s = "xyz" r = "xya" TypeId: &mut alloc::string::String, size: 4 after pushing b, r = "xyab" after pushing b, s = "xyzb"
/*----------------------------------------------- Illustrates lifetime for generic. - Borrow checker needs help to analyze lifetime of T */ fn lifetime2<'a, T>(rt:&'a T) -> &'a T where T:Debug { // Lifetime annotation, 'a, enables borrow checker // to ensures that T lives at least as long as rt, // its reference. show_type(&rt); show_value(&rt); rt // the only time it makes sense to return // a reference is when returning a possibly // modified input reference } let mut s = "xyz".to_string(); show("\n s = ",&s); let r = &mut lifetime2(&mut s); show_type(&r); show("\n r = ",&r); s = "xyz" TypeId: &alloc::string::String, size: 4 value: "xyz" TypeId: &mut &alloc::string::String, size: 4 r = "xyz"
Useful functions
/*----------------------------------------------- Accepts either String or str */ fn shows<S: Into<String>>(s:S) { print!("{}",s.into()); } sub_title("shows<S: Into<String>>(s:S)"); shows("This accepts either String or str"); shows<S: Into<String>>(s:S) ----------------------------- This accepts either String or str
function pointer defined in using code sub_title("Function pointer"); let fun = pass_by_ref; // define fun ptr let mut s = "xyz".to_string(); fun(&s); s.push('a'); fun(&s); Function pointer ------------------ TypeId: &alloc::string::String, size: 4 value: "xyz" TypeId: &alloc::string::String, size: 4 value: "xyza"
lambda defined in using code // Illustrates both lambda and fun ptr let put = |st|{ print!("{}", st); }; put("\n returning s by value"); sub_title("lambdas"); let l = |s:&str| { show_type(&s); show_value(&s); }; l("xyz"); let s = String::from("abc"); l(&s); lambdas --------- TypeId: &str, size: 8 value: "xyz" TypeId: &str, size: 8 value: "abc"
/*----------------------------------------------- Higher order function - tester accepts test functions and executes them - traps test function panic and continues */ use std::panic; // catch_unwind panic fn tester(f:fn() -> bool, name:&str) -> bool { let rslt = panic::catch_unwind(|| { f() }); // line above simulates execution in try-catch match rslt { Ok(true) => print!("\n {} passed", name), Ok(false) => { print!("\n {} failed", name); return false; }, Err(_) => { print!("\n {} paniced", name); return false; } } return true; } /* hide panic notification */ fn set_my_hook() { panic::set_hook(Box::new(|_|{ print!(" ");})); // Box needed to give lambda lifetime beyond // this call, e.g., stores in heap } fn always_fails() -> bool { false } fn always_succeeds() -> bool { true } #[allow(unreachable_code)] fn always_panics() -> bool { panic!("always panics"); return false; } sub_title("higher_order_function"); set_my_hook(); let rstl = tester(always_fails, "always_fails"); print!("\t\tresult = {}", rstl); let rstl = tester(always_succeeds, "always_passes"); print!("\t\tresult = {}", rstl); let rstl = tester(always_panics, "always_panics"); print!("\t\tresult = {}\n", rstl); /* show intercepted panic and continued */ use std::io::{Write}; let one_second = std::time::Duration::from_millis(1000); for i in 0..5 { print!("\n tick {}\t", 5 - i); std::io::stdout().flush().unwrap(); std::thread::sleep(one_second); }; print!("\n\n BOOM!\t"); putlinen(2); higher_order_function ----------------------- always_fails failed result = false always_passes passed result = true always_panics paniced result = false tick 5 tick 4 tick 3 tick 2 tick 1 BOOM!
Idiomatic Rust seems to often use functions in places where a C++ developer would be likely to use a class method. Rust does support methods bound to structs, which we will explore below.

3.1.1 Function Pointers

Function pointers are instances of the type std::fn that point to code, not data. Plain function pointers can only point to safe functions or closures that don't capture an environment. In the example, below, fun is a function pointer that invokes pass_by_ref. Function Pointer Example /*----------------------------------------------- pass_by_ref is a plain function that calls plain functions in RustBasicDemos::display library. */ fn pass_by_ref<T>(rt:&T) where T:Debug { show_type(&rt); show_value(&rt); } let fun = pass_by_ref; // define funptr let mut s = "xyz".to_string(); fun(&s); s.push('a'); fun(&s); We use function pointers as aliases that provide domain specific names for library functions or provide short names for longer generic function names.

3.1.2 Closures

Closures, also called lambda expressions or lambdas, are anonymous functions that implement one of these traits:
  1. FnOnce is implemented automatically by closures that may consume (move and use) captured variables.
  2. Fn is automatically implemented by closures which take only immutable references to captured data or don't capture anything. FnOnce and FnMut are super traits so closures that implement Fn can be used where FnOnce or FnMut are expected.
  3. FnMut is implemented automatically by closures which take mutable references to captured variables. FnOnce is a super trait so closures that implement FnMut can be used where FnOnce is expected.
Closure syntax: let x:i32 = 3; // immutable capture, below let cl = |val:i32| { print!("\n val = {}, x = {}, val + x = {}", val, x, val + x) }; cl(7); Output: val = 7, x = 3, val + x = 10 Closures are used where small special purpose functions are needed, but then discarded. Typically used for filters. Here are some examples:
Closure Examples: Helper Functions /*----------------------------------------------- Function consume accepts predicate closure - executes closure and returns its value - accepts function pointers too */ fn consume<F: FnOnce() -> bool>(cl:F) -> bool where F: FnOnce() -> bool { cl() } /*----------------------------------------------- Function answer accepts bool and displays value */ fn answer(ans:bool) { if ans == true { print!("\n answer is true"); } else { print!("\n answer is false"); } } /*----------------------------------------------- pure predicate functions */ fn always_true() -> bool { true } fn always_false() -> bool { false } Output: -- demo closures -- ======================= val = 7, x = 3, val + x = 10 count = 1, sum = 3 count = 2, sum = 3 count = 3, sum = 3 answer is true answer is false answer is true answer is false main /*----------------------------------------------- Demonstrate executor and closures */ fn main() { main_title(" -- demo closures -- "); /*-- immutable closure --------------------*/ let x:i32 = 3; // immutable capture, below let cl = |val:i32| { print!( "\n val = {}, x = {}, val + x = {}", val, x, val + x ) }; cl(7); let put = putline; // declaring funptr put(); /*-- mutable closure ----------------------*/ let mut count = 0; let mut counter = |offset:i32| { // mut closure count = count + 1; print!( "\n count = {}, sum = {}", count, count + offset ) }; counter(2); counter(1); counter(0); putline(); /*-- invariant closure --------------------*/ let clst = ||{ true }; // invariant closure let clsf = ||{ false }; // invariant closure let mut ans = consume(clst); answer(ans); ans = consume(clsf); answer(ans); ans = consume(always_true); answer(ans); ans = consume(always_false); answer(ans); putlinen(2); }
These examples present closures with immutable capture, mutable capture, and no capture. Also included is an example of a function that accepts either closures or function pointers and executes them.

3.1.3 Function Error Handling

Rust functions with operations that may fail, e.g., opening a file, should return one of two kinds of result wrappers, std::Result<T,E> or std::Option<T>. These are both enumerations: Result: enum Result<T, E> { Ok(T), Err(E), } Option: enum Option<T> { Some(T), None, }
  1. Option<T> is an enumeration that contains either Some(result:T) or None. Option<T> has methods:
    • is_some() -> bool
    • is_none() -> bool
    • contains<U>(x:&U) -> bool
    • expect(msg: &str) -> T
      Unwraps option returning content of Some or panics with msg
    • unwrap() -> T
      Returns value of Some or panics
    • take(&mut self) -> Option<T>
      Returns the Option value, leaving None in its place. Provides a way to access Move types from aggregates - simply wrap the elements in an Option<T> then use take() as needed.
    • map<U, F>(f:F) -> Option<U> where F: FnOnce(T) -> U
      Applies f to value of Some.
    • iter() -> Iter<T>
      Returns iterator over value of Some if available
    • filter<P>(predicate: P) -> Option<T> where P: FnOnce(&T) -> bool
      Returns None if no value else calls predicate with unwrapped value and returns.
    • Many more functions: std::Option.
  2. enum Result<T, E> is an enumeration that contains either Ok(result) or Err(err). Result<T, E> has methods:
    • is_ok() -> bool
    • is_err() -> bool
    • ok() -> Option<T>
    • err() -> Option<E>
    • unwrap() -> T, will return result if available or panic
    • unwrap_err() -> E, will return error if available or panic
    • unwrap_or_else<F>(op:F) -> T where F: FnOnce(E) -> T
      Returns result if available, else calls op(err)
    • expect(msg: &str) -> T, unwraps result if available else panics with msg
    • map<U, F>(op: F) -> Result<U, E> where F: FnOnce(T) -> U
      op is a lambda that replaces t:T with a computed value
    • iter() -> Iter<T>
      Returns iterator over Some value if it exists.
    • Many more functions: std::Result.
The difference between these is that Result can pass information about the error back to the caller. Returning Option signals that failure to return a result is not an error.
Result and Option Syntax Examples: These examples show how Result<T,E> and Option<T> work using a simulated error event that is passed as a bool. Part of the example uses .expect(e:Err) which panics if an error occurred (e.g., we entered false). That is not what you would do for production code. There you want to report the error but continue to operate, so using matching is a sensible choice. Functions returning Result or Option use display::{*}; fn demo_result<'a>(p: bool) -> Result<&'a str, &'a str> { print!("\n value of input predicate is {}", p); if p { return Ok("it's ok"); } else { return Err("not ok"); } } fn demo_option<'a>(p:bool) -> Option<&'a str> { print!("\n value of input predicate is {}", p); if p { return Some("something just for you!"); } else { return None; } } Output: -- demo Result -- ----------------------- -- using match value of input predicate is true result is it's ok value of input predicate is false result is not ok -- using expect value of input predicate is true result is it's ok -- demo Option -- ----------------------- --using match value of input predicate is true something just for you! value of input predicate is false sorry, nothing here --using unwrap value of input predicate is true something just for you! That's all folks! Using code: use display::{*}; sub_title(" -- demo Result -- "); shows("\n-- using match"); let r = demo_result(true); match r { Ok(rslt) => print!("\n result is {}", rslt), Err(rslt) => print!("\n result is {}", rslt) } let r = demo_result(false); match r { Ok(rslt) => print!("\n result is {}", rslt), Err(rslt) => print!("\n result is {}", rslt) } shows("\n\n-- using expect"); let r = demo_result(true) .expect("predicate was false"); print!("\n result is {}", r); ///////////////////////////////////////////// // uncomment to see panic // let _r = demo_result(false) // .expect("predicate was false"); putline(); sub_title(" -- demo Option -- "); shows("\n--using match"); let r = demo_option(true); match r { Some(rslt) => print!("\n {}", rslt), None => print!("\n sorry, nothing here") } let r = demo_option(false); match r { Some(rslt) => print!("\n {}", rslt), None => print!("\n sorry, nothing here") } shows("\n\n--using unwrap"); let r = demo_option(true).unwrap(); print!("\n {}", r); ///////////////////////////////////////////// // uncomment to see panic // let _r = demo_option(false).unwrap(); print!("\n\n That's all folks!\n\n");
Let's follow up with a practical example, handling file open errors, being careful to avoid panics:
File Error Handling File Open Code use std::io::prelude::*; use std::fs::File; use display::{*}; #[allow(dead_code)] fn open_file_for_read(file_name:&str) ->Result<File, std::io::Error> { use std::fs::OpenOptions; let rfile = OpenOptions::new() .read(true) .open(file_name); rfile } #[allow(dead_code)] use std::io::{Error, ErrorKind}; fn read_file_to_string(mut f:File) -> Result<String, std::io::Error> { let mut contents = String::new(); let bytes_rslt = f.read_to_string(&mut contents); if bytes_rslt.is_ok() { Ok(contents) } else { Err(Error::new(ErrorKind::Other, "read error")) } } Using Code /*----------------------------------------------------- - Choose name of a file in the error_probes crate root directory to show successful operation. - Choose one that does not exist to show failure operation. */ let file:File; let file_name = "foobar.txt"; let rslt = open_file_for_read(file_name); if rslt.is_ok() { print!("\n file {:?} opened successfully", file_name); file = rslt.unwrap(); let s = read_file_to_string(file); if s.is_ok() { print!("\n contents: \"{}\"", s.unwrap()); } } else { print!("\n failed to open file {:?}", file_name); } Output: file "foobar.txt" opened successfully contents: "This is foobar.txt" Note that, in the code above, unwrap() has been applied only where we know it won't panic. This way we gracefully handle errors and can still continue processing. You can test open failure handling by commenting out the .create(true) in file_open_for_write and deleting first_test.txt file from the crate directory. The Rust operator ? provides a convenient way to handle errors at the call site and then elevate to parent call site with an appropriate Result<T, E>. See this for an example .
You may find additional details provided in RustErrorHandling.pdf useful for helping you to write effective error handling code. Topics include panics, trapping panics, returning results, and console and file I/O. Next3, we turn to a very useful construct, iterators, as implemented in Rust.

3.2 Iterators

Rust collections like Vec, String, and Map provide iterators used to step over and operate on elements from the collection. Iterating over Vec of integers let v = vec![1, -1, 2, -2, 3, -3]; print!("\n "); let it = v.iter(); for val in it { print!("{} ", val); } Output: 1 -1 2 -2 3 -3 You can also provide iterators for your own custom types by defining an Iterator trait for the type. The only requirement is that it provide a method next() that returns an option with Some(item) or None, where item is the next element in the collection: fn next(&mut self) -> Option<Self::Item> You can manually step through a collection by calling next() on an iterator instance, it.next() which returns Option<T>(item).

3.2.1 Operations with Iterators

The standard iterators provide a set of adapters, including:
  1. map: fn map<B,F>(self, f:F) -> Map<self, F> where F: FnMut(Self::Item) -> B
    Here, the map function takes a closure f:F that accepts an instance of the associated type Item and returns some computed value of type B. The associated type Item is the type of elements of the collection.
  2. filter: fn filter<P>(self, predicate: P) -> Filter<Self, P> where P: FnMut(&Self::Item) -> bool
    Filter function takes a predicate closure P that determines whether an element of the collection is sent to the output.
  3. collection: fn collect<B>(self) -> B where B: FromIterator<Self::Item>
    Turns an iterable collection into a different collection.
Here's a set of examples of iterator and adapter use:
Iterator Examples: Helper functions use display::{*}; #[allow(dead_code)] fn show_prefix(p:&str) { print!("{}", p); } #[allow(dead_code)] /*-- note: Iterator has an associated type, Item --*/ fn show_csl<T>(mut t:T) where T:Iterator, T::Item:std::fmt::Debug, { print!("{:?}", t.next().unwrap()); // no leading comma for val in t { print!(", {:?}", val); // leading comma } } #[allow(dead_code)] /*-- note: Iterator has an associated type, Item --*/ fn display_csl<T>(mut t:T) where T:Iterator, T::Item:std::fmt::Display, { print!("{}", t.next().unwrap()); // no leading comma for val in t { print!(", {}", val); // leading comma } } #[allow(dead_code)] fn show_pcsl<T>(t:T) where T:Iterator, T::Item:std::fmt::Debug, { show_prefix("\n "); show_csl(t); } #[allow(dead_code)] fn display_pcsl<T>(t:T) where T:Iterator, T::Item:std::fmt::Display, { show_prefix("\n "); display_csl(t); } Output: Demonstrating Iterators ========================= -- demo iter over vec of ints -- 1 -1 2 -2 3 -3 -- demo comma separated display -- 1, -1, 2, -2, 3, -3 -- demo iter over instance of map type -- ("two", 2) ("zero", 0) ("one", 1) ("three", 3) -- demo iter over array of floats -- 1 2.2 3.3 4.4 5.5 -- demo comma separated display -- 1.0, 2.2, 3.3, 4.4, 5.5 -- demo iter over array of strs -- "one", "two", "three", "four" -- demo using Display trait instead of Debug -- one, two, three, four -- demo map function modifying items -- "onez" "twoz" "threez" "fourz" -- demo collect items into Vec -- ["onez", "twoz", "threez", "fourz"] That's all Folks! Iteration Code main_title("Demonstrating Iterators"); putline(); shows("\n-- demo iter over vec of ints --"); let v = vec![1, -1, 2, -2, 3, -3]; print!("\n "); let it = v.iter(); for val in it { print!("{} ", val); } shows("\n-- demo comma separated display --"); show_pcsl(v.iter()); putline(); shows("\n-- demo iter over instance of map type --"); show_prefix("\n "); use std::collections::HashMap; let mut map:HashMap<&str, i32> = HashMap::new(); map.entry("zero").or_insert(0); map.entry("one").or_insert(1); map.entry("two").or_insert(2); map.entry("three").or_insert(3); let it_map = map.iter(); for val in it_map { print!("{:?} ", val); } putline(); shows("\n-- demo iter over array of floats --"); let a = [1.0, 2.2, 3.3, 4.4, 5.5]; print!("\n "); let ita = a.iter(); for val in ita { print!("{} ", val); } shows("\n-- demo comma separated display --"); let iter = a.iter(); show_pcsl(iter); putline(); shows("\n-- demo iter over array of strs --"); let mut s = [ "one", "two", "three", "four" ]; show_pcsl(s.iter()); shows( "\n-- demo using Display trait instead of Debug --" ); display_pcsl(s.iter()); putline(); shows("\n-- demo map function modifying items --"); show_prefix("\n "); let iter = s.iter_mut().map(|item| { let mut mod_item:String = item.to_string(); mod_item.push('z'); mod_item // returning String, not str }); for val in iter { print!("{:?} ",val); } putline(); shows("\n-- demo collect items into Vec --"); let iter = s.iter_mut().map(|item| { let mut mod_item:String = item.to_string(); mod_item.push('z'); mod_item // returning String, not str }); let v:Vec::<String> = iter.collect(); print!("\n {:?}", v); putline(); println!("\n That's all Folks!\n");
In the next section we look briefly at structs and traits, focusing on implementation of methods. In the next chapter we will dig more deeply into structs and their implementation of types.

3.3 Structs and Methods

Rust structs, like enums, come in three forms:
  1. StructExprStruct
    struct Person1 { name:String, occup:String, id:u32, }
  2. StructExprTuple
    struct Person2 ( String, String, u32 );
  3. StructExprUnit
    struct Person3;
Struct methods are analogous to class methods in C++. They are implemented in an "impl" block decorated with the struct type name, like this:
Method implementation struct Person1 { name:String, occup:String, id:u32, } impl Person1 { fn show(&self) { print!("\n Person1: {:?}", &self); } }
Here, the method Person1::show is simply delegating its responsibility to the print! macro. The self:Self plays the same role as the this pointer in C++. The examples below illustrate both method implementation and the implementation of traits, which we discuss in the next subsection.
Method Implementation Examples: Method implementations: #[allow(unused_imports)] use display::{*}; use std::fmt; /*-- basic demo --*/ #[derive(Debug)] struct Person1 { name:String, occup:String, id:u32, } #[allow(dead_code)] impl Person1 { fn show(&self) { print!("\n Person1: {:?}", &self); } } #[derive(Debug)] struct Person2 ( String, String, u32 ); #[allow(dead_code)] impl Person2 { fn show(&self) { print!("\n Person2: {:?}", &self); } } #[derive(Debug)] struct Person3; #[allow(dead_code)] impl Person3 { fn show(&self) { print!("\n Person3"); } } /*-- enums used for Demo1 and Demo2 --*/ #[derive(Debug, PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum Level { Basic, Intermediate, Advanced } #[derive(Debug, PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum Topic { Rust, Cpp, Design, } /*-- Struct Demo1 has private data --*/ #[derive(Debug)] pub struct Demo1 { name: String, level: Level, topic: Topic, } /*-- implement methods for Demo1 --*/ #[allow(dead_code)] impl Demo1 { pub fn new() -> Self { // set default values Self { name:String::from(""), level: Level::Basic, topic: Topic::Rust, } } pub fn set_name(&mut self, s:&str) { self.name = s.to_string(); } pub fn get_name(&self) -> &String { &self.name } pub fn set_level(&mut self, l:Level) { self.level = l; } pub fn get_level(&self) -> &Level { &self.level } pub fn set_topic(&mut self, t:Topic) { self.topic = t; } pub fn get_topic(&self) -> &Topic { &self.topic } } /*-- struct Demo2 has public data --*/ #[derive(Debug)] pub struct Demo2 { pub name: String, pub level: Level, pub topic: Topic, } /*-- implement Display trait for Demo2 --*/ #[allow(dead_code)] impl fmt::Display for Demo2 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "Demo2 { { name: \"{}\", level: {:?}, topic: {:?} } }", self.name, self.level, self.topic) } } /*-- implement Default trait for Demo2 --*/ impl Default for Demo2 { fn default() -> Self { Self { name: String::from(""), level: Level::Basic, topic: Topic::Rust, } } } /*-- implement init method for Demo2 --*/ impl Demo2 { pub fn init(self) -> Demo2 { Demo2 { name: self.name, level: self.level, topic: self.topic, } } } Using Code: sub_title("Demonstrating Basic Structs"); let p1 = Person1 { name:"Jim".to_string(), occup:"dev".to_string(), id:42 }; p1.show(); let p2 = Person2 { 0:"Jim".to_string(), 1:"dev".to_string(), 2:42 }; p2.show(); let p3 = Person3; p3.show(); putline(); sub_title("Demonstrating Demo1 Struct"); let mut demo1 = Demo1::new(); demo1.set_name("Demo1 probe"); print!("\n {:?}", demo1); print!( "\n Demo1 level = {:?}", demo1.get_level() ); putline(); sub_title("Demonstrating Demo2 Struct"); let mut demo2 = Demo2 { name: String::from("Jim's Demo2"), ..Default::default() }.init(); print!( "\n Using Debug format\n {:?}", demo2 ); print!( "\n setting level to Intermediate:" ); demo2.level = Level::Intermediate; print!( "\n Using Display format\n {}", demo2 ); putline(); println!("\n\n That's all Folks!\n"); Output: Demonstrating Basic Structs ----------------------------- Person1: Person1 { name: "Jim", occup: "dev", id: 42 } Person2: Person2("Jim", "dev", 42) Person3 Demonstrating Demo1 Struct ---------------------------- Demo1 { name: "Demo1 probe", level: Basic, topic: Rust } Demo1 level = Basic Demonstrating Demo2 Struct ---------------------------- Using Debug format Demo2 { name: "Jim\'s Demo2", level: Basic, topic: Rust } setting level to Intermediate: Using Display format Demo2 { name: "Jim's Demo2", level: Intermediate, topic: Rust } That's all Folks!
Note that a struct may have any finite number of impl blocks.

3.3.1 Traits

"Traits are the abstract mechanism for adding functionality to types and establishing relationships between them." - Steve Donovan, Rustifications . Traits have two uses:
  1. They act like interfaces, defining a contract for use, e.g.:
    trait Speaker { fn salutation(&self) -> String; }
    Traits Example: Code in Rust playground Implementing Code: trait Speaker { fn salutation(&self) -> String; } /////////////////////////////////////////////////////////// // The following structs act like classes that implement // a Speaker interface #[derive(Debug,Copy,Clone)] pub struct Presenter; impl Speaker for Presenter { fn salutation(&self) -> String { "Hello, today we will discuss ...".to_string() } } #[derive(Debug)] pub struct Friend { pub name : String, } impl Speaker for Friend { fn salutation(&self) -> String { let mut s = String::from( "Hi good buddy, its me, " ); let nm = self.name.as_str(); s.push_str(nm); return s; } } impl Friend { pub fn new(name : String) -> Self { Self { name, } // no semicolon so self } // is returned } #[derive(Debug,Copy,Clone)] pub struct TeamLead; impl Speaker for TeamLead { fn salutation(&self) -> String { "Hi, I have a task for you ...".to_string() } } Using Code: let presenter : Presenter = Presenter; let joe : Friend = Friend::new("Joe".to_string()); let sue : Friend = Friend::new("Sue".to_string()); let team_lead : TeamLead = TeamLead; let mut v :Vec<&dyn Speaker> = Vec::new(); v.push(&presenter); v.push(&joe); v.push(&sue); v.push(&team_lead); for speaker in v.iter() { print!("\n {:?}",speaker.salutation()); } Output: -- demo polymorphic struct instances -- "Hello, today we will discuss ..." "Hi good buddy, its me, Joe" "Hi good buddy, its me, Sue" "Hi, I have a task for you ..."
  2. They act as generic constraints, enforcing compile failure if a generic argument does not satisfy a required trait, e.g.:
    fn demo_ref<T>(t:&T) where T:Debug { show_type(t); show_value(t); }
There are a number of commonly used traits defined in the std library:
  • Debug and Display for displaying values on the console and in formatted strings.
  • Copy can only be implemented by blitable types. If you implement Copy then you must also implement Clone. However, you can implement Clone for types that are not blittable. Many of the Rust containers implement Clone.
  • ToString for values convertable to strings.
  • Default used to set default values.
  • From and Into for conversions. If you implement From then Into is implemented by the compiler.
  • The std library implements FromStr for numeric types.
Many of these traits can be implemented by derivation, e.g., #[derive(Debug, Clone, ...)]. The Interfaces Examples, above, has examples of this use.
You will find more useful traits discussed in the Steve Donovan citation in the quote at the top of this section.

3.4 Enumerations and Matching

We saw, in the previous chapter, how enumerations are defined. Here, we are concerned with common uses, especially with matching. Here's the matching syntax:
Explicit match syntax let value = Name::Jack; match value { Name::John => print!("\n I am John"), Name::Jim => print!("\n I am Jim"), Name::Jack => print!("\n I am Jack") } if else match syntax let value = Name::Jack; if let Name::Jim = value { print!("\n I am Jim"); } else { print!("\n I am not Jim"); }
The second "=" token in the if else syntax block is not an assignment. It is a match operator. Some of the enumeration syntax for matching and if let can be verbose. Here's examples that show how to use the enumeration types:
Enumeration and Matching Examples: Enumeration Code use display::{*}; use std::fmt::{Debug}; #[allow(dead_code)] #[derive(Debug)] enum Name { John, Jim=42, Jack } #[allow(dead_code)] #[derive(Debug)] enum NameTuple { John(String, u32), Jim(String, u32), Jack(String, u32) } #[allow(dead_code)] #[derive(Debug)] enum NameStruct { John { occup:String, id:u32 }, Jim { occup:String, id:u32 }, Jack { occup:String, id:u32 } } fn main() { main_title("Demonstrating enum_probes"); print!("\n - enumerations, match, if let"); putline(); /*-- enum discriminant with explicit matching --*/ sub_title(" -- enum discriminant -- "); let test = Name::Jim; match test { Name::John => { let john_discriminant = Name::John as u32; print!( "\n I am John. my discriminant is {:?}", john_discriminant )}, Name::Jim => { let jim_discriminant = Name::Jim as u32; print!( "\n I am Jim. my discriminant is {:?}", jim_discriminant )}, Name::Jack => { let jack_discriminant = Name::Jack as u32; print!( "\n I am Jack. my discriminant is {:?}", jack_discriminant )}, } putline(); let test1 = Name::John; let test2 = Name::Jim; let test3 = Name::Jack; if let Name::Jack = test1 { print!("\n I am John"); } else { print!("\n I am not John"); } if let Name::Jack = test2 { print!("\n I am Jim"); } else { print!("\n I am not Jim"); } if let Name::Jack = test3 { print!("\n I am Jack"); } else { print!("\n I am not Jack"); } putline(); /*-- enum tuple with if let matching --*/ sub_title(" -- enum tuple -- "); let value = NameTuple::John("pilot".to_string(), 52); if let NameTuple::John(occup, id) = value { print!( " my name is John occupupation is {} id is {}", occup, id ); } putline(); /*-- enum struct with match and all else --*/ sub_title(" -- enum struct -- "); let value = NameStruct::Jack { occup:"plumber".to_string(), id:32 }; match value { NameStruct::Jack {occup, id} => print!("\n Jack - occup: {}, id: {}", occup, id), _ => print!("\n not Jack") } putline(); println!("\n\nThat's all Folks!\n"); } Output: Demonstrating enum_probes =========================== - enumerations, match, if let -- enum discriminant -- ----------------------------- I am Jim. my discriminant is 42 I am not John I am not Jim I am Jack -- enum tuple -- ---------------------- my name is John occupupation is pilot id is 52 -- enum struct -- ----------------------- Jack - occup: plumber, id: 32 That's all Folks!
Both forms of matching are widely used in Rust code. If you work some code examples until you are comfortable with these operations, you will find it much easier to understand the Rust literature.

3.5 Epilogue

Rust has a rich set of operations that make it very expressive, once you become familiar with their idomatic use. They do make the learning process interesting (The good news is ..., the bad news is ...).

3.6 Exercises

  1. Write a function heading that accepts a str and displays it on the console with a hypenated line above and below. The lines should be two characters longer that the string with the string presented one character indented from the start of the lines above and below.
  2. Write a closure that does the same thing. Can you write the closure in a single line of code?
  3. Repeat the last exercise, but supply a prefix that is used on all three lines, i.e., "\n ".
  4. Write a function that prompts for, and accepts, an integer provided by the user. Return a Result from the function that returns Ok(42) or Err("you didn't enter 42"), and display the result without inducing a panic.
  5. Write a function that displays a collection of values on the console, separated by commas. Make sure you don't have a leading or final trailing comma.
  6. Repeat the last exercise using an iterator.
  7. For the struct you implemented in the exercises for the Data chapter, add a method that displays the struct in some pleasing format on the console.
  8. For this struct, implement the Clone trait and demonstrate that it works.

3.7 References

Reference Link Description
Rust Reference Rust Reference is the official language definition - surprisingly readable.
Functions that accept both String and str Creating a Rust function that accepts String or &str - Herman Radtke
Functions that return either String or str Creating a Rust function that returns a &str or String - Herman Radtke
Avoiding borrow problems with Getter functions Getter Functions In Rust - Herman Radtke
and_then, map combinators Using option and result effectively - Herman Radtke
Common Traits Clear descriptions of many of the common Rust traits
Iter, IntoIter, Map/Reduce Effectively Using Iterators in Rust - Herman Radtke
Ending borrow Strategies for solving 'cannot move out of' borrowing errors in Rust - Herman Radtke
Working with Files and Doing File I/O Linux Journal - Mihalis Tsoukalos, June 20, 2019
  Next Prev Pages Sections About Keys