I had an idea for an abstraction that presents (potentially composite) reference types, stripped of their lifetime. The idea is to retain safety by providing an interface that only lets you construct these types behind a regular reference with the appropriate lifetime.
use std::{ops::Deref, ptr::NonNull};
pub struct Ref<T: ?Sized>(NonNull<T>);
impl<T: ?Sized> Deref for Ref<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { self.0.as_ref() }
}
}
pub trait Local {
type Target;
/// # Safety
/// The returned value must immediately be taken as a reference, whose
/// lifetime is shorter than the input value.
unsafe fn build(self) -> Self::Target;
}
impl<T: ?Sized> Local for &T {
type Target = Ref<T>;
unsafe fn build(self) -> Self::Target {
Ref(NonNull::from(self))
}
}
impl<A: Local, B: Local> Local for (A, B) {
type Target = (A::Target, B::Target);
unsafe fn build(self) -> Self::Target {
let (a, b) = self;
(a.build(), b.build())
}
}
#[macro_export]
macro_rules! local {
($e:expr) => {{
let e = $e;
&unsafe { $crate::Local::build(e) }
}};
}
The idea is to support a parametric trait like this:
pub trait Component<Args> {
fn run(&self, args: &Args);
}
impl<Args> Component<Args> for () {
fn run(&self, _: &Args) {}
}
pub fn foo() -> impl Component<(Ref<str>, Ref<bool>)> {}
#[test]
fn test_foo() {
let f = foo();
let b = false;
f.run(local!(("test", &b)))
}
If Rust had proper support for higher kinded types, I'd of course not need this hack. I tried various ways using GATs, but they're currently so broken (compiler ICEs, bogus errors, etc.) as to be unusable. I also tried just quantifying over reference types directly, but again without HKTs there are too many patterns that are impossible.
Something like this hack seems to be the only viable option if you want to quantify over composites of borrowed types. However the fact that I've never seen this in the wild makes me suspicious that I'm missing some obvious soundness issue.
The immediate flaws I see are fixable. The trait needs to be unsafe trait, and the local macro would need to prevent lifetime extension. (As I’m finishing the previous sentence, @jonh is has just given a code example for lifetime extension, so I don’t need to anymore.)
Also, while GATs have their flaws, some of the (pre-GAT) workarounds with HRTBs are quite workable in my experience; so I wouldn’t rule out the possibility that this can be addressed in a “more proper” approach too quickly; feel free to share some attempts running into the mentioned (bogus) compiler errors, to demonstrate your use-case, and I’ll gladly try to see if I can come up with a way to work around them. (No promises of course, probably there are some hard limitations that cannot be overcome, too.)
Edit: On second thought, maybe the lifetime issue isn’t quite as trivial to solve as I first imagined. I just tried to write up a possible solution here but then it seemed flawed, too.
Ah right, thanks. This specific pattern seems easy to prevent, since the point of this abstraction is only to support the Component trait. So I think something like this would work?
macro_rules! run_local {
($c:expr, $a:expr) => {{
let a = $a;
$crate::Component::run($c, &unsafe { $crate::Local::build(a) })
}};
}
#[test]
fn test_foo() {
let f = foo();
let b = false;
run_local!(&f, ("test", &b))
}
If you have thoughts on how this might be possible, I'd like to hear it! Although it's not totally necessary, if I can prevent lifetime extension with the ergonomics of the original macro that would be preferable.
Without looking at your solution, my first instinct would be to write something along these lines, separating the two roles of specifying a higher-kinded argument type and coercing a specific value into that type.
(This version is just a draft; I'm sure there are better names and other ways to improve the ergonomics, but hopefully it gets the point across.)
use std::marker::PhantomData as PhD;
pub trait WithLt<'a> {
type Concrete: 'a;
}
pub trait Covariant<'a> {
type Output: 'a;
fn shorten(&'a self)->Self::Output;
}
pub trait Component<Args> {
fn run<'a>(&self, args: &'a impl Covariant<'a, Output = <Args as WithLt<'a>>::Concrete>) where Args:WithLt<'a>;
}
pub struct Ref<T:?Sized>(PhD<T>);
impl<'a, T:'a+?Sized> WithLt<'a> for Ref<T> {
type Concrete = &'a T;
}
impl<'a, T, U> WithLt<'a> for (T,U) where T:WithLt<'a>, U:WithLt<'a> {
type Concrete = (T::Concrete, U::Concrete);
}
impl<'a, 'b:'a, T:'b+?Sized> Covariant<'a> for &'b T {
type Output = &'a T;
fn shorten(&'a self)->&'a T { &**self }
}
impl<'a, T, U> Covariant<'a> for (T,U) where T:Covariant<'a>, U:Covariant<'a> {
type Output = (T::Output, U::Output);
fn shorten(&'a self)->Self::Output {
let (t, u) = self;
(t.shorten(), u.shorten())
}
}
impl<Args> Component<Args> for () {
fn run<'a>(&self, _: &'a impl Covariant<'a, Output = <Args as WithLt<'a>>::Concrete>) where Args:WithLt<'a> {}
}
pub fn foo() -> impl Component<(Ref<str>, Ref<bool>)> {}
#[test]
fn test_foo() {
let f = foo();
let b = false;
f.run(&("test", &b))
}
The HRTB on F prevents the local lifetime from being something which is not fully subsumed by the lifetime of T. Am I being naive again or does this one actually work?