Started: April 16, 2026
Finished: April 17, 2026
Released: April 17, 2026
Last Revision: April 18, 2026
grist_lens is a library I started working on for my game engine named gristmill.
This library improves the ergonomic design of the existing libraries by leveraging a proc macro in a clean and concise way.
I felt like the ergonomics of existing APIs like pl-lens or lens-rs
were too clunky and obtuse to what was actually happening in the backend.
So I decided to make my own lens. Along with this I will share my reasoning for
why I don’t like the existing solutions.
Lenses in rust essentially boil down to more advanced field borrows. This allows users of lenses to create more advanced types that have partial borrows of a larger struct, or borrow the struct and still call methods on the struct that borrow other parts.
Here are some examples that illustrate further what lenses are. Note: this code is only meant as pseudo-code to educate what lens accomplish, not to provide compiling code.
struct Foo {
x: i32,
y: f32
}
impl Foo {
fn increment_y(&mut self) {
self.y += 1.0;
}
}
let mut foo = Foo { x: 0, y: 1.0 };
let borrow_x = &mut foo.x
foo.increment_y(); // this is invalid in rust
// using borrow_x down here ..
This would be invalid in normal rust as the borrow checker doesn’t realize that increment_y only borrows y and not x.
But lenses would solve this as it would split the borrows up so the borrow checker is satisfied.
Same pseudo-code using lenses instead
struct Foo {
x: i32,
y: f32
}
impl Foo {
fn increment_y(self: &Optics<'_, Foo>) {
self.y += 1.0;
}
}
let mut foo = Foo { x: 0, y: 1.0 };
let mut foo_optic = Optics::new(&mut foo); // Creating an optic that creates lenses
let borrow_x = &mut foo_optic.x;
foo_optic.increment_y(); // this is now valid to rust
// using borrow_x down here ..
This example shows how lenses would allow double mutable borrowing
struct Foo {
x: i32,
y: f32
}
struct Bar<'a> {
x: &'a mut i32
}
impl Foo {
fn increment_y(&mut self) {
self.y += 1.0;
}
}
impl<'a> Bar<'a> {
fn increment_x(&mut self) {
*self.x += 1;
}
}
let mut foo = Foo { x: 0, y: 1.0 };
let borrowed_x = Bar { x: &mut foo.x } // Borrowing x
foo.increment_y(); // This is invalid in rust again..
foo.increment_x();
But yet again we can solve this with lenses:
struct Foo {
x: i32,
y: f32
}
struct Bar<'a> {
x: &'a mut i32
}
impl Foo {
fn increment_y(self: &Optics<'_, Foo>) {
self.y += 1;
}
}
impl<'a> Bar<'a> {
fn increment_x(&mut self) {
*self.x += 1;
}
}
let mut foo = Foo { x: 0, y: 1.0 };
let mut foo_optic = Optics::new(&mut foo);
let borrowed_x = Bar { x: &mut foo_optic.x } // Borrowing x
foo_optic.increment_y(); // This would be valid with lenses
foo_optic.increment_x();
Lenses are a tactic to take a smaller part of a larger structure without taking the entire structure.
If you wish to learn more about optics, and lenses look here.
lens-rs?From the lens-rs documentation it described like this1:
“lens[es] implemented in rust
- the
Reviewoptics describes how to construct a single value.-- A
Traversalcan access [..] multiple substructures.-- A
Prismcan access the substructure [that] may exist.-- A
Lenscan access the substructure [that] must exist.”
Usage Example1:
use lens_rs::*;
fn test() -> Option<()> {
let mut nested: Result<Result<_, ()>, ()> = Review::review(optics!(Ok.Ok), (1,2));
*x.preview_mut(optics!(Ok.Ok._0))? += 1;
assert_eq!(nested.preview(optics!(Ok.Ok._0))?, 2);
let mut x = (1, (2, (3, 4)));
*x.view_mut(optics!(_1._1._1)) *= 2;
assert_eq!(x.view(optics!(_1._1._1)), 8);
let mut x: (_, Result<_, ()>) = (1, Ok((2, 3)));
*x.preview_mut(optics!(_1.Ok._1))? *= 2;
assert_eq!(x.preview(optics!(_1.Ok._1))?, 6);
let mut x = (1, vec![Some((2, 3)), None]);
x
.traverse_mut(optics!(_1._mapped.Some._0))
.into_iter()
.for_each(|i| *i += 1);
assert_eq!(x.traverse(optics!(_1._mapped.Some._0)), vec![3]);
Some(())
}
Creating your own types is like so1:
#[derive(Lens, Debug)]
struct Foo {
#[optic] a: i32,
#[optic] b: i32,
}
With example 1, I find this library despite it being the largest library for optics, to be unparsable. The syntax and usage is overly verbose, and again it relies heavily on the usage of macros.
I solve this in my library by avoiding the usage of macros and instead work with the type system directly.
With example 2, I find that marking a type to be a lens is too overly verbose and unnecessary. It is unneeded to have each field have #[optic].
I solve this by using a smarter proc-macro.
pl-lens?pl-lens is a library designed to handle the challenge of lenses in rust only.
pl_lens example For anyone unfamiliar with pl-lens this library here is a quick example of how it’s used2:
use pl_lens::Lenses;
use pl_lens::lens;
use pl_lens::{Lens, RefLens};
#[derive(Lenses)]
struct Address {
street: String,
city: String,
postcode: String
}
#[derive(Lenses)]
struct Person {
name: String,
age: u8,
address: Address
}
let p0 = Person {
name: "Pop Zeus".to_string(),
age: 58,
address: Address {
street: "123 Needmore Rd".to_string(),
city: "Dayton".to_string(),
postcode: "99999".to_string()
}
};
assert_eq!(lens!(Person.name).get_ref(&p0), "Pop Zeus");
assert_eq!(lens!(Person.address.street).get_ref(&p0), "123 Needmore Rd");
let p1 = lens!(Person.address.street).set(p0, "666 Titus Ave".to_string());
assert_eq!(lens!(Person.name).get_ref(&p1), "Pop Zeus");
assert_eq!(lens!(Person.address.street).get_ref(&p1), "666 Titus Ave");
There are many things I dislike that is happening here, I will list them out then break them down each individually:
lens! macro.get_ref.setThe lens! macro existing is providing a clunkier API to the underlying mechanisms.
This clunkier API is a byproduct of the LensRef trait and the Lens type in general. The lens! macro is an alias for another macro
called compose_lens! which isn’t immediately obvious. This is extremely obtuse way to do lenses, and feels bad in actual code.
I solve this problem by avoiding macros once again.
Along with this there is the LensRef and Lens traits which a confusing mess of an API. Lens provides the set method3 but LensRef provides get_ref4, mutate_with_fn, and modify. But there is also ValueLens which provides get5.
So what do all of these traits actually do? Lens is described as ”[..] a purely functional means to access and/or modify a field that is nested in an immutable data structure.”3. Despite it saying “means to access” there is no actual way to access the value as far as I am aware.
Then there is RefLens which is described as “[a] lens that allows the target to be accessed and mutated by reference.”4. This is actually accurate to it’s description as it provides the get_ref method, and the modify and mutate_with_fn methods. These methods allow you to get and mutate the lens value which is to the credit of this trait.
There is also ValueLens which is described as “[a] lens that allows the target to be accessed only by cloning or copying the target value.”5. I would describe this as a misnomer as it’s not a value, only a clone or copy. Other than that this description lives up to it’s namesake as under the hood it does clone or copy6.
I solve this confusing API by not using too many traits overall and having two main traits named LensRef, and LensMut, which provide reference access and mutable reference access respectively.
The last grievance I have is with the up-to-dateness of the crate. The crate has significant age and hasn’t been updated all that much and required me to use #[allow(semicolon_in_expressions_from_macros)] to allow it to compile.
I solve this by having my crate use the latest edition and tested with the most recent version of rust.
grist_lensgrist_lens example Example #17
use grist_lens::{Optic, lens, Usage, Field, Lens};
#[lens]
struct Foo { x: i32, y: u32 };
let mut object = Foo { x: 10, y: 20 };
let object_optic = Optic::new(&mut object);
let x_field = object_optic.get::<Foo_x>();
let y_field = object_optic.get_mut::<Foo_y>();
My approach to this problem is to have one macro handle implementation of the traits associated with usage of the lens. Then leverage another type to use these traits and track the lifetimes of these objects to prevent UB8.
Prisms, or Traversals.
Prisms don’t end up all that useful in rust.RefCell making it count borrows at runtime.