ib RustBite Options
about
05/04/2022
RustBites - Options
Code Repository Code Examples

Rust Bite - Options

Options, plug-ins

1.0 - Introduction

Rust type Option<T> is an enumeration that is designed to handle cases were a value or reference may or may not exist. The Some(T) element wraps an instance of T when it exists. Otherwise, it contains the element None. Option Type enum Option<T> { Some(T), None, } References: std::option
Rust defines iterators for most of its collection types. A Rust iterator defines the function:
fn next() -> Option<T>
where T is the Item type of the collection being iterated. During iteration next() returns Some(t). When the end is reached next() returns None.
This is illustrated by the example below. Usually we don't use the iterator directly, as done in function do_iter(v: &Vec<i32>) but instead use a for-in loop, as in function do_idiomatic_iter(v: &Vec<i32>). Iteration uses Option /* iterators provide next function that returns an Option containing Some(item) if there is a next item, otherwise None */ fn do_iter(v: &Vec<i32>) { print!("\n "); let mut iter = v.iter(); loop { let item = iter.next(); if item.is_none() { print!("{:?}", item); break; } print!("{:?} ", item); } } /*------------------------------------ Demonstrate idiomatic iteration for loop iterates and unwraps Option */ fn do_idiomatic_iter(v: &Vec<i32>) { print!("\n "); for item in v { print!("{:?} ", item); } } fn main() { do_iter(&vec![1, 2, 3]); do_idiomatic_iter(&vec![1, 2, 3]); } Output Some(1) Some(2) Some(3) None 1 2 3 Comments playground example In the example below, we show how to make these functions generic, and apply those to a variety of collection types. Since this has more to do with iteration than with Option<T> it is placed in a details dropdown, closed by default.
Iterating over Generic Types Functions iterate over generic types use std::fmt::*; use std::collections::HashMap; fn type_is<T>(_t:T) -> String { format!("{}", std::any::type_name::<T>()) } /*---------------------------------------- - Pass by ref to avoid moving t into function. - that prepends & char to type - remove first & char from type string by taking slice */ fn show_type<T>(t:&T) { let s = type_is(t); let s = &s[1..]; // remove leading & char print!("\n type is {}", s); } /*-------------------------------------- Demonstrate basic iteration */ fn do_iter<T>(iter: &mut T) where T: Iterator + Debug, T::Item: Debug { print!("\n "); loop { let item = iter.next(); if item.is_none() { break; } print!("{:?} ", item.unwrap()); } } /*------------------------------------ Demonstrate idiomatic iteration */ fn do_idiomatic_iter<T>(t:T) where T: IntoIterator, T::Item: Debug { print!("\n "); for item in t { print!("{:?} ", item); } } fn main() { let putln = || println!(); let putmsg = |msg:&str| print!("\n {}", msg); putmsg("-- iterating over array --"); let arr = [1, 2, 3]; show_type(&arr); do_iter(&mut arr.iter()); do_idiomatic_iter(&arr); putln(); putmsg("-- iterating over String --"); let s = "a string".to_string(); show_type(&s); do_iter(&mut s.chars()); /* String doesn't implement IntoIterator */ do_idiomatic_iter(s.bytes()); putln(); putmsg("-- iterating over String slice --"); let slice = &s[2..]; show_type(&slice); do_iter(&mut slice.char_indices()); /* &str doesn't implement IntoIterator */ do_idiomatic_iter(slice.bytes()); putln(); putmsg("-- iterating over byte array --"); let bytes = &s[2..].as_bytes(); show_type(&bytes); do_iter(&mut bytes.iter()); do_idiomatic_iter(&**bytes); putln(); putmsg("-- iterating over Vec --"); let v = vec![1, 2, 3]; show_type(&v); do_iter(&mut v.iter()); do_idiomatic_iter(&v); putln(); putmsg("-- iterating over HashMap --"); let mut h = HashMap::<String, i32>::new(); h.insert("zero".to_string(), 0); h.insert("one".to_string(), 1); h.insert("two".to_string(), 2); h.insert("three".to_string(), 3); show_type(&h); do_iter(&mut h.iter()); do_idiomatic_iter(&h); print!("\n\n That's all Folks\n\n"); } Output -- iterating over array -- type is [i32; 3] 1 2 3 1 2 3 -- iterating over String -- type is alloc::string::String 'a' ' ' 's' 't' 'r' 'i' 'n' 'g' 97 32 115 116 114 105 110 103 -- iterating over String slice -- type is &str (0, 's') (1, 't') (2, 'r') (3, 'i') (4, 'n') (5, 'g') 115 116 114 105 110 103 -- iterating over byte array -- type is &&[u8] 115 116 114 105 110 103 115 116 114 105 110 103 -- iterating over Vec -- type is alloc::vec::Vec 1 2 3 1 2 3 -- iterating over HashMap -- type is std::collections::hash::map::HashMap ("two", 2) ("one", 1) ("zero", 0) ("three", 3) ("two", 2) ("one", 1) ("zero", 0) ("three", 3) That's all Folks Comments playground example
There is a lot to learn about the different kinds of Rust collections and about generics in the dropdown, so you might want to come back an review later.

2. - Pluggin Architectures

The example, below, describes a pluggin architecture. User-defined types may use pluggins to interact with their environment in flexible ways. In Figure 1. we show a user-defined type, Plugged, that may use any pluggin that satisfies the Plug trait. In this Figure, Plugged may have either Pluggin1 or Pluggin2 installed. But when it first starts up, it doesn't have any.
Figure 1. Pluggable Architecture
One standard design has Plugged declare a member reference to an instance of it's Pluggin type. When it is created it has no pluggin until one is registered. That allows Plugged to accept different Pluggin instances for different needs. The idea is that, at run-time, the program can decide which of the available Pluggins would work well for its needs, and register the appropriate pluggin. That, however, has to be modified for Rust. Rust will not allow an uninitialized or null reference. That's where Option<T> is used. In the first example, shown below, Demo holds an Option<&'a mut T>. Before a Pluggin is registered Demo holds None. After registration, Demo holds Some(p) where p has type &'a mut T, where T is the specific type of the Pluggin. The 'a (tick a) is a generic lifetime parameter. Rust has to ensure that the lifetime of the Pluggin instance is at least as long as the lifetime of the Demo instance. The declaration:
pub struct Demo<'a, T> where T: Plug + Debug + 'a {
  pluggin: Option<&'a mut T>
}
says that:
  • Demo has lifetime 'a
  • T has at least lifetime 'a
  • Option holds a mutable reference to T with the same lifetime
All that allows the Rust compiler to check that the lifetime of the Pluggin includes the lifetime of Demo.
The borrow checker analyzes reference lifetimes, usually without any annotation by the developer. But for cases of dependency like this example it needs some help. You can find more information about lifetimes here: Rust Lifetime Annotation.
Option Holding Reference /*------------------------------------- Pluggin Architecture using Option<T> - Version 1 - holds Option of reference to T - requires annotating lifetimes so Rust can ensure that Demo doesn't outlive its pluggin */ use std::fmt::Debug; /*------------------------------------ Trait declared by Demo and implemented by Pluggin */ pub trait Plug { fn do_plug_op(&self, msg: &str); fn name(&mut self, n: &str); } /*------------------------------------- Demo accepts a Pluggin that supplies a service for Demo to use. */ #[derive(Debug)] pub struct Demo<'a, T> where T: Plug + Debug + 'a { pluggin: Option<&'a mut T>, } impl<'a, T> Demo<'a, T> where T: Plug + Debug + 'a { pub fn new() -> Self { Self { pluggin: None, } } pub fn do_op(&mut self, msg: &str) { print!("\n {}", msg); /*-- show Demo can change state of pluggin --*/ if self.pluggin.is_some() { /*-- get inner &T as mutable --*/ let pl = self.pluggin.as_mut().unwrap(); pl.name("Joe"); pl.do_plug_op("pluggin called from Demo"); } } pub fn accept(&mut self, p: &'a mut T) { self.pluggin = Some(p); } } /*----------------------------------- Pluggin is a service provider that implements Plug trait so Demo knows how to call it. */ #[derive(Debug)] pub struct Pluggin { name: String } impl Plug for Pluggin { fn do_plug_op(&self, msg: &str) { print!("\n {}: {}", &self.name, msg); } fn name(&mut self, n: &str) { self.name = n.to_string(); } } impl Pluggin { pub fn new() -> Self { Self { name: "".to_string() } } } /*------------------------------------- Here's Pluggin architecture at work - show that caller can access state of pluggin after Demo changes it */ fn main() { let mut p = Pluggin::new(); p.name("Fred"); p.do_plug_op("pluggin called from main"); let mut d = Demo::<Pluggin>::new(); d.accept(&mut p); /*-- use Demo with pluggin --*/ d.do_op("demo<Pluggin>::do_op called"); /*-- use original pluggin --*/ p.do_plug_op("original pluggin called from main"); print!("\n\n That's all Folks!\n\n"); } Output Fred: pluggin called from main demo::do_op called Joe: pluggin called from Demo Joe: original pluggin called from main That's all Folks! Comments Pluggin has a name String as a data member to
illustrate how both external code using Pluggin
and Demo<T> can access and mutate Pluggin's state.
Demo<T> changes the Pluggin's name, just to show
how the owner of a Pluggin can interact with its state.
How cool is Rust? Very cool!
Example:
playground example
In the second example, below, Demo<T> holds an Option<T>. That means that the Pluggin instance is moved into Demo so all the issues with lifetime go away because ownership is transferred to Demo<T>
Option Holding Value /*------------------------------------- Pluggin Architecture using Option<T> - Version 2 - holds Option of T - does't require annotating lifetimes since Pluggin is moved into Demo, e.g., Demo now owns Pluggin - also shows how to access pluggin from caller */ use std::fmt::Debug; /*------------------------------------ Trait declared by Demo and implemented by Pluggin */ pub trait Plug { fn do_plug_op(&self, msg: &str); fn name(&mut self, name: &str); } /*------------------------------------- Demo accepts a Pluggin that supplies a service for Demo to use. */ #[derive(Debug)] pub struct Demo<T> where T: Plug + Debug + Clone { pluggin: Option<T>, } impl<T> Demo<T> where T: Plug + Debug + Clone { pub fn new() -> Self { Self { pluggin: None, } } pub fn do_op(&mut self, msg: &str) { print!("\n {}", msg); if let Some(plug) = &mut self.pluggin { plug.name("Jeff"); plug.do_plug_op("pluggin called by Demo"); } } /*-- register pluggin --*/ pub fn accept(&mut self, p: T) { self.pluggin = Some(p); } /*-- get clone of pluggin if it exists --*/ pub fn pluggin(&self) -> Option<T> { self.pluggin.clone() } } /*----------------------------------- Pluggin is a service provider that implements Plug trait so Demo knows how to call it. */ #[derive(Debug, Clone)] pub struct Pluggin { name: String } impl Plug for Pluggin { fn do_plug_op(&self, msg: &str) { print!("\n {}: {}", &self.name, msg); } fn name(&mut self, n:&str) { self.name = n.to_string(); } } impl Pluggin { fn new() -> Self { Self { name: "".to_string() } } } /*------------------------------------- Here's Pluggin architecture at work */ fn main() { let mut p = Pluggin::new(); p.name("Fred"); p.do_plug_op("pluggin call from main"); let mut d = Demo::<Pluggin>::new(); d.accept(p); /*-- use Demo with pluggin --*/ d.do_op("demo<Pluggin>::do_op called"); /*-- retrieve pluggin --*/ let opt = d.pluggin(); if let Some(pl) = opt { pl.do_plug_op("pluggin called from main"); } print!("\n\n That's all Folks!\n\n"); } Output Fred: pluggin call from main demo::do_op called Jeff: pluggin called by Demo Jeff: pluggin called from main That's all Folks! Comments Now lifetime annotations are no longer needed.
We do need to be careful about moving out of
the Pluggin's state since its held by value. Note
the clone() operation in Demo<T>::pluggin().
playground example
  Next Prev Pages Sections About Keys