ocular/compose

Composition functions for combining optics.

This module provides functions for composing optics together. The naming convention follows F# Aether: same-type compositions use the optic name (e.g., lens), while cross-type compositions combine the names (e.g., lens_opt, prism_lens).

Same-Type Composition

import ocular/compose as c

// Compose two lenses
let street_lens = user_address_lens |> c.lens(address_street_lens)

// Compose two optionals
let deep_opt = level1_opt |> c.optional(level2_opt)

// Compose two prisms
let circle_or_rect = outer_prism |> c.prism(inner_prism)

Cross-Type Composition

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

// Prism + Lens = Optional (review can't be implemented)
let some_name_opt = some_prism |> c.prism_lens(person_name_lens)

// Iso + Lens = Lens
let wrapped_name = wrapper_iso |> c.iso_lens(person_name_lens)

Composition Table

OuterInnerResultFunction
LensLensLenslens
LensOptionalOptionallens_opt
OptionalLensOptionalopt_lens
OptionalOptionalOptionaloptional
PrismPrismPrismprism
PrismLensOptionalprism_lens
PrismOptionalOptionalprism_opt
IsoIsoIsoiso
IsoLensLensiso_lens
IsoPrismPrismiso_prism
IsoOptionalOptionaliso_opt
OptionalPrismOptionalopt_prism

Values

pub fn epi(
  outer: types.Epimorphism(a, a, c, c),
  inner: types.Epimorphism(c, c, e, e),
) -> types.Epimorphism(a, a, e, e)

Compose two epimorphisms. Result: Epimorphism (both gets may fail)

Example

// String -> Int (parse), Int -> Bool (is positive)
let string_positive = string_int_epi |> compose.epi(int_positive_epi)

compose.get_epi("42", string_positive)  // Ok(True)
compose.get_epi("-5", string_positive)  // Ok(False)
compose.get_epi("abc", string_positive)  // Error(Nil)
pub fn epi_iso(
  epi: types.Epimorphism(a, a, c, c),
  iso: types.Iso(c, c, e, e),
) -> types.Epimorphism(a, a, e, e)

Compose an epimorphism with an iso. Result: Epimorphism

Example

// String -> Int (parse), Int -> Float (convert)
let string_float = string_int_epi |> compose.epi_iso(int_float_iso)

compose.get_epi("3.14", string_float)  // Error(Nil) - can't parse "3.14" as Int
compose.get_epi("42", string_float)    // Ok(42.0)
pub fn iso(
  outer: types.Iso(a, b, c, d),
  inner: types.Iso(c, d, e, f),
) -> types.Iso(a, b, e, f)

Compose two isos.

Example

// Iso: List(a) <-> List(a) (reverse)
let reverse_iso = ocular.iso(
  get: list.reverse,
  reverse: list.reverse,
)

// Compose: double reverse = identity
let identity = reverse_iso |> compose.iso(reverse_iso)
pub fn iso_epi(
  iso: types.Iso(a, a, c, c),
  epi: types.Epimorphism(c, c, e, e),
) -> types.Epimorphism(a, a, e, e)

Compose an iso with an epimorphism. Result: Epimorphism

Example

// List -> String (concat), String -> Int (parse)
let list_int = list_string_iso |> compose.iso_epi(string_int_epi)

compose.get_epi(["4", "2"], list_int)  // Ok(42) after concat->parse
pub fn iso_lens(
  iso: types.Iso(a, b, c, d),
  lens: types.Lens(c, d, e, f),
) -> types.Lens(a, b, e, f)

Compose an iso with a lens. Result: Lens

Example

// Wrapper iso: String <-> WrappedString
let wrapper_iso = ocular.iso(
  get: fn(s: String) { WrappedString(s) },
  reverse: fn(w: WrappedString) { w.value },
)

// Compose: WrappedString -> String -> Int (length)
let wrapped_length = wrapper_iso |> compose.iso_lens(string_length_lens)
pub fn iso_opt(
  iso: types.Iso(a, a, c, c),
  opt: types.Optional(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose an iso with an optional. Result: Optional

Example

// Convert Dict <-> List(Pair), then access by key
let dict_key_via_list = dict_list_iso |> compose.iso_opt(key_opt)
pub fn iso_prism(
  iso: types.Iso(a, b, c, d),
  prism: types.Prism(c, d, e, f),
) -> types.Prism(a, b, e, f)

Compose an iso with a prism. Result: Prism

Example

// Convert String <-> WrappedString, then match Some(WrappedString)
let wrapped_some = wrapper_iso |> compose.iso_prism(ocular.some())
pub fn lens(
  outer: types.Lens(a, b, c, d),
  inner: types.Lens(c, d, e, f),
) -> types.Lens(a, b, e, f)

Compose two lenses.

Example

pub type Address { Address(street: String) }
pub type User { User(address: Address) }

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

let street_lens = ocular.lens(
  get: fn(a: Address) { a.street },
  set: fn(v, a: Address) { Address(..a, street: v) },
)

// Compose: User -> Address -> String
let user_street = address_lens |> compose.lens(street_lens)

let user = User(address: Address(street: "Main St"))
let street = ocular.get(user, user_street)  // "Main St"
pub fn lens_epi(
  lens: types.Lens(a, a, c, c),
  epi: types.Epimorphism(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose a lens with an epimorphism. Result: Optional (the epimorphism may fail)

Example

// User -> String (name), String -> Int (parse)
let user_age_from_string = user_name_lens |> compose.lens_epi(string_int_epi)

// If user.name = "25", get_opt returns Ok(25)
// If user.name = "abc", get_opt returns Error(Nil)
pub fn lens_iso(
  lens: types.Lens(a, b, c, d),
  iso: types.Iso(c, d, e, f),
) -> types.Lens(a, b, e, f)

Compose a lens with an iso. Result: Lens

Example

// User.name is a String, convert to WrappedString
let wrapped_name = user_name_lens |> compose.lens_iso(wrapper_iso)
pub fn lens_opt(
  lens: types.Lens(a, b, c, d),
  opt: types.Optional(c, d, e, f),
) -> types.Optional(a, b, e, f)

Compose a lens with an optional. Result: Optional (since the path may not exist)

Example

pub type User { User(address: Option(Address)) }
pub type Address { Address(city: String) }

let address_opt = ocular.optional(
  get: fn(u: User) {
    case u.address {
      Some(a) -> Ok(a)
      None -> Error(Nil)
    }
  },
  set: fn(v, u: User) { User(..u, address: Some(v)) },
)

let city_lens = ocular.lens(
  get: fn(a: Address) { a.city },
  set: fn(v, a: Address) { Address(..a, city: v) },
)

// Compose: User -?-> Address -> String (Optional result)
let city_opt = address_opt |> compose.lens_opt(city_lens)

let user = User(address: Some(Address(city: "NYC")))
let city = ocular.get_opt(user, city_opt)  // Ok("NYC")
pub fn opt_lens(
  opt: types.Optional(a, a, c, c),
  lens: types.Lens(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose an optional with a lens. Result: Optional (since the outer path may not exist)

Example

// User has optional address, address has required street
let street_lens = ocular.lens(
  get: fn(a: Address) { a.street },
  set: fn(v, a: Address) { Address(..a, street: v) },
)

// Compose: User -?-> Address -> String
let street_opt = address_opt |> compose.opt_lens(street_lens)
pub fn opt_opt(
  outer: types.Optional(a, a, c, c),
  inner: types.Optional(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose two optionals (alias for optional).

pub fn opt_prism(
  opt: types.Optional(a, a, c, c),
  prism: types.Prism(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose an optional with a prism. Result: Optional

Example

// Optional address, address is Shape (Circle or Rectangle)
let address_circle = address_opt |> compose.opt_prism(circle_prism)
pub fn optional(
  outer: types.Optional(a, a, c, c),
  inner: types.Optional(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose two optionals. Result: Optional (both paths must exist)

Example

// Level1 -?-> Level2 -?-> String
let level1_opt: Optional(Level1, Level2) = ...
let level2_opt: Optional(Level2, String) = ...

let deep_opt = level1_opt |> compose.optional(level2_opt)

// Only succeeds if both levels exist
let result = ocular.get_opt(level1, deep_opt)
pub fn prism(
  outer: types.Prism(a, a, c, c),
  inner: types.Prism(c, c, e, e),
) -> types.Prism(a, a, e, e)

Compose two prisms. Both prisms must match for the composition to succeed.

Example

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

pub type Container {
  Box(shape: Shape)
  Bag(items: List(String))
}

let box_prism = ocular.prism(
  get: fn(c: Container) {
    case c {
      Box(s) -> Ok(s)
      Bag(_) -> Error(Nil)
    }
  },
  set: fn(s, _c) { Box(s) },
  review: fn(s) { Box(s) },
)

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

// Compose: Container -> Shape -> Float (only for Box(Circle(_)))
let box_circle_radius = box_prism |> compose.prism(circle_prism)

let container = Box(Circle(5.0))
let radius = ocular.preview(container, box_circle_radius)  // Ok(5.0)
pub fn prism_epi(
  prism: types.Prism(a, a, c, c),
  epi: types.Epimorphism(c, c, e, e),
) -> types.Prism(a, a, e, e)

Compose a prism with an epimorphism. Result: Prism

Example

// Some(String) -> String, String -> Int
let some_int = ocular.some() |> compose.prism_epi(string_int_epi)

compose.preview(Some("42"), some_int)  // Ok(42)
compose.review(some_int, 100)  // Some("100")
pub fn prism_iso(
  prism: types.Prism(a, b, c, d),
  iso: types.Iso(c, d, e, f),
) -> types.Prism(a, b, e, f)

Compose a prism with an iso. Result: Prism

Example

// Match Some(Int), then convert Int <-> String
let some_string = ocular.some() |> compose.prism_iso(int_string_iso)
pub fn prism_lens(
  prism: types.Prism(a, a, c, c),
  lens: types.Lens(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose a prism with a lens. Result: Optional (not Prism) because review cannot be implemented without a default value for the middle structure.

Example

// Option(Circle) with Circle having a radius field
let some_circle = ocular.some() |> compose.prism_lens(circle_radius_lens)

let maybe_circle = Some(Circle(5.0))
let radius = ocular.get_opt(maybe_circle, some_circle)  // Ok(5.0)
pub fn prism_opt(
  prism: types.Prism(a, a, c, c),
  opt: types.Optional(c, c, e, e),
) -> types.Optional(a, a, e, e)

Compose a prism with an optional. Result: Optional

Example

// Shape is Circle or Rectangle, Circle has optional label
let circle_label = circle_prism |> compose.prism_opt(label_opt)
Search Document