Skip to content
code-shoilyPublic

About

A functional lens library for Gleam - inspired by Aether

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

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

Package Version Hex Docs

Ocular

A functional lens and optics library for Gleam.

Ocular provides composable, type-safe tools to read and update deeply nested immutable data structures without boilerplate record update cascades. Inspired by F#'s Aether, built for Gleam's pipe-first ergonomics and zero-cost BEAM & JS targets.


The Problem

In pure functional languages like Gleam, updating a field 3 levels deep in nested immutable records requires tedious, repetitive boilerplate:

// Without optics: verbose, noisy, easy to introduce bugs
let updated_user =
  User(
    ..user,
    company: Company(
      ..user.company,
      address: Address(..user.company.address, street: "456 Elm St"),
    ),
  )

With Ocular, you compose your lenses once and update deeply in a single line:

let updated_user = ocular.set(user, user_street_lens, "456 Elm St")

Installation

gleam add ocular@1

Quick Start (30 Seconds)

import ocular
import ocular/compose as c

// 1. Define your data types
pub type Address {
  Address(street: String, city: String)
}

pub type User {
  User(name: String, address: Address)
}

// 2. Define lenses for the fields
let user_address_lens = ocular.lens(
  get: fn(u: User) { u.address },
  set: fn(new_addr, u: User) { User(..u, address: new_addr) },
)

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

// 3. Compose them! User -> Address -> Street
let user_street = user_address_lens |> c.lens(address_street_lens)

let alice = User(name: "Alice", address: Address(street: "123 Main St", city: "Springfield"))

// 4. Read, write, or modify deep values with ease:
ocular.get(alice, user_street)
// => "123 Main St"

ocular.set(alice, user_street, "456 Elm St")
// => User(name: "Alice", address: Address(street: "456 Elm St", city: "Springfield"))

ocular.modify(alice, user_street, string.uppercase)
// => User(name: "Alice", address: Address(street: "123 MAIN ST", city: "Springfield"))

Ready-to-Use Built-in Optics

You don't even need custom records to start using Ocular. Standard Gleam types come with built-in optics out of the box:

import gleam/dict
import gleam/option.{None, Some}
import ocular

// 1. Dictionary keys (returns Result(value, Nil))
let d = dict.from_list([#("name", "Alice")])
ocular.get_opt(d, ocular.dict_key("name"))  // Ok("Alice")
ocular.set_opt(d, ocular.dict_key("name"), "Bob")

// 2. Tuples
let pair = #("Alice", 30)
ocular.get(pair, ocular.first())   // "Alice"
ocular.set(pair, ocular.second(), 31) // #("Alice", 31)

// 3. Options (Prisms)
let opt = Some(42)
ocular.preview(opt, ocular.some())  // Ok(42)
ocular.review(ocular.some(), 100)   // Some(100)

// 4. Lists & Traversals (All elements at once)
let nums = [1, 2, 3]
ocular.get_all(nums, ocular.list_traversal())                  // [1, 2, 3]
ocular.modify_all(nums, ocular.list_traversal(), fn(x) { x * 2 }) // [2, 4, 6]

What are Optics?

Optics are composable abstractions for focusing on parts of immutable data structures:

Optic Can Read? Can Write? Focus Count When to use
Lens ✅ ✅ Exactly 1 Guaranteed record fields (user.name)
Prism ✅ (May fail) ✅ 0 or 1 Matching specific sum type variants (Some, Ok)
Optional ✅ (May fail) ✅ 0 or 1 Paths that might not exist (dict_key, list_index)
Iso ✅ ✅ Exactly 1 Lossless two-way conversions (e.g. record ↔ tuple)
Epimorphism ✅ (May fail) ✅ 0 or 1 Partial conversions with guaranteed reverse (e.g. parsing String → Int)
Traversal ✅ ✅ 0 to N Operating on multiple items simultaneously (e.g. all list elements)

Composing Optics

Optics compose cleanly using Gleam's pipe operator |>:

import ocular/compose as c

// Lens + Lens = Lens
let street_lens = user_company_lens |> c.lens(company_address_lens)

// Lens + Optional = Optional (if any step can fail, the whole chain returns Result)
let role_opt = user_tags_lens |> c.lens_opt(ocular.dict_key("role"))

ocular.get_opt(user, role_opt)
// => Ok("admin") or Error(Nil)

// Traversal + Lens = Traversal (focus on field across all list items)
let user_names = ocular.list_traversal() |> c.traversal_lens(user_name_lens)
ocular.get_all(users, user_names)
// => ["Alice", "Bob", "Charlie"]

📖 Looking for the complete composition guide?
Check out COMPOSITION_REFERENCE.md for the complete table of all cross-type composition functions (c.lens_opt, c.prism_lens, c.traversal_lens, etc.).


Eliminating Boilerplate: Code Generation

Writing lenses manually for large schemas can get repetitive. Ocular includes an optional code generator (powered by glance):

# 1. Add dev dependencies
gleam add --dev glance simplifile

# 2. Copy the generator template to your project
cp build/packages/ocular/examples/ocular_gen_full.gleam src/ocular_gen.gleam

# 3. Generate lenses, prisms, and optionals automatically!
gleam run -m ocular_gen -- 'src/domain/*.gleam' src/generated/optics.gleam

Given:

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

It automatically generates:

  • user_name() (Lens)
  • user_email() (Optional)
  • user_age() (Lens)

Optic Laws & Mathematical Correctness

Every optic in Ocular satisfies the formal algebraic optic laws (GetPut, PutGet, PutPut, ReviewPreview, etc.).

These are verified with property-based testing across thousands of randomized test cases using qcheck. See OPTIC_LAWS.md for details.


Development

gleam check
gleam test
gleam format --check src test

Acknowledgements

Ocular is inspired by Aether for F#, created by Andrew Cherry (xyncro) and contributors.

License

MIT License. See LICENSE.

About

A functional lens library for Gleam - inspired by Aether

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages