about
Bits Generic Rust
05/16/2024
0
Bits: Generic Rust
generic types, traits, and functions
Synopsis:
- 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
1.0 Generics and Traits
Examples in Section 2.6.1
/* implementing code using T */
}
Example in Section 2.4.1
fn default() -> Self;
}
Examples in Section 2.5
#[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
2.1 Main Module Structure
#![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
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
2.3.1 HelloGeneric Definition
/*---------------------------------------------------------
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
/*---------------------------------------------------------
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
2.4.1 Stats Definition
/*-------------------------------------------------------------------
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>
2.5.1 Point<T, N> Definition
/*-------------------------------------------------------------------
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
}
}
pub struct Point<T, const N: usize>
where T: Debug + Default + Clone
{
coor: Vec<T>,
}
-
new() ->Point<T, N> , a constructor that buildscoor withN 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 ofcoor -
coors(&mut self) -> &mut Vec<T> returns mutable reference to internal coordinates, allowing both reading and writing coordinate values.
-
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 usingVec AsRef<[T]> -
std::ops::DeRef implicit conversion to array slice usingVec DeRef. -
clone(&self) -> Point<T, N> returns independent copy, implemented by compiler.
2.5.2 Point<T, N> Demonstration
/*---------------------------------------------------------
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
2.6.1 Generic Functions Definition
/*-------------------------------------------------------------------
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!();
}
-
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.
-
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.
2.6.2 Generic Functions Demonstration
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
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 |
st::collections |
Definition of collection types like |