| name | fp-go-lens |
| description | Use this skill when working with lenses and optics in Go using the fp-go library (github.com/IBM/fp-go/v2/optics/lens). Trigger on mentions of lenses, optics, MakeLens, MakeLensRef, MakeLensStrict, lens composition, immutable updates to nested structs, accessing nested data structures, Compose, ComposeRef, ComposeOption, FromNillable, FromNillableRef, Modify, getter/setter patterns, or functional updates to Go structs. Also trigger when the user needs to update deeply nested fields immutably or work with optional fields in struct hierarchies. Also trigger for `// fp-go:Lens` annotation or go generate for lens code generation. |
fp-go Lenses for Structs
Overview
Lenses are functional optics that provide immutable access to nested data structures. A Lens[S, A] focuses on a field A within a structure S, providing Get (read) and Set (immutable update) operations.
import (
L "github.com/IBM/fp-go/v2/optics/lens"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
Code Generation (Recommended)
The fastest way to create lenses is via code generation. Annotate any struct with // fp-go:Lens and run go generate ./....
type Person struct {
Name string
Age int
Phone *string
}
Running go generate ./... produces a gen_lens.go file with:
| Generated type | Description |
|---|
PersonLenses | Lenses for Person (value type) |
PersonRefLenses | Lenses for *Person (pointer type) |
PersonPrisms | Prisms for Person |
PersonRefPrisms | Prisms for *Person |
Each type has fields for every struct field, with both a required lens and an optional lens:
type PersonLenses struct {
Name L.Lens[Person, string]
NameO LO.LensO[Person, string]
Age L.Lens[Person, int]
AgeO LO.LensO[Person, int]
Phone L.Lens[Person, *string]
PhoneO LO.LensO[Person, *string]
}
Constructor functions:
lenses := MakePersonLenses()
refLenses := MakePersonRefLenses()
person := Person{Name: "Alice", Age: 30}
updated := lenses.Name.Set("Bob")(person)
name := lenses.Name.Get(person)
Pointer fields (*string, *SomeStruct) automatically generate optional lenses using LO.FromNillable.
Embedded structs and generic types are also supported — for embedded structs, lenses are generated for each promoted field.
Manual Lens Creation
Use manual creation when code generation is not suitable or for one-off lenses.
MakeLens — Value Types
Use MakeLens for structs passed by value. The setter receives a copy automatically.
type Person struct {
Name string
Age int
}
nameLens := L.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}
updated := nameLens.Set("Bob")(person)
name := nameLens.Get(person)
Method expressions also work (receiver becomes first argument):
func (p Person) GetName() string { return p.Name }
func (p Person) SetName(name string) Person { p.Name = name; return p }
nameLens := L.MakeLens(Person.GetName, Person.SetName)
Setter signature: func(S, A) S — struct first, value second.
MakeLensRef — Pointer Types
Use MakeLensRef for structs passed by pointer. The framework handles copying automatically — your setter modifies the pointer directly.
type Address struct {
Street string
City string
}
streetLens := L.MakeLensRef(
func(a *Address) string { return a.Street },
func(a *Address, s string) *Address { a.Street = s; return a },
)
addr := &Address{Street: "Main St", City: "Boston"}
updated := streetLens.Set("Oak Ave")(addr)
Setter signature: func(*S, A) *S — pointer first, value second. No manual copy needed.
MakeLensStrict — Pointer Types with Comparable Fields (Optimization)
Use MakeLensStrict for pointer structs when the field type is comparable (string, int, pointers, etc.). If the new value equals the current value, the original pointer is returned unchanged (no allocation).
nameLens := L.MakeLensStrict(
func(p *Person) string { return p.Name },
func(p *Person, name string) *Person { p.Name = name; return p },
)
same := nameLens.Set("Alice")(person)
updated := nameLens.Set("Bob")(person)
For non-comparable types use MakeLensWithEq with a custom Eq[A].
MakeLensWithEq — Pointer Types with Custom Equality
Use MakeLensWithEq when the field type is not comparable (slices, maps, structs containing those). Provide an Eq[A] to determine equality; if values are equal, the original pointer is returned unchanged without copying.
import A "github.com/IBM/fp-go/v2/array"
tagsLens := L.MakeLensWithEq(
A.StrictEquals[string](),
func(p *Person) []string { return p.Tags },
func(p *Person, t []string) *Person { p.Tags = t; return p },
)
For custom element equality (e.g. case-insensitive strings or struct slices), use EQ.FromEquals(func(a, b []T) bool { ... }) instead. For comparable types, EQ.FromStrictEquals[T]() is equivalent to using MakeLensStrict.
Comparison
| Constructor | Source type | Copy responsibility | Best for |
|---|
MakeLens | S (value) | Automatic (value copy) | Structs by value |
MakeLensRef | *S (pointer) | Automatic (framework) | Structs by pointer |
MakeLensStrict | *S (pointer) | Automatic + equality skip | Comparable fields, performance-sensitive |
MakeLensWithEq | *S (pointer) | Automatic + custom Eq | Non-comparable fields with equality |
Composing Lenses
Compose — Value Outer Structure
Compose[S](ab)(sa) combines a Lens[S, A] and a Lens[A, B] into a Lens[S, B].
type Street struct { Name string }
type Address struct { Street Street }
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
streetNameLens := L.MakeLens(
func(a Address) string { return a.Street.Name },
func(a Address, n string) Address { a.Street.Name = n; return a },
)
personStreetNameLens := F.Pipe1(addressLens, L.Compose[Person](streetNameLens))
updated := personStreetNameLens.Set("Oak Ave")(person)
ComposeRef — Pointer Outer Structure
ComposeRef[S](ab)(sa) is the pointer version — use when the outer lens is Lens[*S, A].
personStreetNameLens := F.Pipe1(addressRefLens, L.ComposeRef[Person](streetNameLens))
Working with Optional Fields
Optional field lenses have type LensO[S, A] (alias for Lens[S, Option[A]]). Get returns Option[A]; Set takes Option[A].
All functions below are in the optics/lens/option package (imported as LO).
FromNillable — Pointer Field to Option
LO.FromNillable converts a Lens[S, *A] to a LensO[S, *A]. Get returns None when the pointer is nil.
type Company struct {
Name string
Address *Address
}
addressPtrLens := L.MakeLens(
func(c Company) *Address { return c.Address },
func(c Company, a *Address) Company { c.Address = a; return c },
)
optAddressLens := LO.FromNillable(addressPtrLens)
company := Company{Name: "Acme"}
result := optAddressLens.Get(company)
withAddr := optAddressLens.Set(O.Some(&Address{City: "Boston"}))(company)
cleared := optAddressLens.Set(O.None[*Address]())(withAddr)
For pointer outer structs use LO.FromNillableRef (Lens[*S, *A] → LensO[*S, *A]).
ComposeOption — Optional Container, Required Field
LO.ComposeOption[S, B](defaultA)(ab) composes a LensO[S, A] (optional container) with a Lens[A, B] (required field) into a LensO[S, B].
- Get: returns
None[B] when A is absent
- Set(Some[B]): updates B in A, creating A from
defaultA if absent
- Set(None[B]): removes A entirely
type Config struct { Database *Database }
dbLens := LO.FromNillable(L.MakeLens(
func(c Config) *Database { return c.Database },
func(c Config, db *Database) Config { c.Database = db; return c },
))
portLens := L.MakeLensRef(
func(db *Database) int { return db.Port },
func(db *Database, p int) *Database { db.Port = p; return db },
)
defaultDB := &Database{Host: "localhost", Port: 5432}
configPortLens := F.Pipe1(dbLens, LO.ComposeOption[Config, int](defaultDB)(portLens))
config := Config{}
port := configPortLens.Get(config)
updated := configPortLens.Set(O.Some(3306))(config)
Compose (option package) — Optional Container, Optional Field
LO.Compose[S, B](defaultA)(ab) is like ComposeOption but ab is a LensO[A, B] (the inner field is also optional).
settingsLens := LO.FromNillable(settingsPtrLens)
retriesLens := LO.FromNillable(retriesPtrLens)
defaultSettings := &Settings{}
configRetriesLens := F.Pipe1(settingsLens, LO.Compose[Config, *int](defaultSettings)(retriesLens))
Chaining Multiple Optional Levels
Use F.Pipe2 / F.Pipe3 to chain compose steps. Choose the compose function based on the type of the inner lens (ab):
| Function | ab type | Use when |
|---|
LO.ComposeOption | Lens[A, B] — required field | The inner field always exists once A is present |
LO.Compose | LensO[A, B] — optional field | The inner field is itself optional |
defaultAddress := &Address{}
defaultStreet := &Street{}
streetNameLens := L.MakeLensStrict(
func(s *Street) string { return s.Name },
func(s *Street, n string) *Street { s.Name = n; return s },
)
addressStreetLens := LO.FromNillableRef(L.MakeLensRef(
func(a *Address) *Street { return a.Street },
func(a *Address, s *Street) *Address { a.Street = s; return a },
))
personAddressLens := LO.FromNillable(L.MakeLens(
func(p Person) *Address { return p.Address },
func(p Person, a *Address) Person { p.Address = a; return p },
))
streetNameInPerson := F.Pipe2(
personAddressLens,
LO.Compose[Person, *Street](defaultAddress)(addressStreetLens),
LO.ComposeOption[Person, string](defaultStreet)(streetNameLens),
)
Modifying Values
Modify applies a transformation function to the focused value:
type Counter struct { Value int }
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
counter := Counter{Value: 5}
incremented := valueLens.Modify(N.Add(1))(counter)
Or using the package-level function in pipelines:
incremented := F.Pipe1(valueLens, L.Modify[Counter](N.Add(1)))(counter)
Import Reference
import (
L "github.com/IBM/fp-go/v2/optics/lens"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
EQ "github.com/IBM/fp-go/v2/eq"
A "github.com/IBM/fp-go/v2/array"
N "github.com/IBM/fp-go/v2/number"
)
Complete Manual Example
package main
import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
O "github.com/IBM/fp-go/v2/option"
)
type Street struct {
Name string
}
type Address struct {
City string
Street *Street
}
type Person struct {
Name string
Age int
Address *Address
}
func main() {
streetNameLens := L.MakeLensStrict(
func(s *Street) string { return s.Name },
func(s *Street, n string) *Street { s.Name = n; return s },
)
addressStreetLens := LO.FromNillableRef(L.MakeLensRef(
func(a *Address) *Street { return a.Street },
func(a *Address, s *Street) *Address { a.Street = s; return a },
))
personAddressLens := LO.FromNillable(L.MakeLens(
func(p Person) *Address { return p.Address },
func(p Person, a *Address) Person { p.Address = a; return p },
))
defaultAddress := &Address{City: "Unknown"}
defaultStreet := &Street{}
streetNameInPerson := F.Pipe2(
personAddressLens,
LO.Compose[Person, *Street](defaultAddress)(addressStreetLens),
LO.ComposeOption[Person, string](defaultStreet)(streetNameLens),
)
person := Person{
Name: "Alice",
Age: 30,
Address: &Address{
City: "Boston",
Street: &Street{Name: "Main St"},
},
}
updated := streetNameInPerson.Set(O.Some("Oak Ave"))(person)
noAddr := Person{Name: "Bob"}
withAddr := streetNameInPerson.Set(O.Some("Elm St"))(noAddr)
}
Further Reading