about
RustStory Operations
9/15/2022
Chapter 3. - Rust Operations
Functions, function ptrs, methods, closures
3.0 Prologue
3.1 Functions
- How you supply function arguments, e.g., the type signatures used.
- What you are allowed to do inside a function body.
- What signatures you use for return types.
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! |
3.1.1 Function Pointers
3.1.2 Closures
- FnOnce is implemented automatically by closures that may consume (move and use) captured variables.
- 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.
-
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 Examples:
3.1.3 Function Error Handling
-
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.
-
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.
Result and Option Syntax Examples:
File Error Handling
3.2 Iterators
3.2.1 Operations with Iterators
-
map:
fn map<B,F>(self, f:F) -> Map<self, F> whereF: FnMut(Self::Item) -> B
Here, the map function takes a closuref:F that accepts an instance of the associated typeItem and returns some computed value of typeB . The associated typeItem is the type of elements of the collection. -
filter:
fn filter<P>(self, predicate: P) -> Filter<Self, P> whereP: FnMut(&Self::Item) -> bool Filter function takes a predicate closureP that determines whether an element of the collection is sent to the output. -
collection:
fn collect<B>(self) -> B whereB: FromIterator<Self::Item> Turns an iterable collection into a different collection.
Iterator Examples:
3.3 Structs and Methods
-
StructExprStruct struct Person1 { name:String, occup:String, id:u32, } -
StructExprTuple struct Person2 ( String, String, u32 ); -
StructExprUnit struct Person3;
Method Implementation Examples:
3.3.1 Traits
-
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 ..." -
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); }
-
Debug andDisplay for displaying values on the console and in formatted strings. -
Copy can only be implemented by blitable types. If you implementCopy then you must also implementClone . However, you can implementClone for types that are not blittable. Many of the Rust containers implementClone . -
ToString for values convertable to strings. -
Default used to set default values. -
From andInto for conversions. If you implementFrom thenInto is implemented by the compiler. -
The std library implements
FromStr for numeric types.
3.4 Enumerations and Matching
Enumeration and Matching Examples:
3.5 Epilogue
3.6 Exercises
- 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.
- Write a closure that does the same thing. Can you write the closure in a single line of code?
- Repeat the last exercise, but supply a prefix that is used on all three lines, i.e., "\n ".
- 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.
- 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.
- Repeat the last exercise using an iterator.
- 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.
- 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 |