about
Bits Generic Rust
05/31/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 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.2 HelloGeneric
2.2.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);
}
}
2.2.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.3 Stats
2.3.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 onT unlessT 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 instd::ops :Add, Sub, Mul, Div plus traits instd::cmd :PartialEq, PartialOrd plus traits defined instd::default::Default, std::marker::Copy, std::fmt::Debug, and std::convert::Into . - That means that T implements operators:+, -, *, / and can be compared and put into at least a partial order. -Default guarantees thatT::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 areCopy types. -Debug means thatStats instances can be written using the debug format "{:?} " -Into<f64> means that any type will be implicitly converted tof64 if needed, as in dividing an instance ofT by an integer. IfStats is declared with a parameterT that does not meet all of these requirements, the declaration will fail to compile. This is very similar to the use ofC# constraints. Note thatArithmetic has only been implemented inStats for the typesi32 andf64 . Implementing for other integral and float types is simply a matter of adding 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 outStats instances with aVec control block pointing to a mutable array ofT values in the native heap. The derive declaration instructs the compiler to impl- ementDebug andClone traits forStats . TheClone trait implements a methodclone() forStats that copies instance whenclone is invoked.Stats methods:Stats implements the methods: -new(v:Vec<T>) ->Stats<T> , a constructor that accepts a vector ofT values -max(&self) -> T returns most positive value initems . -min(&self) -> T returns most negative value initems . -sum(&self) -> T returns sum of values initems . -avg(&self) -> T returns average of values initems . -clone(&self) -> Stats<T> returns copy of itself. Since all methods but the constructor accept a refer- ence,&self , invocations do not consume theStats instance.
2.3.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.4 Point<T, N>
2.4.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
}
}
Point<T, N> implements generic Point type:
The code block:#[derive(Debug, Clone)] pub struct Point<T, const N: usize> where T: Debug + Default + Clone { coor: Vec<T>, } instructs the compiler to lay outPointN<T> instances with aVec control block pointing to a mutable array ofT values in the native heap. The derive declaration instructs the compiler to implementDebug andClone traits. ⇐Point<T, N> methods: -new() ->Point<T, N> , a constructor that builds Veccoor 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. ⇐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 usingVec AsRef<[T]> -std::ops::DeRef implicit conversion to array slice usingVec DeRef. -clone(&self) -> Point<T, N> returns independent copy, implemented by compiler. Since all methods but the constructor and initializer accept a reference,&self , method invocations do not consume thePoint instance.
2.4.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.5 Generic Functions
2.5.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!();
}
Generic functions:
Five of these functions use iterators, which are explained in the next Bit. ⇐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. ⇐offset builds a string of blank spaces to prepend to display lines. ⇐find_last_utf8 finds the last occurance of a character in given string. It is used to find and remove the last trailing comma in a string of comma separated list of values. ⇐fold<T> does all the computation to partition a slice into a column of rows of values. ⇐show_fold<T> prints a column of rows of values for enumerable types, like the std::collections, making output more readable.
2.5.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 ]
2.6 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 typeHelloGeneric<T> to illustrate generics syntax - stats.rs provides type definition forStats<T> and traitArithmetic . - points_generic.rs defines a user typePointN<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 demo functions as shown in the left
panel.
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 |
Defining generic types:
Rust types can be defined with generic data members:-
defines the data member "
- Methods cannot be called on
by traits that declare those methods. That guarantees
that the invocation is well defined.
-
written using the debug format "
-
value, e.g., 0 for ints and 0.0 for floats.
-
should return an independent copy of their instances.
If
that does not meet all of these requirements, the
declaration will fail to compile. Note that this is very
similar to the use of
struct HelloGeneric<T>
where: T: Debug + Default + Clone
{
datum: T,
}
instances in memory with room to hold a
The derive declaration instructs the compiler to impl-
ement
The
its instance when
-
accepts an instance d of
constructed
d in datum.
-
that returns a newly constructed
instance with default value for datum.
-
reference to datum;
-
instance.
-
by the compiler.
Since all methods but constructors accept a reference