optics

package
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 18, 2025 License: Apache-2.0 Imports: 0 Imported by: 0

README ΒΆ

πŸ” Optics

Functional optics for composable data access and manipulation in Go.

πŸ“– Overview

Optics are first-class, composable references to parts of data structures. They provide a uniform interface for reading, writing, and transforming nested immutable data without verbose boilerplate code.

✨ Why Use Optics?

Optics bring powerful benefits to your Go code:

  • 🎯 Composability: Optics naturally compose with each other and with monadic operations, enabling elegant data transformations through function composition
  • πŸ”’ Immutability: Work with immutable data structures without manual copying and updating
  • 🧩 Type Safety: Leverage Go's type system to catch errors at compile time
  • πŸ“¦ Reusability: Define data access patterns once and reuse them throughout your codebase
  • 🎨 Expressiveness: Write declarative code that clearly expresses intent
  • πŸ”„ Bidirectionality: Read and write through the same abstraction
  • πŸš€ Productivity: Eliminate boilerplate for nested data access and updates
  • πŸ§ͺ Testability: Optics are pure functions, making them easy to test and reason about
πŸ”— Composition with Monadic Operations

One of the most powerful features of optics is their natural composition with monadic operations. Optics integrate seamlessly with fp-go's monadic types like Option, Either, Result, and IO, allowing you to:

  • Chain optional field access with Option monads
  • Handle errors gracefully with Either or Result monads
  • Perform side effects with IO monads
  • Combine multiple optics in a single pipeline using Pipe

This composability enables you to build complex data transformations from simple, reusable building blocks.

πŸš€ Quick Start

import (
    "github.com/IBM/fp-go/v2/optics/lens"
    F "github.com/IBM/fp-go/v2/function"
)

type Person struct {
    Name string
    Age  int
}

// Create a lens for the Name field
nameLens := lens.MakeLens(
    func(p Person) string { return p.Name },
    func(p Person, name string) Person {
        p.Name = name
        return p
    },
)

person := Person{Name: "Alice", Age: 30}

// Get the name
name := nameLens.Get(person) // "Alice"

// Set a new name (returns a new Person)
updated := nameLens.Set("Bob")(person)
// person.Name is still "Alice", updated.Name is "Bob"

πŸ› οΈ Core Optics Types

πŸ”Ž Lens - Product Types (Structs)

Focus on a single field within a struct. Provides get and set operations.

Use when: Working with struct fields that always exist.

ageLens := lens.MakeLens(
    func(p Person) int { return p.Age },
    func(p Person, age int) Person {
        p.Age = age
        return p
    },
)
πŸ”€ Prism - Sum Types (Variants)

Focus on one variant of a sum type. Provides optional get and definite set.

Use when: Working with Either, Result, or custom sum types.

πŸ’‘ Important Use Case - Generalized Constructors for Do Notation:

Prisms act as generalized constructors, making them invaluable for Do notation workflows. The prism's ReverseGet function serves as a constructor that creates a value of the sum type from a specific variant. This is particularly useful when building up complex data structures step-by-step in monadic contexts:

import "github.com/IBM/fp-go/v2/optics/prism"

// Prism for the Success variant
successPrism := prism.MakePrism(
    func(r Result) option.Option[int] {
        if s, ok := r.(Success); ok {
            return option.Some(s.Value)
        }
        return option.None[int]()
    },
    func(v int) Result { return Success{Value: v} }, // Constructor!
)

// Use in Do notation to construct values
result := F.Pipe2(
    computeValue(),
    option.Map(func(v int) int { return v * 2 }),
    option.Map(successPrism.ReverseGet), // Construct Result from int
)
πŸ”„ Iso - Isomorphisms

Bidirectional transformation between equivalent types with no information loss.

Use when: Converting between equivalent representations (e.g., Celsius ↔ Fahrenheit).

import "github.com/IBM/fp-go/v2/optics/iso"

celsiusToFahrenheit := iso.MakeIso(
    func(c float64) float64 { return c*9/5 + 32 },
    func(f float64) float64 { return (f - 32) * 5 / 9 },
)
❓ Optional - Maybe Values

Focus on a value that may or may not exist.

Use when: Working with nullable fields or values that may be absent.

import "github.com/IBM/fp-go/v2/optics/optional"

timeoutOptional := optional.MakeOptional(
    func(c Config) option.Option[*int] {
        return option.FromNillable(c.Timeout)
    },
    func(c Config, t *int) Config {
        c.Timeout = t
        return c
    },
)
πŸ”’ Traversal - Multiple Values

Focus on multiple values simultaneously, allowing batch operations.

Use when: Working with collections or updating multiple fields at once.

import (
    "github.com/IBM/fp-go/v2/optics/traversal"
    TA "github.com/IBM/fp-go/v2/optics/traversal/array"
)

numbers := []int{1, 2, 3, 4, 5}

// Double all elements
doubled := F.Pipe2(
    numbers,
    TA.Traversal[int](),
    traversal.Modify[[]int, int](N.Mul(2)),
)
// Result: [2, 4, 6, 8, 10]

πŸ”— Composition

The real power of optics comes from composition:

type Company struct {
    Name    string
    Address Address
}

type Address struct {
    Street string
    City   string
}

// Individual lenses
addressLens := lens.MakeLens(
    func(c Company) Address { return c.Address },
    func(c Company, a Address) Company {
        c.Address = a
        return c
    },
)

cityLens := lens.MakeLens(
    func(a Address) string { return a.City },
    func(a Address, city string) Address {
        a.City = city
        return a
    },
)

// Compose to access city directly from company
companyCityLens := F.Pipe1(
    addressLens,
    lens.Compose[Company](cityLens),
)

company := Company{
    Name: "Acme Corp",
    Address: Address{Street: "Main St", City: "NYC"},
}

city := companyCityLens.Get(company)           // "NYC"
updated := companyCityLens.Set("Boston")(company)

βš™οΈ Auto-Generation with go generate

Lenses can be automatically generated using the fp-go CLI tool and a simple annotation. This eliminates boilerplate and ensures consistency.

πŸ“ How to Use
  1. Annotate your struct with the fp-go:Lens comment:
//go:generate go run github.com/IBM/fp-go/v2/main.go lens --dir . --filename gen_lens.go

// fp-go:Lens
type Person struct {
    Name  string
    Age   int
    Email string
    Phone *string  // Optional field
}
  1. Run go generate:
go generate ./...
  1. Use the generated lenses:
// Generated code creates PersonLenses, PersonRefLenses, and PersonPrisms
lenses := MakePersonLenses()

person := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}

// Use the generated lenses
updatedPerson := lenses.Age.Set(31)(person)
name := lenses.Name.Get(person)

// Optional lenses for zero-value handling
personWithEmail := lenses.EmailO.Set(option.Some("new@example.com"))(person)
🎁 What Gets Generated

For each annotated struct, the generator creates:

  • StructNameLenses: Lenses for value types with optional variants (LensO) for comparable fields
  • StructNameRefLenses: Lenses for pointer types with prisms for constructing values
  • StructNamePrisms: Prisms for all fields, useful for partial construction
  • Constructor functions: MakeStructNameLenses(), MakeStructNameRefLenses(), MakeStructNamePrisms()

The generator supports:

  • βœ… Generic types with type parameters
  • βœ… Embedded structs (fields are promoted)
  • βœ… Optional fields (pointers and omitempty tags)
  • βœ… Custom package imports

See samples/lens for complete examples.

πŸ“Š Optics Hierarchy

[Iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)[S, A]
    ↓
[Lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)[S, A]
    ↓
[Optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)[S, A]
    ↓
[Traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)[S, A]

[Prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)[S, A]
    ↓
[Optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)[S, A]
    ↓
[Traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)[S, A]

More specific optics can be converted to more general ones.

πŸ“¦ Package Structure

Each package includes specialized sub-packages for common patterns:

  • array: Optics for arrays/slices
  • either: Optics for Either types
  • option: Optics for Option types
  • record: Optics for maps

πŸ“š Documentation

For detailed documentation on each optic type, see:

🌐 Further Reading

Haskell Lens Library

The concepts in this library are inspired by the powerful Haskell lens library, which pioneered many of these abstractions.

Articles and Resources
Why Functional Optics?

Functional optics solve real problems in software development:

  • Nested Updates: Eliminate deeply nested field access patterns
  • Immutability: Make working with immutable data practical and ergonomic
  • Abstraction: Separate data access patterns from business logic
  • Composition: Build complex operations from simple, reusable pieces
  • Type Safety: Catch errors at compile time rather than runtime

πŸ’‘ Examples

See the samples/lens directory for complete working examples, including:

  • Basic lens usage
  • Lens composition
  • Auto-generated lenses
  • Prism usage for sum types
  • Integration with monadic operations

πŸ“„ License

Apache License 2.0 - See LICENSE file for details.

Documentation ΒΆ

Overview ΒΆ

Package optics provides functional optics for composable data access and manipulation.

Overview ΒΆ

Optics are first-class, composable references to parts of data structures. They provide a uniform interface for reading, writing, and transforming nested immutable data without verbose boilerplate code.

The optics package family includes several types of optics, each suited for different data structure patterns:

  • Lens: Focus on a field within a product type (struct)
  • Prism: Focus on a variant within a sum type (union/Either)
  • Iso: Bidirectional transformation between equivalent types
  • Optional: Focus on a value that may not exist
  • Traversal: Focus on multiple values simultaneously

Why Optics? ΒΆ

Working with deeply nested immutable data structures in Go can be verbose:

// Without optics - updating nested data
updated := Person{
	Name: person.Name,
	Age:  person.Age,
	Address: Address{
		Street: person.Address.Street,
		City:   "New York", // Only this changed!
		Zip:    person.Address.Zip,
	},
}

With optics, this becomes:

// With optics - clean and composable
updated := cityLens.Set("New York")(person)

Core Optics Types ΒΆ

## Lens - Product Types (Structs)

A Lens focuses on a single field within a struct. It provides get and set operations that maintain immutability.

type Person struct {
	Name string
	Age  int
}

nameLens := lens.MakeLens(
	func(p Person) string { return p.Name },
	func(p Person, name string) Person {
		p.Name = name
		return p
	},
)

person := Person{Name: "Alice", Age: 30}
name := nameLens.Get(person)           // "Alice"
updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}

**Use lenses when:**

  • Working with struct fields
  • The field always exists
  • You need both read and write access

## Prism - Sum Types (Variants)

A Prism focuses on one variant of a sum type. It provides optional get (the variant may not match) and definite set operations.

type Result interface{ isResult() }
type Success struct{ Value int }
type Failure struct{ Error string }

successPrism := prism.MakePrism(
	func(r Result) option.Option[int] {
		if s, ok := r.(Success); ok {
			return option.Some(s.Value)
		}
		return option.None[int]()
	},
	func(v int) Result { return Success{Value: v} },
)

result := Success{Value: 42}
value := successPrism.GetOption(result) // Some(42)

**Use prisms when:**

  • Working with sum types (Either, Result, etc.)
  • The value may not be the expected variant
  • You need to match on specific cases

## Iso - Isomorphisms

An Iso represents a bidirectional transformation between two equivalent types with no information loss.

celsiusToFahrenheit := iso.MakeIso(
	func(c float64) float64 { return c*9/5 + 32 },
	func(f float64) float64 { return (f - 32) * 5 / 9 },
)

fahrenheit := celsiusToFahrenheit.Get(20.0)        // 68.0
celsius := celsiusToFahrenheit.ReverseGet(68.0)    // 20.0

**Use isos when:**

  • Converting between equivalent representations
  • Wrapping/unwrapping newtypes
  • Encoding/decoding data

## Optional - Maybe Values

An Optional focuses on a value that may or may not exist, similar to Option[A].

**Use optionals when:**

  • Working with nullable fields
  • The value may be absent
  • You need to handle the None case

## Traversal - Multiple Values

A Traversal focuses on multiple values simultaneously, allowing batch operations.

**Use traversals when:**

  • Working with collections
  • Updating multiple fields at once
  • Applying transformations to all matching elements

Composition ΒΆ

The real power of optics comes from composition. Optics of the same or compatible types can be composed to create more complex accessors:

type Company struct {
	Name    string
	Address Address
}

type Address struct {
	Street string
	City   string
}

// Individual lenses
addressLens := lens.MakeLens(
	func(c Company) Address { return c.Address },
	func(c Company, a Address) Company {
		c.Address = a
		return c
	},
)

cityLens := lens.MakeLens(
	func(a Address) string { return a.City },
	func(a Address, city string) Address {
		a.City = city
		return a
	},
)

// Compose to access city directly from company
companyCityLens := F.Pipe1(
	addressLens,
	lens.Compose[Company](cityLens),
)

company := Company{
	Name: "Acme Corp",
	Address: Address{Street: "Main St", City: "NYC"},
}

city := companyCityLens.Get(company)           // "NYC"
updated := companyCityLens.Set("Boston")(company)

Optics Hierarchy ΒΆ

Optics form a hierarchy where more specific optics can be converted to more general ones:

Iso[S, A]
    ↓
Lens[S, A]
    ↓
Optional[S, A]
    ↓
Traversal[S, A]

Prism[S, A]
    ↓
Optional[S, A]
    ↓
Traversal[S, A]

This means:

  • Every Iso is a Lens
  • Every Lens is an Optional
  • Every Prism is an Optional
  • Every Optional is a Traversal

Laws ΒΆ

Each optic type must satisfy specific laws to ensure correct behavior:

**Lens Laws:**

  1. GetSet: lens.Set(lens.Get(s))(s) == s
  2. SetGet: lens.Get(lens.Set(a)(s)) == a
  3. SetSet: lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)

**Prism Laws:**

  1. GetOptionReverseGet: prism.GetOption(prism.ReverseGet(a)) == Some(a)
  2. ReverseGetGetOption: if GetOption(s) == Some(a), then ReverseGet(a) == s

**Iso Laws:**

  1. RoundTrip1: iso.ReverseGet(iso.Get(s)) == s
  2. RoundTrip2: iso.Get(iso.ReverseGet(a)) == a

Real-World Example: Configuration Management ΒΆ

type DatabaseConfig struct {
	Host     string
	Port     int
	Username string
	Password string
}

type CacheConfig struct {
	TTL     int
	MaxSize int
}

type AppConfig struct {
	Database *DatabaseConfig
	Cache    *CacheConfig
	Debug    bool
}

// Create lenses for nested access
dbLens := lens.FromNillable(lens.MakeLens(
	func(c AppConfig) *DatabaseConfig { return c.Database },
	func(c AppConfig, db *DatabaseConfig) AppConfig {
		c.Database = db
		return c
	},
))

dbHostLens := lens.MakeLensRef(
	func(db *DatabaseConfig) string { return db.Host },
	func(db *DatabaseConfig, host string) *DatabaseConfig {
		db.Host = host
		return db
	},
)

defaultDB := &DatabaseConfig{
	Host:     "localhost",
	Port:     5432,
	Username: "admin",
	Password: "",
}

// Compose to access database host from app config
appDbHostLens := F.Pipe1(
	dbLens,
	lens.ComposeOption[AppConfig, string](defaultDB)(dbHostLens),
)

config := AppConfig{Database: nil, Debug: true}

// Get returns None when database is not configured
host := appDbHostLens.Get(config) // None[string]

// Set creates database with default values
updated := appDbHostLens.Set(option.Some("prod.example.com"))(config)
// updated.Database.Host == "prod.example.com"
// updated.Database.Port == 5432 (from default)

Package Structure ΒΆ

The optics package is organized into subpackages:

  • optics/lens: Lenses for product types
  • optics/prism: Prisms for sum types
  • optics/iso: Isomorphisms for equivalent types
  • optics/optional: Optional optics for maybe values
  • optics/traversal: Traversals for multiple values

Each subpackage may have additional specialized subpackages for common patterns:

  • array: Optics for array/slice operations
  • either: Optics for Either types
  • option: Optics for Option types
  • record: Optics for record/map types

Performance Considerations ΒΆ

Optics are designed to be efficient:

  • No reflection - all operations are type-safe at compile time
  • Minimal allocations - optics themselves are lightweight
  • Composition is efficient - creates function closures
  • Immutability ensures thread safety

For performance-critical code:

  • Cache composed optics rather than recomposing
  • Use pointer-based lenses (MakeLensRef) for large structs
  • Consider batch operations with traversals

Type Safety ΒΆ

All optics are fully type-safe:

  • Compile-time type checking
  • No runtime type assertions
  • Generic type parameters ensure correctness
  • Composition maintains type relationships

Getting Started ΒΆ

1. Choose the right optic for your data structure 2. Create basic optics for your types 3. Compose optics for nested access 4. Use Modify for transformations 5. Leverage the optics hierarchy when needed

Further Reading ΒΆ

For detailed documentation on each optic type, see:

  • github.com/IBM/fp-go/v2/optics/lens
  • github.com/IBM/fp-go/v2/optics/prism
  • github.com/IBM/fp-go/v2/optics/iso
  • github.com/IBM/fp-go/v2/optics/optional
  • github.com/IBM/fp-go/v2/optics/traversal

For related functional programming concepts:

  • github.com/IBM/fp-go/v2/option: Optional values
  • github.com/IBM/fp-go/v2/either: Sum types
  • github.com/IBM/fp-go/v2/function: Function composition

Directories ΒΆ

Path Synopsis
iso
Package iso provides isomorphisms - bidirectional transformations between types without loss of information.
Package iso provides isomorphisms - bidirectional transformations between types without loss of information.
lens
Package lens provides conversions from isomorphisms to lenses.
Package lens provides conversions from isomorphisms to lenses.
option
Package option provides isomorphisms for working with Option types.
Package option provides isomorphisms for working with Option types.
Package lens provides functional optics for zooming into and modifying nested data structures.
Package lens provides functional optics for zooming into and modifying nested data structures.
iso
Package iso provides utilities for composing lenses with isomorphisms.
Package iso provides utilities for composing lenses with isomorphisms.
option
Package option provides utilities for working with lenses that focus on optional values.
Package option provides utilities for working with lenses that focus on optional values.
Package optional provides optional optics for focusing on values that may not exist.
Package optional provides optional optics for focusing on values that may not exist.
Package prism provides prisms - optics for focusing on variants within sum types.
Package prism provides prisms - optics for focusing on variants within sum types.
Package traversal provides traversals - optics for focusing on multiple values simultaneously.
Package traversal provides traversals - optics for focusing on multiple values simultaneously.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL