___________
       /           \
      /    _____    \
     |    /     \    |
     |   |   ●   |   | 
     |    \_____/    |
      \             /
       \___________/
            ||
            ||
         ___||___
        |________|

Package Version Hex Docs

A lens library for Gleam. Ocular provides composable, type-safe optics for accessing and modifying nested data structures. 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.

Installation

gleam add ocular@1

Quick Start

import ocular
import ocular/compose as c

// Define your data types
pub type User {
  User(name: String, age: Int)
}

// Create a lens for a 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
let user = User(name: "Alice", age: 30)

ocular.get(user, name_lens)           // "Alice"
ocular.set(user, name_lens, "Bob")    // User(name: "Bob", age: 30)
ocular.modify(user, name_lens, string.uppercase)  // User(name: "ALICE", age: 30)

Composition (The Aether Way)

Ocular embraces Gleam’s pipe operator for composition:

// Compose lenses for nested access
let street_lens = user_company_lens
  |> c.lens(company_address_lens)
  |> c.lens(address_street_lens)

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

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

Composition Reference

FunctionInputOutputDescription
c.lensLens + LensLensFocus deeper
c.optionalOptional + OptionalOptionalChain fallible paths
c.prismPrism + PrismPrismChain variant matching
c.isoIso + IsoIsoChain isomorphisms
c.lens_optLens + OptionalOptionalFocus then try
c.opt_lensOptional + LensOptionalTry then focus
c.prism_lensPrism + LensOptionalMatch then focus
c.prism_optPrism + OptionalOptionalMatch then try
c.iso_lensIso + LensLensShift then focus
c.lens_isoLens + IsoLensFocus then shift
c.iso_prismIso + PrismPrismShift then match
c.prism_isoPrism + IsoPrismMatch then shift
c.iso_optIso + OptionalOptionalShift then try

Note: prism_lens returns an Optional (not a Prism) because we can’t implement review without a default value for the middle structure.

Optic Types

Ocular provides five optic types, each with different capabilities:

OpticCan Read?Can Write?Multi-focus?Reversible?Reliability
IsoNo100% (Guaranteed)
LensNo100% (Guaranteed)
PrismNoPartial (May fail)
OptionalNoPartial (May fail)
TraversalYes0 to N

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

When to use each:

Working with Optional Values

Handle paths that might not exist:

import ocular/compose as c

// Dictionary key access returns an Optional
let name_opt = user
  |> c.lens_opt(ocular.dict_key("name"))  // May fail

// Safe access - returns Result
ocular.get_opt(name_opt, user)  // Ok("Alice") or Error(Nil)

// Safe update
ocular.set_opt(name_opt, "Bob", user)

Common Optics

Ocular provides built-in optics for standard library types:

import ocular
import ocular/compose as c

// Dict access
let name_opt = ocular.dict_key("name")
ocular.get_opt(name_opt, dict)  // Ok(value) or Error(Nil)

// List access by index
let second_opt = ocular.list_index(1)
ocular.get_opt(second_opt, ["a", "b", "c"])  // Ok("b")

// List head (with default)
let head_lens = ocular.list_head("")
ocular.get(head_lens, ["a", "b"])  // "a"

// Option unwrapping
let some_prism = ocular.some()
ocular.preview(some_prism, Some("value"))  // Ok("value")
ocular.review(some_prism, "value")          // Some("value")

// Tuple access
let first_lens = ocular.first()
ocular.get(first_lens, #("hello", 42))  // "hello"

// List traversal (all elements)
let all_items = ocular.list_traversal()
ocular.get_all(all_items, [1, 2, 3])  // [1, 2, 3]

Polymorphic Updates

Lenses can change types during updates:

// String view of a User that returns HtmlUser
fn user_display_lens() {
  ocular.lens(
    get: fn(user: User) { user.name },
    set: fn(html: Html, user: User) { HtmlUser(..user, display: html) },
  )
}

// Changes type from User to HtmlUser!
let html_user = ocular.set(user_display_lens(), Html("<b>Alice</b>"), user)

Code Generation (Optional)

Since Gleam doesn’t have macros, Ocular provides an optional code generator to eliminate lens boilerplate.

Option 1: Use the Generator (Recommended for larger projects)

Copy the generator to your project:

# Copy the generator from ocular's examples
cp build/packages/ocular/examples/ocular_gen_full.gleam src/ocular_gen.gleam

# Add dev dependencies
gleam add --dev glance simplifile

# Generate lenses
gleam run -m ocular_gen -- src/models.gleam src/models/lenses.gleam

Input (src/models.gleam):

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

Output (src/models/lenses.gleam):

// AUTO-GENERATED - do not edit manually
import ocular
import ocular/types.{type Lens, Lens}
import models

pub fn user_name() -> Lens(User, User, String, String) {
  Lens(
    get: fn(s) { s.name },
    set: fn(v, s) { User(..s, name: v) },
  )
}
// ... etc

Option 2: Write Lenses by Hand (Fine for smaller projects)

pub fn user_name() {
  ocular.lens(
    get: fn(u: User) { u.name },
    set: fn(v, u: User) { User(..u, name: v) },
  )
}

Why a Template?

The generator requires additional dependencies (glance, simplifile) that not all users need. By providing it as a copy-paste template:

Future: Separate Package

In the future, ocular_gen may be published as a separate Hex package:

gleam add --dev ocular_gen  # Would include glance + simplifile automatically

Development

gleam run   # Run the project
gleam test  # Run the tests
gleam docs   # Generate documentation

Acknowledgements

Ocular is heavily inspired by the brilliant Aether library for F#, created by Andrew Cherry (xyncro) and contributors. Aether’s elegant approach to optic composition (e.g., lens_opt, prism_iso) strongly influenced Ocular’s design.

License

This project is licensed under the MIT License.

Search Document