Optics Composition Patterns
Patterns for combining Orlando's optics with transducer pipelines for expressive, immutable data transformations.
Streaming Lenses: Optics + Transducers
Orlando uniquely combines lenses with transducer pipelines. No other lens library offers this.
Extract, Filter, Transform
import init, { Pipeline, lens, lensPath } from 'orlando-transducers';
await init();
const users = [
{ name: "Alice", profile: { email: "alice@company.com", verified: true }},
{ name: "Bob", profile: { email: "bob@gmail.com", verified: false }},
{ name: "Carol", profile: { email: "carol@company.com", verified: true }},
];
const emailLens = lensPath(['profile', 'email']);
const verifiedLens = lensPath(['profile', 'verified']);
// Pipeline with lens-based extraction
const companyEmails = new Pipeline()
.filterLens(verifiedLens, v => v === true) // filter by lens value
.viewLens(emailLens) // extract via lens
.filter(email => email.endsWith('@company.com'))
.toArray(users);
// Result: ["alice@company.com", "carol@company.com"]
Batch Updates via Pipeline
const products = [
{ id: 1, name: "Widget", price: 10, category: "tools" },
{ id: 2, name: "Gadget", price: 20, category: "tools" },
{ id: 3, name: "Doohickey", price: 15, category: "accessories" },
];
const priceLens = lens('price');
// Apply 20% discount to all items via pipeline
const discounted = new Pipeline()
.overLens(priceLens, price => price * 0.8)
.toArray(products);
// Each product has price * 0.8, originals unchanged
Selective Updates
const categoryLens = lens('category');
const priceLens = lens('price');
// Discount only tools
const discountTools = new Pipeline()
.map(product => {
if (categoryLens.get(product) === 'tools') {
return priceLens.over(product, price => price * 0.8);
}
return product;
})
.toArray(products);
Redux-Style State Management
Lens-Based Reducers
import { lens, lensPath } from 'orlando-transducers';
const state = {
user: {
profile: { name: "Alice", email: "alice@example.com" },
preferences: { theme: "dark", notifications: true },
},
cart: { items: [], total: 0 },
};
// Define lenses for each slice of state
const nameLens = lensPath(['user', 'profile', 'name']);
const themeLens = lensPath(['user', 'preferences', 'theme']);
const cartLens = lens('cart');
const totalLens = lensPath(['cart', 'total']);
// Reducers become simple lens operations
function reducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return nameLens.set(state, action.payload);
case 'TOGGLE_THEME':
return themeLens.over(state, theme =>
theme === 'dark' ? 'light' : 'dark'
);
case 'SET_TOTAL':
return totalLens.set(state, action.payload);
default:
return state;
}
}
Multiple Updates
// Chain lens operations for multiple immutable updates
const newState = themeLens.set(
nameLens.set(state, "Alicia"),
"light"
);
// Original state unchanged
console.log(state.user.profile.name); // "Alice"
console.log(newState.user.profile.name); // "Alicia"
console.log(newState.user.preferences.theme); // "light"
Deep Nested Access with lensPath
const config = {
database: {
primary: {
host: "db.example.com",
port: 5432,
credentials: {
username: "admin",
password: "secret",
},
},
},
};
const dbHostLens = lensPath(['database', 'primary', 'host']);
const dbPortLens = lensPath(['database', 'primary', 'port']);
const dbUserLens = lensPath(['database', 'primary', 'credentials', 'username']);
dbHostLens.get(config); // "db.example.com"
dbPortLens.set(config, 5433); // new config with updated port
dbUserLens.over(config, u => u.toUpperCase()); // new config with "ADMIN"
Optional Fields
import { optional } from 'orlando-transducers';
const phoneLens = optional('phone');
const bioLens = optional('bio');
const users = [
{ name: "Alice", phone: "555-0100" },
{ name: "Bob" }, // no phone
{ name: "Carol", phone: "555-0102", bio: "Developer" },
];
// Safe extraction with defaults
const pipeline = new Pipeline()
.map(user => ({
name: user.name,
phone: phoneLens.getOr(user, "N/A"),
bio: bioLens.getOr(user, "No bio provided"),
}))
.toArray(users);
// Result:
// [
// { name: "Alice", phone: "555-0100", bio: "No bio provided" },
// { name: "Bob", phone: "N/A", bio: "No bio provided" },
// { name: "Carol", phone: "555-0102", bio: "Developer" },
// ]
Prism Patterns: Sum Types
import { prism } from 'orlando-transducers';
// API response: { status: "success", data: ... } or { status: "error", message: ... }
const successPrism = prism(
resp => resp.status === 'success' ? resp.data : undefined,
data => ({ status: 'success', data })
);
const errorPrism = prism(
resp => resp.status === 'error' ? resp.message : undefined,
message => ({ status: 'error', message })
);
const responses = [
{ status: 'success', data: { id: 1 } },
{ status: 'error', message: 'Not found' },
{ status: 'success', data: { id: 2 } },
];
// Extract only successful data
const pipeline = new Pipeline()
.map(resp => successPrism.preview(resp))
.filter(data => data !== undefined)
.toArray(responses);
// Result: [{ id: 1 }, { id: 2 }]
Rust: Optics Composition
#![allow(unused)] fn main() { use orlando_transducers::optics::{Lens, Fold, Traversal}; // Compose lenses for deep access let department_lens = Lens::new( |company: &Company| company.department.clone(), |company: &Company, dept: Department| Company { department: dept, ..company.clone() }, ); let employees_traversal = Traversal::new( |dept: &Department| dept.employees.clone(), |dept: &Department, f: &dyn Fn(&Employee) -> Employee| { Department { employees: dept.employees.iter().map(f).collect(), ..dept.clone() } }, ); let name_lens = Lens::new( |emp: &Employee| emp.name.clone(), |emp: &Employee, name: String| Employee { name, ..emp.clone() }, ); // Company -> Department -> [Employee] -> name // Use traversal to update all employee names let dept = employees_traversal.over_all(&department, |emp| { name_lens.over(emp, |n| n.to_uppercase()) }); }
Iso Patterns: Unit Conversions
#![allow(unused)] fn main() { use orlando_transducers::optics::Iso; let meters_feet = Iso::new( |m: &f64| m * 3.28084, |f: &f64| f / 3.28084, ); let celsius_fahrenheit = Iso::new( |c: &f64| c * 9.0 / 5.0 + 32.0, |f: &f64| (f - 32.0) * 5.0 / 9.0, ); // Isos are reversible let feet_meters = meters_feet.reverse(); assert_eq!(feet_meters.to(&3.28084), 1.0); // Isos can be used as lenses let as_lens = celsius_fahrenheit.as_lens(); let f = as_lens.get(&100.0); // 212.0 }