about
RustStory Operations
9/15/2022
Chapter 3. - Rust Operations
Functions, function ptrs, methods, closures
3.0 Prologue
Operations Syntax
Function Syntax | |
---|---|
|
|
Function Example | |
|
|
Closure Syntax | |
|
|
Closure Example | |
|
|
Method Syntax | |
|
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 |
Method Example | |
|
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.
|
Function Object Syntax | |
|
Function objects are closures that capture data from an associated object.
The FO method |
Function Object Example | |
|
|
Function Pointer Syntax | |
|
|
Function Pointer Example | |
|
|
3.1 Functions
Function Signature | Behavior |
---|---|
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); |
|
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");
}
- 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");
}
- 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");
}
- 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");
}
- 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");
}
- 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.
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
- 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 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! |
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; } 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 ..." -
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 |