1 unstable release

0.1.0 Aug 21, 2024

#2598 in Rust patterns

MPL-2.0 license

12KB
156 lines

kathy

Const-evaluated, zero-cost, very simple keypath functionality for rust.

Requires nightly and env RUSTFLAGS="-Znext-solver=globally" to work and use correctly :)

0 runtime cost

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 runtime.

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">();

usage

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 Indexed 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:

  1. 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.
  2. fn idx<const I: usize>() -> _, which also takes no arguments ans uses the single generic argument, a const usize, 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.

Dependencies

~225–660KB
~16K SLoC