[Pre-RFC] Declarative fields projection

[Pre-RFC] Keypaths in Rust

A lightweight, zero-cost abstraction library for safe, composable access to nested data structures in Rust. Inspired by Functional lenses and Swift's KeyPath system, this library provides type-safe keypaths for struct fields and enum variants for superior performance.

Inspired by Lenses: Compositional Data Access And Manipulation

The library is under development GitHub - codefonsi/rust-key-paths: ReadableKeyPath and WritableKeyPath for struct and enums in Rust

  • Feature Name: field_projection using keypaths
  • Start Date: 2025-09-06
  • RFC PR: rust-lang/rfcs#0000

:key: KeyPaths in Rust

Safe, composable way to access and modify nested data in Rust. Inspired by KeyPath and Functional Lenses system.

[dependencies]
rust-key-paths = "2.0.6"
key-paths-derive = "2.0.6"

Basic usage

use std::sync::Arc;
use key_paths_derive::Kp;

#[derive(Debug, Kp)]
struct SomeComplexStruct {
    scsf: Option<SomeOtherStruct>,
    scfs2: Arc<std::sync::RwLock<SomeOtherStruct>>,
}

#[derive(Debug, Kp)]
struct SomeOtherStruct {
    sosf: Option<OneMoreStruct>,
}

#[derive(Debug, Kp)]
enum SomeEnum {
    A(String),
    B(Box<DarkStruct>),
}

#[derive(Debug, Kp)]
struct OneMoreStruct {
    omsf: Option<String>,
    omse: Option<SomeEnum>,
}

#[derive(Debug, Kp)]
struct DarkStruct {
    dsf: Option<String>,
}

impl SomeComplexStruct {
    fn new() -> Self {
        Self {
            scsf: Some(SomeOtherStruct {
                sosf: Some(OneMoreStruct {
                    omsf: Some(String::from("no value for now")),
                    omse: Some(SomeEnum::B(Box::new(DarkStruct {
                        dsf: Some(String::from("dark field")),
                    }))),
                }),
            }),
            scfs2: Arc::new(std::sync::RwLock::new(SomeOtherStruct {
                sosf: Some(OneMoreStruct {
                    omsf: Some(String::from("no value for now")),
                    omse: Some(SomeEnum::B(Box::new(DarkStruct {
                        dsf: Some(String::from("dark field")),
                    }))),
                }),
            })),
        }
    }
}
fn main() {
    let mut instance = SomeComplexStruct::new();

    SomeComplexStruct::scsf()
        .then(SomeOtherStruct::sosf())
        .then(OneMoreStruct::omse())
        .then(SomeEnum::b())
        .then(DarkStruct::dsf())
        .get_mut(&mut instance).map(|x| {
        *x = String::from("πŸ––πŸΏπŸ––πŸΏπŸ––πŸΏπŸ––πŸΏ");
    });

    println!("instance = {:?}", instance.scsf.unwrap().sosf.unwrap().omse.unwrap());
    // output - instance = B(DarkStruct { dsf: Some("πŸ––πŸΏπŸ––πŸΏπŸ––πŸΏπŸ––πŸΏ") })
}

Composing keypaths

Chain through nested structures with then():

#[derive(Kp)]
struct Address { street: String }

#[derive(Kp)]
struct Person { address: Box<Address> }

let street_kp = Person::address().then(Address::street());
let street = street_kp.get(&person);  // Option<&String>

Partial and Any keypaths

Use #[derive(Pkp, Akp)] (requires Kp) to get type-erased keypath collections:

  • PKp – partial_kps() returns Vec<PKp<Self>>; value type erased, root known
  • AKp – any_kps() returns Vec<AKp>; both root and value type-erased for heterogeneous collections

Filter by value_type_id() / root_type_id() and read with get_as(). For writes, dispatch to the typed Kp (e.g. Person::name()) based on TypeId.

See examples: pkp_akp_filter_typeid, pkp_akp_read_write_convert.


Supported containers

The #[derive(Kp)] macro (from key-paths-derive) generates keypath accessors for these wrapper types:

Container Access Notes
Option<T> field() Unwraps to inner type
Box<T> field() Derefs to inner
Pin<T>, Pin<Box<T>> field(), field_inner() Container + inner (when T: Unpin)
Rc<T>, Arc<T> field() Derefs; mut when unique ref
Vec<T> field(), field_at(i) Container + index access
HashMap<K,V>, BTreeMap<K,V> field_at(k) Key-based access
HashSet<T>, BTreeSet<T> field() Container identity
VecDeque<T>, LinkedList<T>, BinaryHeap<T> field(), field_at(i) Index where applicable
Result<T,E> field() Unwraps Ok
Cow<'_, T> field() as_ref / to_mut
Option<Cow<'_, T>> field() Optional Cow unwrap
std::sync::Mutex<T>, std::sync::RwLock<T> field() Container (use LockKp for lock-through)
Arc<Mutex<T>>, Arc<RwLock<T>> field(), field_lock() Lock-through via LockKp
tokio::sync::Mutex, tokio::sync::RwLock field_async() Async lock-through (tokio feature)
parking_lot::Mutex, parking_lot::RwLock field(), field_lock() parking_lot feature

The macro-generated method names do not look good to me. In my experience of using Rust std, I didn't find many usage of abbreviations in names except some already-familiar names such as len or vec. The _fw, _r suffix reminds me of the horrible time when writing legacy C codes.

Is it possible to make those methods generic over return types instead of manually marking f, w or r, and let the compiler to infer it when possible? (I don't come up with a complete solution for now, but I wonder if it is feasible like what we do in iter.collect().)

4 Likes

This seems like an interesting project with an interesting idea, but I don't think this is at a stage yet where it is worth to think about including this in the standard library. Flesh it out more in the crate, see if people are interested in using the crate, and if give crate gets popular it may be worth thinking about. But before that, that seems extremely unlikely.

5 Likes

Magical name suffixes and prefixes... Good part about lenses in Haskell is that names are predictable. Always _ followed by a field or variant name. Here I see f with tuples, case with enums. Then there's r and w for what I'm assuming read/write, f for... field? forward?

1 Like

The problems and probable solutions

In order to remove boilerplate code _r, _w, _f below are the proposed solution and doubt.

Solution 1: Another probable solution is to take two functional component instead of one for each field one is for reading another one is for writing. but the solution may cause small performance downgrade. Also the solution may cause harm while composing two functional component read + read = good, write + read = bad performance due to reading from mutable ref, write + write = default performance downgrade.

The _r (readiblity) solution: 80 % deep struct are getting used for read only purposes so omitting _r with field name by default is a great idea.

The _w (writablity) solution: _w is needed for generating functional component that take take &mut root and return &mut field. This is required to allow mutablity for types that allow interior mutablity like Cell also for the struct instance that are let mut or mutable borrow. There _w is required. I have already tried replacing it with prc macro varient #[Writable] but again loosing read only compatibility as meta can either produce readable or writable component for one name else we need blilerplate _w.

The _f (failablity) Solution: The f char that is for failablity can be replaced with helper method to_optional(), to_result() in library,

Querry operator overloading: can we use prior art of rust programming language to replace to_option(),.to_result() with ? operator.

_f is for failablity that can cause due to Optoin -> None, Result -> Error, Lock -> poision

Noted with thanks.

Hi @pacak,

Just removed _case from enum keypath.

I think it is better suited to https://users.rust-lang.org/ as a new crate announcement.

3 Likes