about
Bits Generic Rust
05/16/2024
0
Bits Repo Code Bits Repo Docs

Bits: Generic Rust

generic types, traits, and functions

Synopsis:

Each generic function or struct is a pattern for defining functions and structs of a specific type. Thus a generic struct is a pattern for making patterns. This page demonstrates creation and uses of generic Rust functions and User-Defined types. The purpose is to quickly acquire some familiarity with generic types and their implementations.
  • Rust generics support definition of function and class patterns, which can be instantiated for a variety of different concrete types.
  • Each instantiation must use types that provide all the facilities used in the pattern.
  • The pattern can provide constraints, using traits, on its type parameters that guarantee methods required by the pattern.
Demo Notes  
Three of the languages: C++, Rust, and C# provide generics. Each generic function or class is a pattern for defining functions and classes of a specific type. Thus a generic class is a pattern for making patterns. The other two, Python and JavaScript, are dynamically typed and already support defining functions and classes for multiple types, e.g., no need for generics. This demonstration illustrates use of generic structs and objects, which for C++, Rust, and C#, are defined in a stackframe or in the heap or both. All data for Python and JavaScript are stored in managed heaps.

1.0 Generics and Traits

Generics are patterns for functions and structs that use placeholder generic types, like T below. The patterns become specific functions and structs when the placeholders are replaced with specific defined types.
Generic functions use the syntax:
Examples in Section 2.6.1
pub fn f<T: Tr1 + Tr2 + ..> (t:T, ...) {
   /* implementing code using T */
}
Tr1, Tr2, ... are traits that place requirements on the generic parameter T, usually to implement specified methods. If the body of f invokes methods on T that are not required by specified traits then compilation fails. We say that traits are bounds on T because they narrow the range of valid generic parameters.

A Rust trait is a declaration that requires any generic type that implements the trait to implement methods defined by the trait, e.g.:
Example in Section 2.4.1
pub trait Default: Sized {
   fn default() -> Self;
}
Sized is a base trait that requires an implementing type to have a size known at compile time. Default requires that an implementing type provide a default value using the static method T::default(). That returns a particular value, e.g., 0 for integers and 0.0 for floating point numbers.

Generic structs are defined with the syntax:
Examples in Section 2.5
// derive(...) are traits compiler implements
#[derive(Debug, Clone)]   
pub struct SomeType<T>
   where T: Tr1 + Tr2 + ...
{
   /* code declaring SomeType methods and data */
}

impl<T> SomeType<T>
   where T: Tr1 + Tr2 + ...
{
   /* implementation of SomeType methods */
}

2.0 Code

Examples below show how to use library and user defined types with emphasis on illustrating syntax and basic operations. These demonstrations of generic Rust code are partitioned into modules main.rs, hello_generic.rs, stats.rs, points_generic.rs, and analysis_generics.rs. Discussion of each of these will be presented in separate sections of the page, below, accessed from links in the left panel.

2.1 Main Module Structure

This first panel block illustrates structure of this demonstration of generic Rust. Most of the code definitions have been elided for clarity, but those details will be shown in later blocks. Alter panel widths by dragging splitter bar or clicking in either panel to expand that panel's width.
#![allow(dead_code)]
#![allow(clippy::approx_constant)]

mod analysis_generic;     // identify module source file
use analysis_generic::*;  // import public functions and types
mod hello_generic;
use hello_generic::*;
mod stats;
use stats::*;
mod points_generic;
use points_generic::*;

use std::{fmt::*, collections::HashMap};

/*------------------------------------------
definition of type Demo has been elided
(shown below)
------------------------------------------*/

fn demo_std_generic_types() {
  /* defs elided */
}
fn demo_user_defined_generic_types() {
  /* defs elided */
}
fn demo_generic_functions() {
  /* defs elided */
}

fn main() {

  show_label("generic functions and types", 50);

  demo_std_generic_types();
  demo_user_defined_generic_types();
  demo_generic_functions();

  print!("\n\n  That's all Folks!\n\n");
}

  Modules:

  The main module, main.rs, opens by importing definitions
  from four modules defined for this demonstraion:
  - analysis_generic.rs defines type analysis and
  display functions
  - hello_generic.rs defines simple type
                HelloGeneric<T> to illustrate generics syntax
  - stats.rs provides type definition for Stats<T>
  and trait Arithmetic.
  - points_generic.rs defines a user type PointN<T>
  representing points in an N dimensional hyperspace.

  Code structure

  Code for this demonstration consists of:
  - Definitions of user-defined types
  - Functions for analysis and display of specified
  objects
  - Three demo functions that demonstrate generics
  for standard types, user-defined types, and
  generic functions





                 Program execution begins in the main function of
  the main module. It simply invokes the demo functions
  as shown in the left panel.

2.2 Standard Generic Types

Standard generic types are defined in the Rust Standard Library and include many more than shown here. We have selected four frequently used types for demonstration: arrays, slices, vectors, and hashmaps. You can find details of the others from the references.

  fn demo_std_generic_types() {

  show_label("demo standard generic types", 32);
  println!();

  show_op("arrays: [T; N]");
  println!();
  let arri = [1, 2, 3, 2, 1];
  println!("  {:?}", arri);
  let arrf = [1.0, 1.5, 2.0, 1.5, 1.0];
  println!("  {:?}\n", arrf);

  show_op("slices: &T[m..n]");
  println!();
  let slicei = &arri[1..5]; // [2, 3, 4, 5]
  println!("  {:?}", slicei);
  let slicef = &arrf[2..4]; // [2.0, 1.5]
  println!("  {:?}\n", slicef);

  show_op("vectors: Vec<T>");
  println!();

  /*-- vector of copy type --*/
  let mut v = Vec::<i32>::new();
  let s = &vec![1, 2, 3, 2, 1];
  v.extend_from_slice(s);
  /*
  This works because the elements are i32, i.e., copy.
  To load a vector with non-copy types use:
  v.extend(s.iter().cloned());
  */
  println!("  Vec<i32> {:?}", v);

  /*--- vector of tuple (i32, &str) --*/
  let mut v2 = Vec::<(i32,&str)>::new();
  v2.push((1, "one"));
  v2.push((2, "two"));
  println!("  Vec<(i32, &str)> {:?}\n", v2);

  show_op("maps: HashMap<K,V>");
  println!();
  let mut m = HashMap::<&str, i32>::new();
  m.insert("zero", 0);
  m.insert("one", 1);
  m.insert("two", 2);
  m.insert("three", 3);
  println!("  maps: HashMap<&str, i32>");
  println!("    {:?}", m);
  }

-------------------------
 demo standard generic types
-------------------------

--- arrays: [T; N] ---

  [1, 2, 3, 2, 1]
  [1.0, 1.5, 2.0, 1.5, 1.0]



--- slices: &T[m..n] ---

  [2, 3, 2, 1]
  [2.0, 1.5]




--- vectors: Vec<T> ---

  Vec<i32> [1, 2, 3, 2, 1]
  Vec<(i32, &str)> [(1, "one"), (2, "two")]
















--- maps: HashMap<K,V> ---

  maps: HashMap<&str, i32>
    {"one": 1, "two": 2, "zero": 0, "three": 3}






2.3 HelloGeneric

HelloGeneric<T> is a simple generic type defined in the module hello_generic.rs. It is included here to introduce basic syntax required for generic types.

2.3.1 HelloGeneric Definition

Rust generics usually require trait declarations. A trait declares one or more methods that are used to specify that specific method calls are valid on a generic parameter. In HelloGeneric<T>, T is required to implement the traits Clone and Default. That insures that if tεT then data: T::default() and t.clone() are valid expressions.
/*---------------------------------------------------------
  HelloGeneric: user-defined generic type
  - Not very useful except as a demonstration of how
    to create a generic type.
  - HelloGeneric instances hold a single value of T
  - Generic parameter T is required to implement traits
      Debug   - supports using debug format {:?}
      Default - supports using a default value, T:default()
      Clone   - implements a copy of an instance with clone()
  - Specified traits, like those above, are often called
    bounds because they limit the types that can be used
    as function and method arguments.
*/
#[derive(Debug, Clone)]  // compiler generated code
struct HelloGeneric<T>
  where T: Debug + Default + Clone
{
  datum: T,
}
impl<T> HelloGeneric<T>
  where T: Debug + Default + Clone
{
  /* construct new instance with datum = d */
  fn new(d:T) -> Self {
    HelloGeneric::<T> {
      datum: d,
    }
  }
  /* construct new instance with default data value */
  fn default_new() -> Self {
    HelloGeneric::<T> {
      datum: T::default(),
    }
  }
  /*
    As shown, value() is equivalent to making datum public.
    However value method supports adding code to modify the
    return value.
  */
  fn value(&mut self) -> &mut T {
    &mut self.datum
  }
  /* print representation of an instance */
  fn show(&self) {
    println!("  HelloGeneric {{ {:?} }}", self.datum);
  }
}

Defining generic types:

Rust types can be defined with generic data members:
  - struct HelloGeneric<T: Debug + Default + Clone>
    defines the data member "datum: T" in the left panel.
  - Methods cannot be called on T unless T is bounded 
    by traits that declare those methods. That guarantees 
    that the invocation is well defined.
  - Debug means that HelloGeneric instances can be 
    written using the debug format "{:?}"
  - Default guarantees that d::default() has a defined 
    value, e.g., 0 for ints and 0.0 for floats.
  - Clone ensures that the invocation clone() on 
                HelloGeneric and T is well defined. Those invocations 
    should return an independent copy of their instances.

If HelloGeneric is declared with a parameter T that does 
not meet all of these requirements, the declaration will 
fail to compile. Note that this is very similar to the 
use of C# constraints.

HelloGeneric declaration:

#[derive(Debug, Clone)]
struct HelloGeneric<T>
  where: T: Debug + Default + Clone 
{
  datum: T,
}

instructs the compiler to lay out HelloGeneric instances 
in memory with room to hold a T

The derive declaration instructs the compiler to impl-
ement Debug and Clone traits for HelloGeneric.

The Clone trait declares a method clone() that copies 
its instance when clone() is invoked.

HelloGeneric methods:
  - new(d:T) -> HelloGeneric<T>, a constructor that
    accepts an instance d of T and returns a newly 
    constructed HelloGeneric<T> instance holding value 
    d in datum.
  - default_new() -> HelloGeneric<T>, a constructor 
    that returns a newly constructed HelloGeneric<T> 
    instance with default value for datum.
  - value(&mut self) -> &mut T returns a mutable 
    reference to datum;
  - show(&self) displays a text representation of 
    instance.
  - clone(&self) -> HelloGeneric<T> implemented by the
    compiler.

Since all methods but constructors accept a refer-
ence, &self, invocations do not consume the 
HelloGeneric instance.

2.3.2 HelloGeneric Demonstration

The panels below demonstrate creation and use of HelloGeneric. The left panel contains a function, demo_HelloGeneric found in module hello_generic.rs, that defines the demonstration. The right panel contains output when the function is invoked.
/*---------------------------------------------------------
  Demonstrate creation of HelloGeneric type and use
  of its methods.
*/
#[allow(non_snake_case)]
pub fn demo_HelloGeneric() {

  show_label(" demo user defined HelloGeneric type", 40);
  println!();
  show_op("HelloGeneric<T>");
  println!();

  show_op("let mut h = HelloGeneric::<i32>::new(42)");
  let mut h = HelloGeneric::<i32>::new(42);
  h.show();
  println!();

  show_op("*h.value() = 84");
  *h.value() = 84;
  h.show();
  println!();

  show_op("let c = h.clone()");
  let c = h.clone();  // h still valid
  c.show();
  println!();

  show_op("let n = h : transfer ownership of datum");
  let n = h;  // move ownership of datum to h
  n.show();

  //h.show();  // h invalid, been moved
}






----------------------------------------
   demo user defined HelloGeneric type
----------------------------------------

--- HelloGeneric<T> ---

--- let mut h = HelloGeneric::<i32>::new(42) ---
  HelloGeneric { 42 }

--- *h.value() = 84 ---
  HelloGeneric { 84 }

--- let c = h.clone() ---
  HelloGeneric { 84 }

--- let n = h : transfer ownership of datum ---
  HelloGeneric { 84 }










2.4 Stats

Stats<T> is a relatively simple generic type defined in the module stats.rs. It is included here to show how to provide indexing for a custom type.

2.4.1 Stats Definition

A Rust trait is a declaration that requires any generic type that implements the trait to implement methods defined by the trait. In Stats<T>, T is required to implement the trait Arithmetic. That insures that arithmetic operations can be applied to T. The Arithmetic trait is simply a union of traits defined in modules std::ops and std::cmp + traits Default, Into, Debug, and Copy. The Stats struct holds a vector of data of type T and its operations evaluate simple arithmetic properties of that data.
/*-------------------------------------------------------------------
  stats.rs
  - defines type Stats containing Vec of generic Type
    bounded by Arithmetic trait
  - Works as is only for i32 and f64, but easy to extend to
    all integers and floats
  - Can also be extended to complex numbers
-------------------------------------------------------------------*/

use std::ops::*;
use std::cmp::*;
use std::convert::{Into};
use std::fmt::Debug;

pub trait Arithmetic<T = Self>: Add<Output=T> + Sub<Output=T>
    + Mul<Output=T> + Div<Output=T> + PartialEq + PartialOrd
    + Default + Copy + Debug + Into<f64> {}

impl Arithmetic<f64> for f64 {}
impl Arithmetic<i32> for i32 {}

#[derive(Debug, Clone)]
pub struct Stats<T: Arithmetic + Debug> {
  items: Vec<T>
}

impl<T: Arithmetic> Stats<T>
    where T: Arithmetic + Debug
{
  pub fn new(v:Vec<T>) -> Stats<T> {
    Stats {
      items: v,
    }
  }
  pub fn max(&self) -> T {
    let mut biggest = self.items[0];
    for item in &self.items {
      if biggest < *item {
        biggest = *item;
      }
    }
    biggest
  }
  pub fn min(&self) -> T {
    let mut smallest = self.items[0];
    for item in &self.items {
      if smallest > *item {
        smallest = *item;
      }
    }
    smallest
  }
  pub fn sum(&self) -> T {
    let mut sum = T::default();
    for item in &self.items {
      sum = sum + *item;
    }
    sum
  }
  pub fn avg(&self) -> f64 {
    let mut sum = T::default();
    for item in &self.items {
      sum = sum + *item;
    }
    /*-- cast usize to f64 --*/
    let den:f64 = self.items.len() as f64;
    /*-- can't cast non-primitive to primitive --*/
    let num:f64 = sum.into();
    num/den
  }
}


Defining generic types:

Rust types can be defined with generic data members:
  - struct Stats<T: Arithmetic + Debug>
    defines the data member "items: Vec<T>" in the 
    left panel.
  - Methods cannot be called on T unless T is bounded 
    by traits that declare those methods. That guarantees 
    that the invocation is well defined.
  - This stats.rs module defines a trait "Arithmetic" 
    as a union of traits defined in 
      std::ops: Add, Sub, Mul, Div 
    plus traits in 
      std::cmd: PartialEq, PartialOrd 
    plus traits defined in
      std: Default, Copy, Debug, Into.
  - That means that T implements operators:
      +, -, *, / 
      and can be compared and put into at least 
      a partial order.
  - Default guarantees that T::default() has a defined 
    value, e.g., 0 for ints and 0.0 for floats.
  - Copy is a marker trait, e.g., no methods, but 
    requires construction and assignment to use copy 
    operations instead of move operations. All the 
    primitives are Copy types.
  - Debug means that Stats instances can be written 
    using the debug format "{:?}"
  - Into<f64> means that any type will be implicitly 
    converted to f64 if needed, as in dividing an 
    instance of T by an integer.

If Stats is declared with a parameter T that does not 
meet all of these requirements, the declaration will 
fail to compile. This is very similar to the use of C# 
constraints.

Note that Arithmetic has only been implemented in Stats 
for the types i32 and f64.  Implementing for other 
integral and float types is simply a matter of copying 
the impl statements for those types.

Stats declaration:

#[derive(Debug, Clone)]
pub struct Stats<T: Arithmetic + Debug> {
  items: Vec<T>;
}
instructs the compiler to lay out Stats instances with 
a Vec control block pointing to a mutable array of T 
values in the native heap.

The derive declaration instructs the compiler to impl-
ement Debug and Clone traits for Stats.

The Clone trait implements a method clone() for Stats
that copies the instance when clone is invoked.

Stats methods:

Stats implements the methods:
  - new(v:Vec<T>) ->Stats<T>, a constructor that accepts 
    a vector of T values
  - max(&self) -> T returns most positive value in items.
  - min(&self) -> T returns most negative value in items.
  - sum(&self) -> T returns sum of values in items.
  - avg(&self) -> T returns average of values in items.
  - clone(&self) -> Stats<T>

Since all methods but the constructor accept a reference, &self,
invocations do not consume the Stats instance.

2.4.2 Stats Demonstration

/*---------------------------------------------------------
  Demonstrate user-defined Stats<T> type
*/
pub fn demo_stats() {
  show_label("demo Arithmetic Trait with Stats<T>", 40);
  println!();

  show_op("Stats<T>");
  println!();
  let s = Stats::<f64>::new(vec![1.5, 2.5, 3.0, -1.25, 0.5]);
  println!("  {:?}", s);
  println!("  max: {:?}", s.max());
  println!("  min: {:?}", s.min());
  println!("  sum: {:?}", s.sum());
  println!("  avg: {:?}", s.avg());
}



----------------------------------------
  demo Arithmetic Trait with Stats<T>
----------------------------------------

--- Stats<T> ---

  Stats { items: [1.5, 2.5, 3.0, -1.25, 0.5] }
  max: 3.0
  min: -1.25
  sum: 6.25
  avg: 1.25


2.5 Point<T, N>

Point<T, N> represents an N-dimensional point by wrapping a vector of coordinates call coor. It provides methods for initialization, modification, and indexing.

2.5.1 Point<T, N> Definition

PointN<T> is the first useful generic type presented here. It's defined in the module points_generic.rs and has almost all the functionality needed for a working type. Only an iterator is needed, and that will be added in the next Bit.
/*-------------------------------------------------------------------
  points_generic.rs
  - defines type Point<T, N>
-------------------------------------------------------------------*/

use std::default::*;
use std::fmt::*;

use crate::analysis_generic;    // identify source code
use analysis_generic::*;        // import public functions and types

/*---------------------------------------------------------
  - Declare Point<T, N> struct, like a C++ template class
  - Request compiler implement traits Debug & Clone
*/
#[derive(Debug, Clone)]
pub struct Point<T, const N: usize>
where
  T: Debug + Default + Clone
{
  coor: Vec<T>,
}
impl<T, const N: usize> Point<T, N>
where
T: Debug + Default + Clone
{
  /*-- constructor --*/
  pub fn new() -> Point<T, N> {
    Point::<T, N> {
      coor: vec![T::default(); N],
    }
  }
  /*-------------------------------------------------------
    Point<T, N>::init(&self, v:Vec<T>) fills coor with
    first N values from v and sets any remainder to
    T::default().
  -------------------------------------------------------*/
  pub fn init(mut self, coord: &Vec<T>) -> Point<T, N> {
    for i in 1..N {
      if i < coord.len() {
        self.coor[i] = coord[i].clone();
      }
      else {
        self.coor[i] = T::default();
      }
    }
    self
  }
  pub fn len(&self) -> usize {
    self.coor.len()
  }
  /*-- acts as both get_coor() and set_coor(some_vector) --*/
  pub fn coors(&mut self) -> &mut Vec<T> {
    &mut self.coor
  }
  /*-- displays name, type, and coordinates --*/
  pub fn show(&self, nm:&str, left: usize, width:usize) {
    println!("  {nm:?}: Point<T, N> {{");
    show_fold(&self.coor, left + 2, width);
    println!("  }}")
  }
}
/*-- implements const indexer -----------------*/
impl<T:Debug, const N:usize, Idx> std::ops::Index<Idx> for Point<T, N>
    where
        T:Debug + Default + Clone,
        Idx: std::slice::SliceIndex<[T]>
{
    type Output = Idx::Output;

    fn index(&self, index:Idx) -> &Self::Output {
        &self.coor[index]
    }
}
/*-- implements mutable indexer ---------------*/
impl<T, const N: usize, Idx> std::ops::IndexMut<Idx> for Point<T, N>
    where
        T:Debug + Default + Clone,
        Idx: std::slice::SliceIndex<[T]>
{
    fn index_mut(&mut self, index:Idx) -> &mut Self::Output {
        &mut self.coor[index]
    }
}
/* explicit conversion to slice &[T] */
impl<T, const N: usize> AsRef<[T]> for Point<T, N>
  where
    T: ?Sized,
    T: Debug + Display + Default + Clone,
{
  fn as_ref(&self) -> &[T] {
    &self.coor
  }
}
/* implicit conversion to slice &[T] */
impl<T, const N: usize> std::ops::Deref for Point<T, N>
  where T: Debug + Default + Clone,
{
  type Target = [T];

  fn deref(&self) -> &Self::Target {
    &self.coor
  }
}
----- Point<T, N> declaration ------ #[derive(Debug, Clone)]
pub struct Point<T, const N: usize>
    where T: Debug + Default + Clone
{
    coor: Vec<T>,
}

instructs the compiler to lay out PointN<T> instances with a Vec control block pointing to a mutable array of T values in the native heap. The derive declaration instructs the compiler to implement Debug and Clone traits.
----- Point<T, N> methods ---------- Point<T, N> implements methods:
  • new() ->Point<T, N>, a constructor that builds coor with N T::default() values.
  • init(mut self, coord: &Vec<T>) -> Point<T, N> accepts a reference to a vector of coordinates, returns new instance with the specified coordinates.
  • len(&self) -> usize returns length of coor
  • coors(&mut self) -> &mut Vec<T> returns mutable reference to internal coordinates, allowing both reading and writing coordinate values.
----- Point<T, N> traits ----------
  • std::ops::Index<Idx> supports non-mutable indexing with index notation.
  • std::ops::IndexMut<Idx> supports mutable indexing with index notation.
  • std::ops::AsRef<[T]> provides explicit conversion to array slice using Vec AsRef<[T]>
  • std::ops::DeRef implicit conversion to array slice using Vec DeRef.
  • clone(&self) -> Point<T, N> returns independent copy, implemented by compiler.
Since all methods but the constructor and initializer accept a reference, &self, invocations do not consume the Point instance.

2.5.2 Point<T, N> Demonstration

Demonstration of PointN<T> is provided by the function demo_pointn() in module points_generic.rs, shown in the left panel. When that is executed the results are the content of the right panel.
/*---------------------------------------------------------
  Demonstrate user-defined Point<T, N> type
*/
pub fn demo_pointn() {

  show_label("demo indexing with Point<132, 5>", 40);
  println!();

  show_op("Point<i32, 5>");
  println!();
  let mut p = Point::<i32, 5>::new()
         .init(&vec![1, 2, 3, 2, 1]);
  p.show("p", 2, 12);
  println!();
  show_op("*p.coors() = vec![1, 0, -1, 0, 1]");
  *p.coors() = vec![1, 0, -1, 0, 1];
  p.show("p", 2, 12);

  println!("\n  using immutable indexer:");
  println!("  value of p[0] is {}\n", p[0]);
  println!("  using mutable indexer:");
  show_op("p[0] = 3");
  p[0] = 3;
  p.show("p", 2, 12);
  show_op("p[1] = 4");
  p[1] = 4;
  p.show("p", 2, 12);

}




----------------------------------------
  demo indexing with PointN<132, 5>
----------------------------------------

--- PointN<i32, 5> ---

  "p": Point<T, N> {
    0, 2, 3, 2, 1
  }

--- *p.coors() = vec![1, 0, -1, 0, 1] ---
  "p": Point<T, N> {
    1, 0, -1, 0, 1
  }

  using immutable indexer:
  value of p[0] is 1

  using mutable indexer:
--- p[0] = 3 ---
  "p": Point<T, N> {
    3, 0, -1, 0, 1
  }
--- p[1] = 4 ---
  "p": Point<T, N> {
    3, 4, -1, 0, 1
  }


2.6 Generic Functions

The preceding demonstrations have used several generic functions for analysis of type and display, along with a few non-generic helper functions. These are provided in module analysis_generic.rs and discussed below.

2.6.1 Generic Functions Definition

Code for generic functions, show_type<T>, demo_indexer<T>, fold<T, I>, and show_fold<T, I> is presented in the left panel. Four non-generic functions are also part of the analysis_generic.rs module. Generic functions like demo_indexer<T> allow us to write one function that can accept aguments of many different types. That improves code ergonomics and developer productivity.
/*-------------------------------------------------------------------
  analysis_generics.rs
  - provides analysis and display functions for Generics demo.
  - a few of these require advanced generics code.
  - You don't need to know how these work to understand this
    demo.
  - We will come back to these functions in rust_iter.
-------------------------------------------------------------------*/

use std::fmt::*;

/*---------------------------------------------------------
  Show input's call name and type
  - doesn't consume input
  - show_type is generic function with Debug bound.
    Using format "{:?}" requires Debug.
*/
pub fn show_type<T:Debug>(_t: &T, nm: &str) {
  let typename = std::any::type_name::<T>();
  println!("call name: {nm:?}, type: {typename:?}");
}
/*---------------------------------------------------------
  show_indexer<T:Debug>(nm:&str, s:&[T])
  - accepts any collection that implements Deref to slice
  - that includes array [T;N], slice T[N], Vec<T>, PointN<T>
*/
#[allow(clippy::needless_range_loop)]
pub fn demo_indexer<T>(nm:&str, s:&[T])
  where T: Debug + Display
{
  print!("  {}", nm);
  let max = s.len();
  print!{"  [ {:?}", s[0]};
  /* 1..max is range iterator */
  for i in 1..max {
      print!(", {:?}", s[i]);
  }
  println!(" ]");
  /*---------------------------------------------
    The code above is not idiomatic Rust.
    Rust style prefers using iterators over indexing
    like this:
    for item in s.iter() {
      print!("{item} ");
    }
  */
}
/*---------------------------------------------------------
  build indent string with "left" spaces
*/
pub fn offset(left: usize) -> String {
  let mut accum = String::new();
  for _i in 0..left {
    accum += " ";
  }
  accum
}
/*---------------------------------------------------------
  find index of last occurance of chr in s
  - returns option in case chr is not found
  https://stackoverflow.com/questions/50101842/how-to-find-the-last-occurrence-of-a-char-in-a-string
*/
fn find_last_utf8(s: &str, chr: char) -> Option<usize> {
  s.chars().rev().position(|c| c== chr)
    .map(|rev_pos| s.chars().count() - rev_pos - 1)

    /*-- alternate implementation --*/
  // if let Some(rev_pos) =
  //   s.chars().rev().position(|c| c == chr) {
  //     Some(s.chars().count() - rev_pos - 1)
  // } else {
  //     None
  // }
}
/*---------------------------------------------------------
  fold an enumerable's elements into rows of w elements
  - indent by left spaces
  - does not consume t since passed as reference
  - returns string
  https://users.rust-lang.org/t/generic-code-over-iterators/10907/3
*/
pub fn fold<T, I:Debug>(
  t: &T, left: usize, width: usize
) -> String
    where for<'a> &'a T: IntoIterator<Item = &'a I>, T:Debug
{
  let mut accum = String::new();
  accum += &offset(left);

  for (i, item) in t.into_iter().enumerate() {
    accum += &format!("{item:?}, ");
    if ((i + 1) % width) == 0 && i != 0 {
        accum += "\n";
        accum += &offset(left);
    }
  }
  /*-- Alternate direct implementation --*/
  //let mut i = 0usize;
  // for item in t {
  //   accum += &format!("{item:?}, ");
  //   if ((i + 1) % width) == 0 && i != 0 {
  //       accum += "\n";
  //       accum += &offset(left);
  //   }
  //   i += 1;
  // }

  let opt = find_last_utf8(&accum, ',');
  if let Some(index) = opt {
    accum.truncate(index);
  }
  accum
}
/*---------------------------------------------------------
  show enumerables's elements as folded rows
  - width is number of elements in each row
  - left is indent from terminal left
*/
pub fn show_fold<T:Debug, I:Debug>(t:&T, left:usize, width:usize)
  where for<'a> &'a T: IntoIterator<Item = &'a I>
{
  println!("{}",fold(t, left, width));
}
/*------------------------------------------------------------
  show string wrapped with long dotted lines above and below
*/
pub fn show_label(note: &str, n:usize) {
  let line =
    std::iter::repeat('-').take(n).collect::<String>();
  print!("\n{line}\n");
  print!("  {note}");
  print!("\n{line}\n");
}
pub fn show_label_def(note:&str) {
  show_label(note, 50);
}
/*---------------------------------------------------------
  show string wrapped with dotted lines above and below
*/
pub fn show_note(note: &str) {
  print!("\n-------------------------\n");
  print!(" {note}");
  print!("\n-------------------------\n");
}
/*---------------------------------------------------------
  show string wrapped in short lines
*/
pub fn show_op(opt: &str) {
  println!("--- {opt} ---");
}
/*---------------------------------------------------------
  print newline
*/
pub fn nl() {
  println!();
}
----- generic functions -----------------
  • show_type<T> prints the second argument string, expected to be the name at the call-site of the first argument, then prints the compiler generated type representation of the first argument.
  • demo_indexer<T> prints a representation of the slice returned by an implicit call to std::ops::deref on the second argument. This invocation will fail to compile if the type of the second argument does not implement trait DeRef.
  • show_fold<T> prints a column of rows of values for enumerable types, like the std::collections, making output more readable.
----- helper functions ----------
  • fold<T> does all the computation to partition a slice into a column of rows of values.
  • offset builds a string of blank spaces to prepend to display lines.
  • find_last_utf8 finds the last occurance of a character in a given string. It is used to find and remove the last trailing comma in a string of comma separated list of values.
Five of these functions use iterators, which are explained in the next Bit.

2.6.2 Generic Functions Demonstration

The demonstration code and its output illustrate how effective generic functions like demo_indexer can be to help make code readable and save time by avoiding creation of multiple functions that use the same code except for type declarations.
fn demo_generic_functions() {
  show_note("demo_generic_functions");
  println!();

  show_op("show_type<T:Debug>(_t, \"name\")");
  println!();

  let v = vec![1, 2, 3];
  show_type(&v, "v");
  let m = HashMap::<&str, i32>::new();
  show_type(&m, "m");
  println!();

  show_op("demo_indexer");
  println!();
  demo_indexer("[i32; 3]", &[1, 2, 3]);
  demo_indexer("Vec<i32>", &v);
  let p = PointN::<f64>::new(3).init(vec![1.0, 2.0, -0.5]);
  demo_indexer("PointN<f64>", &p);
}

-------------------------
 demo_generic_functions
-------------------------

--- show_type<T:Debug>(_t, "name") ---

call name: "v", type: "alloc::vec::Vec<i32>"
call name: "m", type: "std::collections::hash::map::HashMap<&str, i32>"

--- demo_indexer ---

  [i32; 3]  [ 1, 2, 3 ]
  Vec<i32>  [ 1, 2, 3 ]
  PointN<f64>  [ 1.0, 2.0, -0.5 ]





3.0 Build

C:\github\JimFawcett\Bits\Rust\rust_generics
> cargo build
   Compiling rust_hello_objects v0.1.0 (C:\github\JimFawcett\Bits\Rust\rust_generics)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
C:\github\JimFawcett\Bits\Rust\rust_generics
>
          

4.0 VS Code View

The code for this demo is available in github.com/JimFawcett/Bits. If you click on the Code dropdown you can clone the repository of all code for these demos to your local drive. Then, it is easy to bring up any example, in any of the languages, in VS Code. Here, we do that for Rust\rust_generics. Figure 1. VS Code IDE - Rust Debug Generics

5.0 References

Reference Description
RustBite_Generics RustBite on Generics
Rust Story E-book with seven chapters covering most of intermediate Rust
Rust Bites Relatively short feature discussions
std library Comprehensive guide organized into primitive types, modules, macros, and keywords
std modules Definition of non-primitive types like Vec<T> and String
st::collections Definition of collection types like HashMap<K, V> and VecDeque<T>