ocular

Ocular - A lens library for Gleam

Ocular provides composable, type-safe optics for accessing and modifying nested data structures in Gleam. Inspired by F# Aether but designed specifically for Gleam’s strengths: pipe-first ergonomics, exhaustive pattern matching, and zero-cost abstractions on BEAM and JavaScript.

Quick Start

import ocular
import ocular/compose as c

// Define a lens for a record field
let name_lens = ocular.lens(
  get: fn(user: User) { user.name },
  set: fn(new_name, user: User) { User(..user, name: new_name) },
)

// Use it - subject (source) comes first for Gleam pipelines
let name = ocular.get(user, name_lens)
let new_user = ocular.set(user, name_lens, "Alice")
let upper_user = ocular.modify(user, name_lens, string.uppercase)

Composition

Import the compose module for combining optics:

import ocular/compose as c

// Compose lenses with the `|>` operator
let street_lens = user_company_lens
  |> c.lens(company_address_lens)
  |> c.lens(address_street_lens)

// Cross-type composition
let city_opt = user_address_lens
  |> c.lens_opt(address_city_opt)  // Lens + Optional = Optional

// Prism composition with review
let circle = ocular.review(circle_prism(), 5.0)  // Circle(5.0)

Optic Types Quick Reference

OpticCan Read?Can Write?Multi-focus?Reversible?Reliability
IsoNo100%
LensNo100%
PrismNoPartial
OptionalNoPartial
TraversalYes0 to N

Rule of thumb: The resulting optic is only as strong as its weakest link.

Importing Types

Import optic types from ocular/types:

import ocular/types.{type Lens, type SimpleLens, type Prism, type Optional, type Iso, type Traversal}

Values

pub fn dict_key(
  key: k,
) -> types.Optional(dict.Dict(k, v), dict.Dict(k, v), v, v)

Optional for dictionary key lookup. Returns Ok(value) if the key exists, Error(Nil) if not.

Example

let dict = dict.from_list([#("name", "Alice"), #("age", "30")])

let result = ocular.get_opt(dict, ocular.dict_key("name"))  // Ok("Alice")
let missing = ocular.get_opt(dict, ocular.dict_key("city"))  // Error(Nil)

// Setting creates the key if it doesn't exist
let added = ocular.set_opt(dict, ocular.dict_key("city"), "NYC")
pub fn dict_key_with_default(
  key: k,
  default: v,
) -> types.Lens(dict.Dict(k, v), dict.Dict(k, v), v, v)

Lens for dictionary key with default value. Always succeeds - returns default if key doesn’t exist.

Example

let dict = dict.from_list([#("name", "Alice")])

let lens = ocular.dict_key_with_default("name", "Unknown")
let name = ocular.get(dict, lens)  // "Alice"

let missing_lens = ocular.dict_key_with_default("age", "0")
let age = ocular.get(dict, missing_lens)  // "0" (default)
pub fn error() -> types.Prism(Result(a, e), Result(a, f), e, f)

Prism for the Error variant of Result.

Example

let x = Error("failure")
let result = ocular.preview(x, ocular.error())  // Ok("failure")

let y = Ok("success")
let fail = ocular.preview(y, ocular.error())  // Error(Nil)
pub fn first() -> types.Lens(#(a, b), #(c, b), a, c)

Lens for the first element of a 2-tuple.

Example

let pair = #("hello", 42)
let first = ocular.get(pair, ocular.first())  // "hello"
let modified = ocular.set(pair, ocular.first(), "world")  // #("world", 42)
pub fn first3() -> types.Lens(#(a, b, c), #(d, b, c), a, d)

Lens for the first element of a 3-tuple.

Example

let triple = #("a", "b", "c")
let first = ocular.get(triple, ocular.first3())  // "a"
pub fn get(source: s, lens: types.Lens(s, t, a, b)) -> a

Get the focused value from a structure using a lens.

Example

pub type User { User(name: String) }

let name_lens = ocular.lens(
  get: fn(u: User) { u.name },
  set: fn(v, u: User) { User(..u, name: v) },
)

let user = User(name: "Alice")
let name = ocular.get(user, name_lens)
// name == "Alice"
pub fn get_all(
  source: s,
  traversal: types.Traversal(s, t, a, b),
) -> List(a)

Get all focused values from a structure.

Example

let items = [1, 2, 3]
let all = ocular.get_all(items, ocular.list_traversal())
// all == [1, 2, 3]
pub fn get_iso(source: s, iso: types.Iso(s, t, a, b)) -> a

Get the value through an iso.

Example

// Iso between Int and String (via int_to_string/string_to_int)
let iso = ocular.iso(
  get: fn(n: Int) { int.to_string(n) },
  reverse: fn(s: String) { 
    case int.parse(s) { Ok(n) -> n Error(_) -> 0 }
  },
)

let s = ocular.get_iso(42, iso)
// s == "42"
pub fn get_opt(
  source: s,
  optional: types.Optional(s, t, a, b),
) -> Result(a, Nil)

Try to get the focused value from a structure using an optional. Returns Ok(value) if the focus exists, Error(Nil) otherwise.

Example

let dict = dict.from_list([#("name", "Alice")])
let result = ocular.get_opt(dict, ocular.dict_key("name"))
// result == Ok("Alice")

let missing = ocular.get_opt(dict, ocular.dict_key("age"))
// missing == Error(Nil)
pub fn iso(
  get get_fn: fn(s) -> a,
  reverse reverse_fn: fn(b) -> t,
) -> types.Iso(s, t, a, b)

Create an iso from get and reverse functions.

Example

// Iso between List(a) and List(a) (reverse)
let reverse_iso = ocular.iso(
  get: fn(xs: List(a)) { list.reverse(xs) },
  reverse: fn(xs: List(a)) { list.reverse(xs) },
)

let result = ocular.get_iso([1, 2, 3], reverse_iso)  // [3, 2, 1]
let back = ocular.reverse(reverse_iso, [3, 2, 1])  // [1, 2, 3]
pub fn lens(
  get get_fn: fn(s) -> a,
  set set_fn: fn(b, s) -> t,
) -> types.Lens(s, t, a, b)

Create a lens from get and set functions.

Example

pub type User { User(name: String, age: Int) }

let name_lens = ocular.lens(
  get: fn(u: User) { u.name },
  set: fn(new_name, u: User) { User(..u, name: new_name) },
)

let user = User(name: "Alice", age: 30)
let new_name = ocular.get(user, name_lens)  // "Alice"
let new_user = ocular.set(user, name_lens, "Bob")  // User(name: "Bob", age: 30)
pub fn list_head(
  default: a,
) -> types.Lens(List(a), List(a), a, a)

Lens for the head (first element) of a list. Returns default if the list is empty.

Example

let items = [1, 2, 3]
let head = ocular.get(items, ocular.list_head(0))  // 1

let empty = []
let default_head = ocular.get(empty, ocular.list_head(0))  // 0

// Set the head
let modified = ocular.set(items, ocular.list_head(0), 10)  // [10, 2, 3]
pub fn list_index(
  index: Int,
) -> types.Optional(List(a), List(a), a, a)

Optional for list index access. Returns Ok(value) if the index exists, Error(Nil) if out of bounds.

Example

let items = ["a", "b", "c"]
let second = ocular.get_opt(items, ocular.list_index(1))  // Ok("b")
let missing = ocular.get_opt(items, ocular.list_index(5))  // Error(Nil)

// Set at index
let modified = ocular.set_opt(items, ocular.list_index(1), "X")  // ["a", "X", "c"]
pub fn list_tail() -> types.Lens(
  List(a),
  List(a),
  List(a),
  List(a),
)

Lens for the tail (rest) of a list. Returns [] if the list is empty.

Example

let items = [1, 2, 3]
let tail = ocular.get(items, ocular.list_tail())  // [2, 3]

// Set the tail
let modified = ocular.set(items, ocular.list_tail(), [4, 5])  // [1, 4, 5]
pub fn list_traversal() -> types.Traversal(List(a), List(b), a, b)

Traversal for all elements of a list. Focuses on every element at once.

Example

let items = [1, 2, 3]

// Get all elements
let all = ocular.get_all(items, ocular.list_traversal())  // [1, 2, 3]

// Modify all elements
let doubled = ocular.modify_all(items, ocular.list_traversal(), fn(x) { x * 2 })
// doubled == [2, 4, 6]

// Set all elements to same value
let zeros = ocular.set_all(items, ocular.list_traversal(), 0)
// zeros == [0, 0, 0]
pub fn modify(
  source: s,
  lens: types.Lens(s, t, a, b),
  with f: fn(a) -> b,
) -> t

Modify a value using a function through a lens.

Example

let user = User(name: "alice")
let upper = ocular.modify(user, name_lens, string.uppercase)
// upper == User(name: "ALICE")

With use syntax

use name <- ocular.modify(user, name_lens)
name |> string.uppercase |> string.append("!")
pub fn modify_all(
  source: s,
  traversal: types.Traversal(s, t, a, b),
  with f: fn(a) -> b,
) -> t

Modify all focused values through a traversal. Alias for update.

Example

let items = ["a", "b", "c"]
let upper = ocular.modify_all(items, ocular.list_traversal(), string.uppercase)
// upper == ["A", "B", "C"]
pub fn modify_iso(
  source: s,
  iso: types.Iso(s, t, a, b),
  with f: fn(a) -> b,
) -> t

Modify through an iso.

Example

// Iso between String and List(String) (chars)
let char_iso = ocular.iso(
  get: fn(s: String) { string.to_graphemes(s) },
  reverse: fn(cs: List(String)) { string.concat(cs) },
)

let result = ocular.modify_iso("hello", char_iso, fn(cs) { list.reverse(cs) })
// result == "olleh"
pub fn modify_opt(
  source: s,
  optional: types.Optional(s, s, a, a),
  with f: fn(a) -> a,
) -> s

Modify a value through an optional if it exists. If the path doesn’t exist, returns the source unchanged.

Example

let dict = dict.from_list([#("name", "alice")])
let upper = ocular.modify_opt(dict, ocular.dict_key("name"), string.uppercase)
// dict.get(upper, "name") == Ok("ALICE")

// Missing key - unchanged
let same = ocular.modify_opt(dict, ocular.dict_key("missing"), string.uppercase)
// same == dict (unchanged)
pub fn modify_prism(
  source: s,
  prism: types.Prism(s, s, a, a),
  with f: fn(a) -> a,
) -> s

Modify a value through a prism if it matches. If the source doesn’t match, returns the source unchanged.

Example

let x = Some(5)
let doubled = ocular.modify_prism(x, ocular.some(), fn(n) { n * 2 })
// doubled == Some(10)

let y = None
let same = ocular.modify_prism(y, ocular.some(), fn(n) { n * 2 })
// same == None (unchanged)
pub fn ok() -> types.Prism(Result(a, e), Result(b, e), a, b)

Prism for the Ok variant of Result.

Example

let x = Ok("success")
let result = ocular.preview(x, ocular.ok())  // Ok("success")

let y = Error("fail")
let fail = ocular.preview(y, ocular.ok())  // Error(Nil)
pub fn optional(
  get get_fn: fn(s) -> Result(a, Nil),
  set set_fn: fn(b, s) -> t,
) -> types.Optional(s, t, a, b)

Create an optional from get and set functions.

Example

pub type Config {
  Config(timeout: Option(Int), retries: Option(Int))
}

let timeout_opt = ocular.optional(
  get: fn(c: Config) {
    case c.timeout {
      Some(t) -> Ok(t)
      None -> Error(Nil)
    }
  },
  set: fn(t, c: Config) { Config(..c, timeout: Some(t)) },
)

let cfg = Config(timeout: Some(30), retries: None)
let t = ocular.get_opt(cfg, timeout_opt)  // Ok(30)
let new_cfg = ocular.set_opt(cfg, timeout_opt, 60)
pub fn over(
  source: s,
  lens: types.Lens(s, t, a, b),
  with f: fn(a) -> b,
) -> t

Alias for modify. Aether-style naming.

Example

let user = User(name: "alice")
let upper = ocular.over(user, name_lens, string.uppercase)
pub fn preview(
  source: s,
  prism: types.Prism(s, t, a, b),
) -> Result(a, Nil)

Try to get the focused value from a structure using a prism. Returns Ok(value) if the prism matches, Error(Nil) otherwise.

Example

let x = Ok(42)
let result = ocular.preview(x, ocular.ok())
// result == Ok(42)

let y = Error("fail")
let result = ocular.preview(y, ocular.ok())
// result == Error(Nil)
pub fn prism(
  get get_fn: fn(s) -> Result(a, Nil),
  set set_fn: fn(b, s) -> t,
  review review_fn: fn(b) -> t,
) -> types.Prism(s, t, a, b)

Create a prism from get, set, and review functions.

Example

pub type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
}

let circle_prism = ocular.prism(
  get: fn(s: Shape) {
    case s {
      Circle(r) -> Ok(r)
      Rectangle(_, _) -> Error(Nil)
    }
  },
  set: fn(r, _s: Shape) { Circle(r) },
  review: fn(r) { Circle(r) },
)

// Preview: extract radius from Circle
let result = ocular.preview(Circle(5.0), circle_prism)  // Ok(5.0)
let fail = ocular.preview(Rectangle(3.0, 4.0), circle_prism)  // Error(Nil)

// Review: construct Circle from radius
let circle = ocular.review(circle_prism, 10.0)  // Circle(10.0)
pub fn reverse(iso: types.Iso(s, t, a, b), value: b) -> t

Reverse an iso to get back the original type.

Example

let iso = ocular.iso(
  get: fn(n: Int) { int.to_string(n) },
  reverse: fn(s: String) { 
    case int.parse(s) { Ok(n) -> n Error(_) -> 0 }
  },
)

let n = ocular.reverse(iso, "42")
// n == 42
pub fn review(prism: types.Prism(s, t, a, b), value: b) -> t

Review (construct) a value using a prism. Creates the whole structure from a part, without needing an existing source.

Example

let circle = ocular.review(circle_prism(), 5.0)
// circle == Circle(5.0)

let some_val = ocular.review(ocular.some(), "hello")
// some_val == Some("hello")
pub fn second() -> types.Lens(#(a, b), #(a, c), b, c)

Lens for the second element of a 2-tuple.

Example

let pair = #("hello", 42)
let second = ocular.get(pair, ocular.second())  // 42
let modified = ocular.set(pair, ocular.second(), 100)  // #("hello", 100)
pub fn second3() -> types.Lens(#(a, b, c), #(a, d, c), b, d)

Lens for the second element of a 3-tuple.

Example

let triple = #("a", "b", "c")
let second = ocular.get(triple, ocular.second3())  // "b"
pub fn set(
  source: s,
  lens: types.Lens(s, t, a, b),
  value: b,
) -> t

Set a new value using a lens, returning the modified structure.

Example

let user = User(name: "Alice")
let new_user = ocular.set(user, name_lens, "Bob")
// new_user == User(name: "Bob")
pub fn set_all(
  source: s,
  traversal: types.Traversal(s, t, a, b),
  value: b,
) -> t

Set all focused values through a traversal to a constant value.

Example

let items = [1, 2, 3]
let zeros = ocular.set_all(items, ocular.list_traversal(), 0)
// zeros == [0, 0, 0]
pub fn set_opt(
  source: s,
  optional: types.Optional(s, t, a, b),
  value: b,
) -> t

Set a value through an optional. Works even if the focus doesn’t currently exist (depends on the optional’s implementation).

Example

let dict = dict.new()
let new_dict = ocular.set_opt(dict, ocular.dict_key("key"), "value")
// dict.get(new_dict, "key") == Ok("value")
pub fn set_opt_mono(
  source: s,
  optional: types.Optional(s, s, a, a),
  value: a,
) -> s

Monomorphic set for optionals - returns source unchanged if path doesn’t exist. This is the “safe” version that won’t create new keys/entries.

Example

let dict = dict.from_list([#("exists", "value")])

// Regular set_opt might create the key
let with_regular = ocular.set_opt(dict, ocular.dict_key("new"), "x")

// set_opt_mono leaves it unchanged
let unchanged = ocular.set_opt_mono(dict, ocular.dict_key("new"), "x")
// dict is unchanged because the key doesn't exist
pub fn set_prism(
  source: s,
  prism: types.Prism(s, s, a, a),
  value: a,
) -> s

Set a value through a prism. If the source doesn’t match the prism’s variant, returns the source unchanged.

Example

let x = Some("hello")
let new_x = ocular.set_prism(x, ocular.some(), "world")
// new_x == Some("world")

let y = None
let same = ocular.set_prism(y, ocular.some(), "world")
// same == None (unchanged)
pub fn some() -> types.Prism(
  option.Option(a),
  option.Option(b),
  a,
  b,
)

Prism for the Some variant of Option.

Example

let x = Some("value")
let result = ocular.preview(x, ocular.some())  // Ok("value")

let y = None
let fail = ocular.preview(y, ocular.some())  // Error(Nil)

// Construct Some
let some_val = ocular.review(ocular.some(), "hello")  // Some("hello")
pub fn some_with_default(
  default: a,
) -> types.Lens(option.Option(a), option.Option(a), a, a)

Lens for Some variant with default value for None. Always succeeds - returns default if None.

Example

let x = Some("hello")
let value = ocular.get(x, ocular.some_with_default("default"))  // "hello"

let y = None
let default = ocular.get(y, ocular.some_with_default("default"))  // "default"

// Setting wraps in Some
let modified = ocular.set(y, ocular.some_with_default(""), "world")  // Some("world")
pub fn third3() -> types.Lens(#(a, b, c), #(a, b, d), c, d)

Lens for the third element of a 3-tuple.

Example

let triple = #("a", "b", "c")
let third = ocular.get(triple, ocular.third3())  // "c"
pub fn update(
  source: s,
  traversal: types.Traversal(s, t, a, b),
  with f: fn(a) -> b,
) -> t

Update all focused values using a function.

Example

let items = [1, 2, 3]
let doubled = ocular.update(items, ocular.list_traversal(), fn(x) { x * 2 })
// doubled == [2, 4, 6]
Search Document