Optics API

Orlando provides a complete hierarchy of functional optics for immutable, composable data access and transformation.

Overview

OpticFocusReadWriteUse Case
LensExactly one valueYesYesObject properties, nested fields
OptionalZero or one valueYesYesNullable fields, partial data
PrismZero or one (sum type)YesYes (construct)Tagged unions, enum variants
IsoExactly one (bidirectional)YesYesUnit conversions, encodings
TraversalZero or more valuesYesYesCollections, arrays
FoldZero or more valuesYesNoRead-only aggregation

Lens

Focus on exactly one part of a data structure with read/write access.

JavaScript

import { lens, lensPath } from 'orlando-transducers';

// Property lens
const nameLens = lens('name');
nameLens.get(user);                              // "Alice"
nameLens.set(user, "Bob");                       // { ...user, name: "Bob" }
nameLens.over(user, s => s.toUpperCase());       // { ...user, name: "ALICE" }

// Path lens for deep access
const cityLens = lensPath(['address', 'city']);
cityLens.get(user);                              // "NYC"
cityLens.set(user, "Boston");                    // deep immutable update

// Composition
const addressLens = lens('address');
const zipLens = lens('zip');
const userZipLens = addressLens.compose(zipLens);
userZipLens.get(user);                           // "10001"

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::optics::Lens;

let name_lens = Lens::new(
    |user: &User| user.name.clone(),
    |user: &User, name: String| User { name, ..user.clone() },
);

let name = name_lens.get(&user);
let updated = name_lens.set(&user, "Bob".into());
let shouted = name_lens.over(&user, |n| n.to_uppercase());

// Composition via then()
let user_city = address_lens.then(&city_lens);
}

Lens Laws

All Orlando lenses satisfy:

  1. GetPut: set(s, get(s)) = s
  2. PutGet: get(set(s, a)) = a
  3. PutPut: set(set(s, a1), a2) = set(s, a2)

Optional

Like a Lens, but the focus may not exist. Safe for nullable or missing fields.

JavaScript

import { optional } from 'orlando-transducers';

const phoneLens = optional('phone');
phoneLens.get(user);                 // undefined (missing field)
phoneLens.getOr(user, "N/A");       // "N/A" (with default)
phoneLens.set(user, "555-0100");     // { ...user, phone: "555-0100" }
phoneLens.over(user, normalize);     // no-op if undefined

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::optics::Optional;

let phone = Optional::new(
    |u: &User| u.phone.clone(),
    |u: &User, p: String| User { phone: Some(p), ..u.clone() },
);

let val = phone.get_or(&user, "N/A".into());
}

Prism

Focus on one variant of a sum type. Can both match (preview) and construct (review).

JavaScript

import { prism } from 'orlando-transducers';

const somePrism = prism(
  x => x.tag === 'Some' ? x.value : undefined,  // preview
  v => ({ tag: 'Some', value: v })                // review
);

somePrism.preview({ tag: 'Some', value: 42 });   // 42
somePrism.preview({ tag: 'None' });               // undefined
somePrism.review(42);                              // { tag: 'Some', value: 42 }

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::optics::Prism;

let some_prism = Prism::new(
    |opt: &Option<i32>| *opt,
    |val: i32| Some(val),
);

assert_eq!(some_prism.preview(&Some(42)), Some(42));
assert_eq!(some_prism.review(42), Some(42));
}

Iso

Lossless, bidirectional conversion between two types.

JavaScript

import { iso } from 'orlando-transducers';

const tempIso = iso(
  c => c * 9/5 + 32,   // Celsius -> Fahrenheit
  f => (f - 32) * 5/9   // Fahrenheit -> Celsius
);

tempIso.to(100);        // 212
tempIso.from(212);      // 100
tempIso.reverse().to(212);  // 100

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::optics::Iso;

let celsius_fahrenheit = Iso::new(
    |c: &f64| c * 9.0 / 5.0 + 32.0,
    |f: &f64| (f - 32.0) * 5.0 / 9.0,
);

// Can be used as either a Lens or a Prism
let as_lens = celsius_fahrenheit.as_lens();
let as_prism = celsius_fahrenheit.as_prism();
}

Traversal

Focus on zero or more values within a structure. Supports reading all and updating all.

JavaScript

import { traversal } from 'orlando-transducers';

const itemsTraversal = traversal(
  arr => arr,
  (arr, fn) => arr.map(fn)
);

itemsTraversal.getAll([1, 2, 3]);               // [1, 2, 3]
itemsTraversal.overAll([1, 2, 3], x => x * 2);  // [2, 4, 6]
itemsTraversal.setAll([1, 2, 3], 0);             // [0, 0, 0]

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::optics::Traversal;

let each = Traversal::new(
    |v: &Vec<i32>| v.clone(),
    |v: &Vec<i32>, f: &dyn Fn(&i32) -> i32| v.iter().map(f).collect(),
);

let doubled = each.over_all(&vec![1, 2, 3], |x| x * 2);  // [2, 4, 6]
}

Fold

Read-only traversal for extracting and aggregating values.

JavaScript

import { fold } from 'orlando-transducers';

const valuesFold = fold(obj => Object.values(obj));

valuesFold.getAll({ a: 1, b: 2, c: 3 });  // [1, 2, 3]
valuesFold.isEmpty({});                     // true
valuesFold.length({ a: 1, b: 2 });          // 2
valuesFold.first({ a: 1, b: 2 });           // 1

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::optics::Fold;

let items = Fold::fold_of(|v: &Vec<i32>| v.clone());

items.any(&data, |x| *x > 10);     // true if any > 10
items.all(&data, |x| *x > 0);      // true if all > 0
items.find(&data, |x| *x > 5);     // Some(first > 5)
items.is_empty(&data);              // bool
items.length(&data);                // usize
items.first(&data);                 // Option<i32>
}

Cross-Type Conversions

Optics can be widened to more general types:

FromToMethod
LensTraversal.to_traversal()
LensFold.to_fold()
PrismTraversal.to_traversal()
PrismFold.to_fold()
IsoLens.as_lens()
IsoPrism.as_prism()
TraversalFold.as_fold()

Composition

All optics support composition for deeper access:

// JavaScript
const userCity = addressLens.compose(cityLens);
#![allow(unused)]
fn main() {
// Rust
let user_city = address_lens.then(&city_lens);
let deep_fold = outer_fold.then(&inner_fold);
let nested = outer_traversal.then(&inner_traversal);
}