about
04/28/2022
RustBites - Functions
Rust Bite - Functions
functions, generic functions, closures
-
Functions:
The Rust syntax for functions is familiar: Function Syntax - Pass by Value fn f(t:T, u:U, ...) -> V { /* code that uses t, u, ... and returns an instance of V */ } In Rust, the value of a scope is the value of its last expression (a statment with no semicolon). It is standard practice for Rust functions to make the return value the result of a final expression, although using return statements are also syntactically correct. The first "Function Examples" dropdown illustrates how that works. Arguments can also be passed by reference: Function Syntax - Pass by Reference fn g(rt:&T, ru:&U, ...) { /* code that uses references rt, ru, ... */ } Function Examples:
functions_and_methods::main.rs // functions_and_methods::main.rs fn f(mut s:String, t:String) -> String { s.push(' '); s.push_str(&t); s } fn g(s:&mut String, t:&str) -> String { s.push(' '); s.push_str(t); s.to_string() } fn main() { print!("\n -- passing function arguments by value --"); let s1 = String::from("a string"); print!("\n s1 = {:?}", s1); let s2 = String::from("and more"); print!("\n s2 = {:?}", s2); let s3 = f(s1, s2); print!("\n s3 = {:?}", s3); // print!("\n s2 = {:?}", s2); print!("\n can't print s2, it's been moved"); // print!("\n s1 = {:?}", s1); print!("\n can't print s1, it's been moved"); println!(); print!("\n -- passing function arguments by reference --"); let mut s1 = String::from("a refreshed string"); let s2 = "and a new more"; let s3 = g(&mut s1, s2); print!("\n s3 = {:?}", s3); print!("\n s2 = {:?}", s2); print!("\n s1 = {:?}", s1); print!("\n note: s1 has been changed as a side-effect"); print!("\n\n That's all Folks!\n\n"); } Output: cargo run -q -- passing function arguments by value -- s1 = "a string" s2 = "and more" s3 = "a string and more" can't print s2, it's been moved can't print s1, it's been moved -- passing function arguments by reference -- s3 = "a refreshed string and a new more" s2 = "and a new more" s1 = "a refreshed string and a new more" note: s1 has been changed as a side-effect That's all Folks! It doesn't make sense to return by reference for free-standing functions, as the references would be invalid after the return. Of course, g could create an instance of something on the heap, using Box. But it is as cheap to return a Box by value as a reference, since the Box is the size of a pointer. We will see that it does make sense to return references from methods where the references are bound to member data. We will look at that in a later section. -
Special Return Types:
Rust provides two enumerations to support special needs for function return values. The first is Option: Option Enumeration enum Option<T> { Some(T), None, } For example, if the function is passed a collection from which it computes a value to return, if the collection is empty, the function may have nothing to return, so it can return None, rather that Some(t:T). For functions that may encounter an error during processing, Rust provides the enumeration Result: Result Enumeration enum Result<T> { Ok(T), Err(E), } Ok(f: File) . If not, it returnsErr(err: std::io::Error) .Below, find examples that illustrate use of these return types in a practical setting. Examples of Return Types:
return_values::main.rs ///////////////////////////////////////////////////////////// // return_values::main.rs - demo Option<T> and Result<T,E> // // // // Jim Fawcett, https://JimFawcett.github.io, 14 Jun 2020 // ///////////////////////////////////////////////////////////// #![allow(dead_code)] /*-- show that scopes evaluate to their last expression --*/ fn demo_scope_value() { let v1 = { 2 + 3 }; assert_eq!(v1, 5); let v2 = { let s = "a string slice"; s }; assert_eq!(v2, "a string slice"); let v3 = { let _s = "another string"; }; assert_eq!(v3, ()); } /*-- demo Option<T> with collection --*/ fn sum(v:&Vec<i32>) -> Option<i32> { if v.is_empty() { return None; } let iter = v.iter(); let sum = iter.sum(); Some(sum) } /*-- demonstrate file error handling with Result<T,E> --*/ use std::fs::File; use std::io::prelude::*; use std::io::{ Result }; /*-- unwrap or bubble up error (uobue) --*/ fn create_test_file(f:&str, cnt:&str) -> Result<File> { print!("\n attempting to create file {:?}", f); let mut file = File::create(f)?; //uobue print!("\n attempting to write contents {:?}", cnt); file.write_all(cnt.as_bytes())?; // uobue Ok(file) } /*-- unwrap or bubble up error (uobue) --*/ fn open_file_and_read(f:&str) -> Result<String> { print!("\n attempting to open file {:?}", f); let mut file = File::open(f)?; //uobue print!("\n attempting to read contents"); let mut buf = String::new(); file.read_to_string(&mut buf)?; // uobue Ok(buf) } /*-- run demonstrations --*/ fn main() -> Result<()> { demo_scope_value(); /*-----------------------------------------------------*/ print!("\n -- demonstrate Option --"); let v = Vec::<i32>::new(); let opt = sum(&v); if opt.is_none() { print!("\n no content to sum"); } let v = vec![1, 2, 3, 4, 5]; let opt = sum(&v); if opt.is_some() { print!( "\n sum of {:?} is {}", v, opt.unwrap() ); } println!(); /*-------------------------------------------------*/ print!("\n -- demonstrate Result<T,E> --"); let file_name = "new_file.txt"; let content = "\n first line\n second line"; print!( "\n attempting to create file {:?}", file_name ); let rslt = File::create(file_name); if rslt.is_ok() { print!("\n open succeeded"); let content = "\n first line\n second line"; print!("\n attempting to write {:?}", content); let mut fl = rslt.unwrap(); let wrslt = fl.write_all(content.as_bytes()); if wrslt.is_ok() { print!("\n write succeeded"); } else { print!("\n write failed"); } } print!("\n attempting to open {:?}", file_name); let rslt = File::open(file_name); if rslt.is_ok() { print!("\n attempting to read contents"); let mut f = rslt.unwrap(); let mut buf = String::new(); let rrslt = f.read_to_string(&mut buf); if rrslt.is_ok() { print!("\n contents:{}", buf); } else { print!("\n can't read file {:?}", file_name); } } println!(); /*-------------------------------------------------*/ print!("\n -- demonstrate Result<T,E> using ? --"); let mut _file = create_test_file(file_name, content)?; // uobue /*-- write more text using _file --*/ _file.write_all( b"\n third line\n fourth line" )?; // uobue let text = open_file_and_read(file_name)?; // uobue print!("\n file contents:{}", text); println!("\n\n That's all Folks!\n\n"); Ok(()) } Output: cargo run -q -- demonstrate Option -- no content to sum sum of [1, 2, 3, 4, 5] is 15 -- demonstrate Result<T,E> -- attempting to create file "new_file.txt" open succeeded attempting to write "\n first line\n second line" write succeeded attempting to open "new_file.txt" attempting to read contents contents: first line second line -- demonstrate Result<T,E> using ? -- attempting to create file "new_file.txt" attempting to write contents "\n first line\n second line" attempting to open file "new_file.txt" attempting to read contents file contents: first line second line third line fourth line That's all Folks! One thing to note about the examples, above: If the last construct in a scope is an expression (a statement minus its semicolon), then that is treated as the value of the scope. If there is no terminating expression, the value of the scope is (), the unit, signifying no value. See demo_scope_value() in the Examples, above, for confirmation. If a function signature declares some return value and if the function scope ends with an expression, the value of the expression is treated as the return value. Return statements also work, but ending expressions are idiomatic Rust. You will observe that all the functions, including main, in the examples, above, use terminal expressions to return results. -
Generic Functions:
Generic functions accept arguments, one or more of which depend on an unspecified type. Generic types are resolved to concrete types when the invoking code is compiled. Usually a generic argument is constrained by one or more traits. In the case of the generic function, show below, T is constrained to support the Debug trait - Debug specifies a display format suitable for debugging applications. If the invoking argument does not satisfy constraints imposed by the function, it will fail to compile. Generic Function /*------------------------------------------ Show slice as stack of rows - span is number of items in row - width is size of field holding item */ fn show_fold<T:Debug>( t:&[T], span:usize, width:usize ) { print!("\n "); let mut count = 0; for item in t { count = count + 1; print!("{:wd$?}", item, wd = width); if count == span { print!("\n "); count = 0; } } } Using Code: fn demo_array_int() -> Option<()> { // code elided // - includes function returning Option let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; show_fold(&arr, 5, 4); Some(()) } Output: // output elided 0 1 2 3 4 5 6 7 8 9 10 11 In the example shown in the dropdown, below, you will find a generic function used to display collection contents to the console. It displays a comma separated list of values from a collection of arbitrary type that satisfies the constraints: the collection must define the IntoIterator trait, to support iterating through the collection, it must support the clone type to avoid consuming the collection, and the collection and its items must satisfy the Debug trait. We will discuss traits in some detail in the Traits Bite. Generic Function that displays collections: show_coll:
collections::main.rs use std::fmt::Debug; use std::any::*; /*----------------------------------------------------------- Display comma separated list of collection items - shows how to build function accepting iterable collections - returns Option::None if collection is empty */ fn show_coll<C>(c:&C) -> Option<()> where C:IntoIterator + Clone + Debug, C::Item: Debug { let mut iter = c.clone().into_iter(); /*------------------------------------------- returns Option if no next element using ? try operator */ print!("\n {:?}", iter.next()?); /*-- breaks when no next element --*/ for item in iter { print!(", {:?}", &item); } Some(()) } Using Code: fn demo_array_int() -> Option<()> { print!("\n === demo_array_int ==="); let mut arr : [i32; 5] = [0; 5]; show_coll(&&arr)?; arr = [1, 2, 3, 4, 5]; show_coll(&&arr)?; let arr_slice = &arr[..]; show_coll(&arr_slice); arr[1] = -1; show_coll(&&arr)?; let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; show_fold(&arr, 5, 4); Some(()) } fn demo_vec_int() { print!("\n === demo_vec_int ==="); let mut vec = vec![1, 2, 3, 4, 5]; show_coll(&vec); print!("\n -- assign vectors --"); vec = vec![5, 4, 3, 2, 1]; show_coll(&vec); // rest of code elided } Output: === demo_array_int === 0, 0, 0, 0, 0 1, 2, 3, 4, 5 1, 2, 3, 4, 5 1, -1, 3, 4, 5 0 1 2 3 4 5 6 7 8 9 10 11 === demo_vec_int === 1, 2, 3, 4, 5 -- assign vectors -- 5, 4, 3, 2, 1 // rest of output elided In the dropdown, below, you will find two functions used to display information about the value and type of a generic parameter. These can be useful when you are investigating a third-party library or trying to get your own code to compile and run correctly. The first of the two functions uses 'a to denote a lifetime. Rust tracks the lifetimes of all references to be sure they do not outlive their referends. The compiler's borrow checker occasionally needs help to determine a lifetime, and that is done with this notation. We will discuss this more in the lifetime Bite. Examples of Generic Display Functions:
collections::main.rs use std::any::*; /*----------------------------------------------------------- Display type name of generic input argument - returns reference, &str, to literal string - requires lifetime annotation, 'a - argument name _t tells compiler we don't intend to use the argument, just its type T */ fn show_type<'a, T:Debug>(_t:T) -> &'a str where T:Debug { type_name::<T>() } use std::fmt::Debug; /*----------------------------------------------------------- Display argument name, argument value, and type of generic input, t:T */ fn show_type_value<T:Debug>(name:&str, t:&T) { print!("\n value of {} = {:?}", name, t); print!("\n type is: {}", show_type(t)); } Using Code: fn demo_struct() { print!("\n -- demo_struct --"); #[derive(Debug)] struct S { a:i8, b:String, c:f64 } let s: S = S { a:42, b:String::from("String"), c:3.1415927 }; assert_eq!(s.a, 42); show_type_value("s", &s); print!( "\n s.a = {}, s.b = {}, s.c = {}", s.a, s.b, s.c ); println!(); #[derive(Debug)] struct T(i8, String, f64); let t:T = T(42, String::from("String"), 3.1415927); assert_eq!(t.0, 42); show_type_value("t", &t); print!( "\n t.0 = {}, t.1 = {}, t.2 = {}", t.0, t.1, t.2 ); println!(); // code elided } Output: -- demo_struct -- value of s = S { a: 42, b: "String", c: 3.1415927 } type is: &collections::demo_struct::S s.a = 42, s.b = String, s.c = 3.1415927 value of t = T(42, "String", 3.1415927) type is: &collections::demo_struct::T t.0 = 42, t.1 = String, t.2 = 3.1415927 // output elided In the last dropdown for this section you will find basic and more advanced topics: - pass and return by value and reference
- functions that take and return function arguments
- dynamic dispatch - compared to static dispatch
- local functions
Examples: Summary of function topics:
functions::main.rs ///////////////////////////////////////////////////////////// // functions_and_methods::main.rs - functions Bite demos // // // // Jim Fawcett, https://JimFawcett.github.com, 19 Jun 2020 // ///////////////////////////////////////////////////////////// // https://stackoverflow.com/questions/36390665 // /how-do-you-pass-a-rust-function-as-a-parameter // https://joshleeb.com/blog/rust-traits-trait-objects/ /*----------------------------------------------------- pass arguments by value - passing by value results in Move for non-copy types - we say that the s and t arguments are consumed */ fn f(mut s:String, t:String) -> String { s.push(' '); s.push_str(&t); s // value of last expression is returned } /*----------------------------------------------------- pass arguments by reference - passing s and t does not consume their referends */ fn g(s:&mut String, t:&str) -> String { s.push(' '); s.push_str(t); s.to_string() } /*----------------------------------------------------- h_in_1 accepts function argument via static dispatch - F: Fn ... specifies accepted function signatures */ fn h_in_1<F: Fn(i32) -> String>(i:i32, f: F) -> String { f(i) } /*----------------------------------------------------- h_in_2 accepts function argument via dynamic dispatch - Fn ... specifies accepted function signatures */ fn h_in_2(i:i32, f: &dyn Fn(i32) -> String) -> String { f(i) } /*----------------------------------------------------- function passed to, and returned from demonstration functions, below */ fn test_function(i:i32) -> String { i.to_string() } /*----------------------------------------------------- static dispatch of returned function - uses impl to specify that return satisfies Fn ... */ fn h_out_1() -> impl Fn(i32) -> String { &test_function } /*----------------------------------------------------- dynamic dispatch of returned function - uses lifetime annotation 'a - &dyn to include vtable */ fn h_out_2<'a>() -> &'a dyn Fn(i32) -> String { &test_function } Using Code: fn main() { print!( "\n -- passing function args by value --" ); let s1 = String::from("a string"); print!("\n s1 = {:?}", s1); let s2 = String::from("and more"); print!("\n s2 = {:?}", s2); let s3 = f(s1, s2); print!("\n s3 = {:?}", s3); // print!("\n s2 = {:?}", s2); print!("\n can't print s2, it's been moved"); // print!("\n s1 = {:?}", s1); print!("\n can't print s1, it's been moved"); println!(); print!( "\n -- passing function args by reference --" ); let mut s1 = String::from("a refreshed string"); let s2 = "and a new more"; let s3 = g(&mut s1, s2); print!("\n s3 = {:?}", s3); print!("\n s2 = {:?}", s2); print!("\n s1 = {:?}", s1); print!( "\n note - s1 has been changed as side-effect" ); println!(); print!("\n -- passing function as argument --"); /*-- static dispatch --*/ let s = h_in_1(42, &test_function); print!("\n s = {}", s); /*-- dynamic dispatch --*/ let s = h_in_2(42, &test_function); print!("\n s = {}", s); println!(); print!("\n -- function returning function --"); let s = h_out_1()(42); // static dispatch print!("\n s = {}", s); let s = h_out_2()(42); // dynamic dispatch print!("\n s = {}", s); println!(); print!( "\n -- defining function inside function --" ); fn whooaaa() { print!("\n whooaaa - inside main!"); } whooaaa(); print!("\n\n That's all Folks!\n\n"); } Output: cargo run -q -- passing function arguments by value -- s1 = "a string" s2 = "and more" s3 = "a string and more" can't print s2, it's been moved can't print s1, it's been moved -- passing function arguments by reference -- s3 = "a refreshed string and a new more" s2 = "and a new more" s1 = "a refreshed string and a new more" note that s1 has been changed as a side-effect -- passing function as argument -- s = 42 s = 42 -- function returning function -- s = 42 s = 42 -- defining function inside another function -- whooaaa - inside main! That's all Folks! In the next section we discuss closures, sometimes call lambdas. They work a lot like locally defined functions, although their syntax is quite different, and they can do one thing that functions can't. -
Closures:
The code below defines a closure and binds it to a mutable variable add. Closures are unnamed function objects that are invoked like functions. They differ from functions by accepting data by reference or value, both as input parameters, as t, below, or by capturing from the local context, as sum, below. Here, sum is captured by mutable reference. Closure Definition let sum = 1; let mut add = |t: &i32| -> i32 { sum += t; sum }; Invocation let rsl = add(&2); // sum captured by closure add In the examples dropdown, below, we illustrate two closures. The first, shown above, captures the variable sum by mutable reference. Because it holds its closure data by reference it should not be passed out of its scope of definition. In the second closure, in demo2(), the variable sum is moved into the closure and so owned by the closure. Thus it is safe to pass this closure out of its scope of definition. Examples of Closures
closures::main.rs ///////////////////////////////////////////////////////////// // closures::main.rs - not so basic demo of closure // // // // Jim Fawcett, https://JimFawcett.github.io, 22 Jun 2020 // ///////////////////////////////////////////////////////////// /* Closure are like functions, except that they can capture data used for their evaluation. */ fn demo1() { print!("\n -- closure demo1 --"); let v1 = vec![1, 2, 3, 4, 5, 6]; print!("\n v1 = {:?}", v1); let mut sum = 0; // capture of sum as mutable borrow let mut add = |t: &i32| -> i32 { sum += t; sum }; for i in &v1 { add(i); } /*----------------------------------------------- statement below fails to compile as sum is already borrowed as mutable ----------------------------------------------- print!("\n sum = {}", sum); add returns immutable copy of sum so this works: */ print!("\n sum = {}", add(&0)); let v2 = vec![0, -1, -2]; print!("\n v2 = {:?}", v2); for i in &v2 { add(i); } print!("\n sum = {}", add(&0)); let v3 = vec![6, 4, 2, 0, -2, -4]; print!("\n v3 = {:?}", v3); for _i in v3.clone().iter().map(add) {} /*----------------------------------------------- add moved into map, ends borrow of sum, so can use sum, but can't use add(&0) */ print!("\n sum = {}", sum); } fn demo2() { print!("\n -- closure demo2 --"); let v1 = vec![1, 2, 3, 4, 5, 6]; let v2 = vec![0, -1, -2]; let mut sum = 0; // capture of sum as move let mut add = move |t: i32| -> i32 { sum += t; //print!("\n--{}", sum); sum }; for i in &v1 { add(*i); } print!("\n sum = {}", add(0)); for i in &v2 { add(*i); } print!("\n sum = {}", add(0)); let v3 = vec![6, 4, 2, 0, -2, -4]; for item in v3.clone().iter() { add(*item); } print!("\n sum = {}", add(0)); } Using Code fn main() { demo1(); println!(); demo2(); println!("\n\n That's all Folks!\n\n"); } Output: cargo run -q -- closure demo1 -- v1 = [1, 2, 3, 4, 5, 6] sum = 21 v2 = [0, -1, -2] sum = 18 v3 = [6, 4, 2, 0, -2, -4] sum = 24 -- closure demo2 -- sum = 21 sum = 18 sum = 24 That's all Folks! The next example is simple, but the technique is quite useful. It defines a function fn take_closure<F>(f:F) where F: FnOnce() -> bool FnOnce() -> bool specifying a function or closure taking no arguments and returning bool. The calliing context,fn main() , defines the closure, gives it capture data, then calls the accepting function.Example: Generic Function accepting Closure
closure_param::main.rs // closure_param::main.rs fn take_closure<F>(f:F) where F:FnOnce() -> bool { if f() { print!("\n closure returned true"); } else { print!("\n closure returned false"); } } fn main() { let s = "closure data".to_string(); let p = false; let c = || { print!("\n closure string = {:?}", s); p }; take_closure(c); println!("\n\n That's all Folks!\n\n"); } Output: C:\su\temp\closure_param> cargo run -q closure string = "closure data" closure returned false That's all Folks! Example: Generic Function returning Closure
closures::main.rs /*---------------------------------------------------- Demonstrate type name and operation of Rust closure - closure can be passed to, or returned from, another function ----------------------------------------------------*/ /* display typename of T */ fn show_type (_t: &T, nm: &str) { let typename = std::any::type_name:: (); println!("\n {nm}: {typename}"); } /* function returning closure */ fn cl_demo() -> impl Fn(&str) { let prefix = "\n "; /* closure using passed arg and prefix capture */ let cl = move |nm:&str| { println!("{}name: {nm:?}", prefix) }; cl } fn main() { let demo = cl_demo(); show_type(&demo, "cl"); demo("Arthur Dent"); println!("\n That's all Folks!\n"); } Output: > cargo run -q cl: closure_demo::cl_demo::{{closure}} name: "Arthur Dent" That's all Folks! The thing that makes closures so useful is that, unlike functions, they can accept "closure data". Essentially a closure is a type with a method that contains the closure code, and members that are created from closure data. The examples, above - especially the first - illustrate that it can sometimes be awkward to satisfy Rust's ownership rules when using closures. These examples show how to do that, but we may wish to use other ways of accomplishing the same thing. We will illustrate that in the next Structs Bite. -
Exercises:
- Write a function that accepts the name of a file containing a comma separated list of integers and produces a vector containing those integer values. You will need to convert the string representation to its numeric value.
- Modify your solution for the first exercise to return an Option<T> that holds the created vector or None if the file doesn't contain appropriate contents. What happens when you apply this to a binary file?
- Write a function that reverses the processing in the first exercise, e.g., accept a vector and create a file with a comma separated list of values.
- If you haven't already, convert the function of the last exercise to handle a vector of any numeric type.