Const-evaluated, zero-cost, very simple keypath functionality for rust.
Requires nightly and env RUSTFLAGS="-Znext-solver=globally"
to work and use correctly :)
Let's take this simply example of a struct that implements kathy::KeyPathIndexable
(the trait that makes this all work), and modify it via IndexMut
(which the Keyable
macro implements for you):
use kathy::Keyable;
use std::ops::IndexMut;
#[derive(Keyable)]
struct Person {
age: u16
};
fn main() {
std::hint::black_box(modify(
Person { age: 40 },
Person::age,
500
));
}
#[inline(never)]
fn modify<KP, F>(person: &mut Person, path: KP, field: F)
where
Person: IndexMut<KP, Output = F>
{
person[path] = field;
}
Now, to see what this actually generates, let's run it through cargo-show-asm
to see what this generates:
$ RUSTFLAGS="-Znext-solver=globally" cargo +nightly asm demo::modify
.section .text.demo::modify,"ax",@progbits
.p2align 2
.type demo::modify,@function
demo::modify:
.cfi_startproc
mov w8, #5
strh w8, [x0, #32]
ret
With no optimizations enabled, doing only the bare minimum to ensure nothing is inlined too aggressively, (and assuming this is the only monomorphization of the requested fn) we get an extremely simple, basically transparent, implementation of the function.
There is no runtime checking or processing - everything is completely transparent and exists only as types at compile-time.
And this works at arbitrary nested depths as well, even including working with usize
-based keypaths/indices. For example:
// create a keypath to the `people` field of the `Family` struct
let height_kp = Family::people
// extend that keypath into the first item inside `people`
.idx::<0>()
// extend that kp into the `dimensions` field of the first person
.kp::<"dimensions">()
// and finally finish the keypath off by telling it to retrieve the height.
.kp::<"height">();
The main building block of this crate is the [Keyable
] derive macro - this implements std::ops::Index
and std::ops::IndexMut
traits for all types which it is used on (along with a few other things).
The specific types which Keyable
-derived structs can be Index
ed by, however, are unimportant. They're increasingly annoying to name the more nested they get, and are the most likely part of this library to change from version to version.
The way to create these Index types, however, is by using the named helpers that are provided by the Keyable
macro. For example, in the first example up above, a Person::age
constant was generated by the macro, which was then used to index into a Person
.
These keypaths can then be extended by two methods:
fn kp<const FIELD: &'static str>() -> _
, which takes no arguments and uses the single generic argument, a const&'static str
, to create another, nested, keypath.fn idx<const I: usize>() -> _
, which also takes no arguments ans uses the single generic argument, a constusize
, to create the nested keypath.
Due to the way this API works, all information about which field or index is being accessed is encoded at the type level, and every KeyPath
-adjacent type in kathy
is 0-sized.