πŸ¦€ Functional Rust
🎬 Traits & Generics Shared behaviour, static vs dynamic dispatch, zero-cost polymorphism.
πŸ“ Text version (for readers / accessibility)

β€’ Traits define shared behaviour β€” like interfaces but with default implementations

β€’ Generics with trait bounds: fn process(item: T) β€” monomorphized at compile time

β€’ Static dispatch (impl Trait) = zero cost; dynamic dispatch (dyn Trait) = runtime flexibility via vtable

β€’ Blanket implementations apply traits to all types matching a bound

β€’ Associated types and supertraits enable complex type relationships

210: Iso Basics

Difficulty: 3 Level: Advanced A bidirectional, lossless transformation β€” convert between representations and compose conversions freely.

The Problem This Solves

Every program has representations: a temperature as `f64` Celsius, or as `f64` Fahrenheit, or as a newtype `Celsius(f64)`. Converting between them is easy. Converting back should recover the original exactly. But nothing in the type system enforces that guarantee β€” you could write a broken "roundtrip" and only discover it in production. An `Iso` (isomorphism) makes the contract explicit: it pairs a forward function `get` with an inverse `reverse_get`, and the two are required to be true inverses. Reversing an Iso swaps the functions. Composing two Isos chains both directions. The type system tracks which representation you're in. This matters most for serialisation (struct ↔ JSON ↔ bytes), unit conversions, and newtype wrappers. Once you have an Iso, you can lift any operation on one type to work on the other β€” no manual conversion at every call site.

The Intuition

An Iso is like a perfect translation dictionary that works both ways. If you translate "cat" to French and back, you get "cat" again β€” not "feline" or "kitten". Lossless means no information is lost in either direction. `get` is the forward direction. `reverse_get` is the backward direction. They must satisfy: `get(reverse_get(a)) == a` and `reverse_get(get(s)) == s`. Violating this isn't a compiler error, but it breaks every downstream operation that assumes the invariant. Composing two Isos: if `A ↔ B` and `B ↔ C`, you get `A ↔ C`. The composed forward function is `get_bc(get_ab(a))`, and the composed reverse is `reverse_get_ab(reverse_get_bc(c))`. Both directions chain automatically.

How It Works in Rust

struct Iso<S, A> {
 get: Box<dyn Fn(&S) -> A>,
 reverse_get: Box<dyn Fn(&A) -> S>,
}

impl<S: 'static, A: 'static> Iso<S, A> {
 fn new(
     get: impl Fn(&S) -> A + 'static,
     reverse_get: impl Fn(&A) -> S + 'static,
 ) -> Self {
     Iso { get: Box::new(get), reverse_get: Box::new(reverse_get) }
 }

 // Reversing swaps the two function fields β€” the type flips to Iso<A, S>
 fn reverse(self) -> Iso<A, S> {
     Iso { get: self.reverse_get, reverse_get: self.get }
 }
}

// Celsius ↔ Fahrenheit
fn celsius_fahrenheit() -> Iso<f64, f64> {
 Iso::new(
     |c| c * 9.0 / 5.0 + 32.0,   // forward: C β†’ F
     |f| (f - 32.0) * 5.0 / 9.0, // reverse: F β†’ C
 )
}

let iso = celsius_fahrenheit();
let f = (iso.get)(&100.0);           // β†’ 212.0
let c = (iso.reverse_get)(&212.0);   // β†’ 100.0

// Newtype wrapper β€” Meters ↔ f64
struct Meters(f64);
let meters_iso: Iso<f64, Meters> = Iso::new(
 |&m| Meters(m),
 |Meters(v)| *v,
);

What This Unlocks

Key Differences

ConceptOCamlRust
Newtype`type meters = Meters of float``struct Meters(f64)` (tuple struct)
Iso structRecord with `get` and `reverse_get`Struct with `Box<dyn Fn>` fields
ReversingCreates new record, swaps fieldsConsumes `self`, returns `Iso<A, S>`
Char handling`String.get` is byte-indexed`.chars()` is Unicode-aware
CompositionSimple function chaining`'static` bounds needed for closures
// Example 210: Iso Basics β€” Lossless Bidirectional Transformations

struct Iso<S, A> {
    get: Box<dyn Fn(&S) -> A>,
    reverse_get: Box<dyn Fn(&A) -> S>,
}

impl<S: 'static, A: 'static> Iso<S, A> {
    fn new(
        get: impl Fn(&S) -> A + 'static,
        reverse_get: impl Fn(&A) -> S + 'static,
    ) -> Self {
        Iso { get: Box::new(get), reverse_get: Box::new(reverse_get) }
    }

    fn reverse(self) -> Iso<A, S> {
        Iso { get: self.reverse_get, reverse_get: self.get }
    }
}

// Approach 1: Simple isomorphisms
fn celsius_fahrenheit() -> Iso<f64, f64> {
    Iso::new(
        |c| c * 9.0 / 5.0 + 32.0,
        |f| (f - 32.0) * 5.0 / 9.0,
    )
}

fn string_chars() -> Iso<String, Vec<char>> {
    Iso::new(
        |s: &String| s.chars().collect(),
        |cs: &Vec<char>| cs.iter().collect(),
    )
}

// Approach 2: Iso from newtype wrappers
#[derive(Debug, Clone, PartialEq)]
struct Meters(f64);

#[derive(Debug, Clone, PartialEq)]
struct Kilometers(f64);

fn meters_iso() -> Iso<Meters, f64> {
    Iso::new(|m: &Meters| m.0, |f: &f64| Meters(*f))
}

fn km_to_m() -> Iso<Kilometers, Meters> {
    Iso::new(
        |km: &Kilometers| Meters(km.0 * 1000.0),
        |m: &Meters| Kilometers(m.0 / 1000.0),
    )
}

// Approach 3: Composition
fn compose_iso<S: 'static, A: 'static, B: 'static>(
    outer: Iso<S, A>, inner: Iso<A, B>,
) -> Iso<S, B> {
    let og = outer.get; let org = outer.reverse_get;
    let ig = inner.get; let irg = inner.reverse_get;
    Iso::new(
        move |s| (ig)(&(og)(s)),
        move |b| (org)(&(irg)(b)),
    )
}

fn main() {
    // Celsius/Fahrenheit roundtrip
    let iso = celsius_fahrenheit();
    let f = (iso.get)(&100.0);
    assert!((f - 212.0).abs() < 0.001);
    let c = (iso.reverse_get)(&f);
    assert!((c - 100.0).abs() < 0.001);

    // String/chars roundtrip
    let iso = string_chars();
    let chars = (iso.get)(&"hello".to_string());
    assert_eq!(chars, vec!['h', 'e', 'l', 'l', 'o']);
    assert_eq!((iso.reverse_get)(&chars), "hello");

    // Reverse
    let fahr_to_cel = celsius_fahrenheit().reverse();
    assert!(((fahr_to_cel.get)(&212.0) - 100.0).abs() < 0.001);

    // Composition
    let km_raw = compose_iso(km_to_m(), meters_iso());
    assert!(((km_raw.get)(&Kilometers(5.0)) - 5000.0).abs() < 0.001);
    assert_eq!((km_raw.reverse_get)(&5000.0), Kilometers(5.0));

    println!("βœ“ All tests passed");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_roundtrip_celsius() {
        let iso = celsius_fahrenheit();
        for c in [0.0, 37.0, 100.0, -40.0] {
            let roundtrip = (iso.reverse_get)(&(iso.get)(&c));
            assert!((roundtrip - c).abs() < 0.0001);
        }
    }

    #[test]
    fn test_roundtrip_string() {
        let iso = string_chars();
        let s = "RustπŸ¦€".to_string();
        let roundtrip = (iso.reverse_get)(&(iso.get)(&s));
        assert_eq!(roundtrip, s);
    }

    #[test]
    fn test_reverse() {
        let iso = celsius_fahrenheit().reverse();
        assert!(((iso.get)(&32.0) - 0.0).abs() < 0.001);
    }

    #[test]
    fn test_compose() {
        let iso = compose_iso(km_to_m(), meters_iso());
        assert!(((iso.get)(&Kilometers(1.0)) - 1000.0).abs() < 0.001);
    }
}
(* Example 210: Iso Basics β€” Lossless Bidirectional Transformations *)

(* An isomorphism is a lossless, reversible conversion between two types.
   get . reverseGet = id  AND  reverseGet . get = id *)

type ('s, 'a) iso = {
  get : 's -> 'a;
  reverse_get : 'a -> 's;
}

(* Approach 1: Simple isomorphisms *)
let celsius_fahrenheit : (float, float) iso = {
  get = (fun c -> c *. 9.0 /. 5.0 +. 32.0);
  reverse_get = (fun f -> (f -. 32.0) *. 5.0 /. 9.0);
}

let string_chars : (string, char list) iso = {
  get = (fun s -> List.init (String.length s) (String.get s));
  reverse_get = (fun cs -> String.init (List.length cs) (List.nth cs));
}

(* Approach 2: Iso from newtype wrappers *)
type meters = Meters of float
type kilometers = Kilometers of float

let meters_iso : (meters, float) iso = {
  get = (fun (Meters m) -> m);
  reverse_get = (fun m -> Meters m);
}

let km_to_m : (kilometers, meters) iso = {
  get = (fun (Kilometers km) -> Meters (km *. 1000.0));
  reverse_get = (fun (Meters m) -> Kilometers (m /. 1000.0));
}

(* Approach 3: Iso combinators *)
let reverse (i : ('s, 'a) iso) : ('a, 's) iso = {
  get = i.reverse_get;
  reverse_get = i.get;
}

let compose_iso (outer : ('s, 'a) iso) (inner : ('a, 'b) iso) : ('s, 'b) iso = {
  get = (fun s -> inner.get (outer.get s));
  reverse_get = (fun b -> outer.reverse_get (inner.reverse_get b));
}

(* An iso IS a lens *)
let iso_to_lens (i : ('s, 'a) iso) = {|
  get = i.get;
  set = (fun a _s -> i.reverse_get a);
|}

(* === Tests === *)
let () =
  (* Celsius/Fahrenheit roundtrip *)
  let c = 100.0 in
  let f = celsius_fahrenheit.get c in
  assert (abs_float (f -. 212.0) < 0.001);
  let c2 = celsius_fahrenheit.reverse_get f in
  assert (abs_float (c2 -. c) < 0.001);

  (* String/chars roundtrip *)
  let s = "hello" in
  let cs = string_chars.get s in
  assert (cs = ['h'; 'e'; 'l'; 'l'; 'o']);
  assert (string_chars.reverse_get cs = s);

  (* Reverse iso *)
  let fahrenheit_celsius = reverse celsius_fahrenheit in
  assert (abs_float (fahrenheit_celsius.get 212.0 -. 100.0) < 0.001);

  (* Composition *)
  let km_raw = compose_iso km_to_m meters_iso in
  let (Kilometers k) = Kilometers 5.0 in
  assert (abs_float (km_raw.get (Kilometers 5.0) -. 5000.0) < 0.001);
  let back = km_raw.reverse_get 5000.0 in
  assert (back = Kilometers 5.0);

  print_endline "βœ“ All tests passed"

πŸ“Š Detailed Comparison

Comparison: Example 210 β€” Iso Basics

Iso Type

OCaml

πŸͺ Show OCaml equivalent
type ('s, 'a) iso = {
get : 's -> 'a;
reverse_get : 'a -> 's;
}

Rust

struct Iso<S, A> {
 get: Box<dyn Fn(&S) -> A>,
 reverse_get: Box<dyn Fn(&A) -> S>,
}

Reverse

OCaml

πŸͺ Show OCaml equivalent
let reverse i = { get = i.reverse_get; reverse_get = i.get }

Rust

fn reverse(self) -> Iso<A, S> {
 Iso { get: self.reverse_get, reverse_get: self.get }
}

Newtype Isos

OCaml

πŸͺ Show OCaml equivalent
type meters = Meters of float
let meters_iso = {
get = (fun (Meters m) -> m);
reverse_get = (fun m -> Meters m);
}

Rust

struct Meters(f64);
fn meters_iso() -> Iso<Meters, f64> {
 Iso::new(|m: &Meters| m.0, |f: &f64| Meters(*f))
}