// 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"