| Crates.io | optics |
| lib.rs | optics |
| version | 0.3.0 |
| created_at | 2025-05-13 22:30:33.455237+00 |
| updated_at | 2025-06-05 20:14:32.266055+00 |
| description | A no_std-compatible optics library providing composable lenses, prisms, isomorphisms, and fallible isomorphisms. |
| homepage | |
| repository | https://github.com/axos88/optics-rs |
| max_upload_size | |
| id | 1672615 |
| size | 202,609 |
optics is a set of composable, type-safe tools for accessing, transforming, and navigating data structures. It allows to separate navigation and performing operations on the data structures. Using optics a function is even able to perform a operations on otherwise opaque data structures.
It takes inspiration from the optics concepts you'd find in functional languages like Haskell β but itβs not an implementatiton following strict type theory or Van Laarhoven/profunctor lenses, but it tries to mimic similar functionality within the constraints of Rustβs type system without higher-kinded types.
The goal was simple:
π Build something useful and composable for everyday Rust projects β no magic.
This is a pre-release, and the code is unfinished β but itβs good enough to start experimenting with in real projects.
Thereβs a lot of room for simplification and improvement and I wouldnβt mind help and ideas to do so.
No dependencies β pure Rust, no external crates except for testing
no_std support β usable in embedded and other restricted environments
Type-safe, explicit interfaces
This is a layman's implementation of optics. I donβt fully grasp all the deep type theory behind profunctor optics or Van Laarhoven lenses. Instead, I built something practical and composable, within the limitations of Rustβs type system and my own understanding.
All optic implementations implement a set of base traits that define the operations they can perform. Currently three base operations are defined: (HasSetter, HasGetter, HasReverseGet). This will likely need to be extended in the future to make it possible to add a Traversal for example.
When certain conditions are met, specific marker traits are implemented for other base operations, such as HasTotalGetter (if the HasGetter error is Infallible), HasOver (if the optic is at least a prism), or HasTotalReverseGet (if the HasReverseGet error is Infallible.
Concrete structs of implementations of the optics are private, and interaction with optics is only allowed when wrapped in an exposed Impl struct (constructor functions returning Impl are exposed). This can be used to combine optics or to downgrade an optic, such as a Lens into a Getter, if the desired behaviour is to restrict the optic to only allow reading data.
Optics - even if they are of different types can be combined. The rule of thumb is that the combination of two optics X<S, I> and Y<I, A> will result in the most advanced optic type that requires a set of base traits that both components implement:
PartialGetter |
Getter | Prism | Lens | Iso | FallibleIso |
Setter | |
|---|---|---|---|---|---|---|---|
PartialGetter |
PartialGetter |
PartialGetter |
PartialGetter |
PartialGetter |
PartialGetter |
PartialGetter |
- |
| Getter | PartialGetter |
Getter | PartialGetter |
Getter | Getter | PartialGetter |
- |
| Prism | PartialGetter |
PartialGetter |
Prism | Prism | Prism | Prism | - |
| Lens | PartialGetter |
Getter | Prism | Lens | Lens | Prism | - |
| Iso | PartialGetter |
Getter | Prism | Lens | Iso | FallibleIso |
- |
FallibleIso |
PartialGetter |
PartialGetter |
Prism | Prism | FallibleIso |
FallibleIso |
- |
| Setter | - | - | Setter | Setter | Setter | Setter | - |
PartialGetter] - for fallible read-only access to dataGetter] - for read-only access to dataSetter] - for write-only access to dataPrism] β Prisms in general allow for focusing on a specific variant of a sum type (like enums in Rust).
They can be used to extract or modify the value of that variant, or a focusing operation that may fail because the
value that may or may not be present, such as Option<u32> -> u32Lens] β Lenses focus on a part of a structure and provide a way to get and set the value of that part, such as Point -> x: u32Iso]morphisms β Isos provide a bijective mapping between types. They can be used to transform data between two types while preserving structure, such as an IpAddrV4 <=> u32, or a CartesianPoint and PolarPointFallibleIso]morphisms β Fallible isos extend the concept of isos by introducing the possibility of failure.
Both the getting and setting operations may fail, and they return Result types that allow you to handle errors.
This can be used for parsing or validating data, such as converting a String to an u16.The crate was designed in a way that allows for easy extensibility. Both in terms of adding new optic types (w/ base traits), or adding new implementations of existing optics, such as a lens that can handle Options of any type.
This section outlines the structural conventions and design patterns employed in the optics crate. Adhering to these guidelines ensures consistency, maintainability, and extensibility across the crate's codebase, and is to some extent enforced by tests.
Each optic type (e.g., Lens, Prism, Iso) is encapsulated within its own module. These modules are not directly exposed to downstream users. The organization within each module is as follows:
A marker trait is defined to represent the optic type. This trait defines as supertraits the necessary base optic traits required for its functionality. For example, a Prism marker trait would extend HasPartialGetter and HasSetter. The marker trait has a blanket implementation to be automatically implement for all structs that implement the required base traits.
An Impl struct (e.g., LensImpl) serves as the public interface for the optic type. The semantics of the Impl wrapper is "a container of optics that currently acts as a ..." This struct wraps the concrete implementation opaquely and is directly returned by the crate's API. Utilizing a concrete struct also allows to use correct combining function signatures and allow downgrading an optic to an inferior type (lens to prism, iso to getter).
The Impl struct is responsible for:
combine_with_xxx functions to compose optics, returning an Impl of the resulting optic type.as_xxx functions returning an Impl of an inferior optic type (e.g., from Lens to Prism or Getter).A composed.rs file within each module contains implementations that compose two optics to form the current optic type. For instance, a ComposedPrism might combine a Lens and a FallibleIso. In some cases errors need to be wrapped either automatically if they implement Into<>, or by mapping functions.
The module is entirely private to the crate, only a constructor function new is exposed.
Though not strictly required, currently all optics provide an implementation using closures.
The modules are entirely private to the crate, only a constructor function new is exposed.
Other files within the module may provide alternative implementations of the optic type, such as concrete structs for specific higher-kinded types (HKTs) that cannot be expressed with closures alone, such as Some
These modules are also intended to be private, only exposing a constructor function returning an Impl struct. If you add new implementations either to the crate or to your own crate, please follow this guideline to avoid gotchas.
When introducing a new optic type or implementation:
By following these conventions, the optics crate maintains a consistent and extensible framework for optic types, promoting code reuse and reducing the potential for conflicts in trait implementations.
The table below shows which optics can act as another optic type.
Getter |
TotalGetter |
Prism |
Lens |
Iso |
FallibleIso |
Setter |
|
|---|---|---|---|---|---|---|---|
Getter |
β | ||||||
TotalGetter |
β | β | |||||
Prism |
β | β | β | ||||
Lens |
β | β | β | β | β | ||
Iso |
β | β | β | β | β | β | β |
FallibleIso |
β | β | β | β | |||
Setter |
β |
If you know your type theory, or even if you just have an eye for clean Rust APIs β Iβd love for you to take a look. Suggestions, critiques, and even teardown reviews are welcome. This is very much a learning-while-doing project for me.
Below is a simplified example of how the optics work in this crate. The code below illustrates how to use and combine the various optic types.
use optics::{LensImpl, FallibleIsoImpl, PrismImpl, mapped_lens, mapped_prism, mapped_fallible_iso, HasSetter, HasGetter};
#[derive(Debug, Clone)]
struct HttpConfig {
bind_address: Option<String>,
workers: usize,
}
#[derive(Debug, Clone)]
struct AppConfig {
http: HttpConfig,
name: String,
}
fn example() {
// Define lenses to focus on subfields
let http_lens = mapped_lens(
|app: &AppConfig| app.http.clone(),
|app, http| app.http = http,
);
let bind_address_prism = mapped_prism(
|http: &HttpConfig| http.bind_address.clone().ok_or(()),
|http, addr| http.bind_address = Some(addr),
);
let minimum_port = 1024;
// Define a fallible isomorphism between String and u16 (parsing a port)
let port_fallible_iso = mapped_fallible_iso(
|addr: &String| {
addr.rsplit(':')
.next()
.and_then(|port| port.parse::<u16>().ok()).ok_or(())
},
move |port: &u16| if *port > minimum_port { Ok(format!("0.0.0.0:{}", port)) } else { Err(()) }
);
// Compose lens and fallible iso into a ComposedFallibleIso
let http_bind_address_prism = http_lens.compose_with_prism(bind_address_prism);
let http_bind_address_port_prism = http_bind_address_prism.compose_with_fallible_iso::<(), _, _>(port_fallible_iso);
let mut config = AppConfig {
http: HttpConfig {
bind_address: Some("127.0.0.1:8080".to_string()),
workers: 4,
},
name: "my_app".into(),
};
// Use the composed optic to get the port
let port = http_bind_address_port_prism.try_get(&config).unwrap();
println!("Current port: {}", port); // 8080
// Use it to increment the port and update the config
http_bind_address_port_prism.set(&mut config, port + 1);
println!("Updated bind address: {:?}", config.http.bind_address); // port is now 8081
}
example();
While the code was written with care, parts of the documentation and some of the tests are AI generated, especially the code quality tests.