about
RustStory Operations
9/15/2022

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. 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 somefunction(ArgType1 t1, ArgType2 t2, ...) -> ReturnType { ... } 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 pointer - 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 show_type(&s); // def in display::lib.rs show_value(&s); // def in display::lib.rs } Pass by Reference /*----------------------------------------------- Pass by ref borrows the argument. Borrow moved into the function's stack frame. Borrow ends at end of function call, so param is valid after call */ fn pass_by_ref_str(rs:&String) { // borrow show_type(&rs); // def in display::lib.rs show_value(&rs); // def in display::lib.rs } Pass by value Using Code: /*-------------------------------------------*/ separator(48); sub_title("Pass string by value"); let mut s = "xyz".to_string(); let s1 = s.clone(); pass_by_value_str(s1); // moves s1 //////////////////////////////////////////////// // next statement fails to compile - s1 moved // println!(s1); Pass by reference Using Code: /*-------------------------------------------*/ separator(48); sub_title("Pass string by reference"); 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); code in playground includes show_type and show_value. They 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 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 4Z 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; }
    Interfaces 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