Why is `!Trait` not possible?

well, if I wrap it around, I can do this:

where Env: TMap<LayoutCtx>
{
    let layout = env.get::<LayoutCtx>();
    let sublayout = layout.make_for_child();
    env.append(sublayout, |env| { // this overwrites the LayoutCtx for child
        child(env);
    });
}

Aha, now I get it. I think this has a good solution, let me think further

1 Like

So, you're essentially wanting to do functional updates of the individual fields. I'm guessing you don't really mean to make copies of all these objects, but the assumption is that the list is a list of references?

We could extend my #[derive] approach like

impl DasLixousCrate::ContextContains<WinitContext> for MyContext {
  fn get(&self) -> &WinitContext {
    &self.winit_context
  }
  fn subcontext_with_this_item_replaced(&self, replacement: &WinitContext) -> MyContext {
    MyContext {
      winit_context: replacement,
      ..self
    }
  }
}

If some of them need to be mutable then you would have to think about the re-borrowing logic, but that's doable. And all of this is very easily generated by a derive macro, making the user not have to write boilerplate.

Technically speaking this does copy the memory usage of the list of references; if somehow the user is having a context with 1000 different types in it, you could break it down into a tree or something, but I'm guessing that's not an issue.

ooh I think there's a way to do this as a linked list of replacements, too, with the types known on the type level, like you were originally thinking (it's just that it would still use a derive macro like this). But that isn't my real suggestion, because the performance cost is probably worse (you'd rather copy all the references when you override something, rather than have to follow a linked list of references every time you use one). Also I think you might have to mess around with generic associated types, which is fine but enough work that I didn't feel like implementing it all just to show it as an example. :sweat_smile:

Yeah, just came back to this and I was also thinking if I couldn't just make this a list of references and replace them like in a real map, then have the functional wrapper around it rereplace to old value.

EDIT: might not work directly as intended, as they'd have different lifetimes (e.g. I want X<'static> to be overwritable with X<'a>), but I'll sleep over that

But out of curiosity, what is really the blocker for implementing !Trait?

Like what I've gathered from this discussion is basically:

  • when treating it as an auto trait which gets unimplemented on types implementing T, the logic is already there

and

  • when this is implemented it offers a nice and simple way around specialization

There isn't one, it already exists -- it's just different from what you describe here :wink:

It's used for impl !Clone for &mut _, to close a subtle soundness hole: https://doc.rust-lang.org/nightly/std/clone/trait.Clone.html#impl-Clone-for-%26mut+T

So it's actually already planned to be allowed to where T: !Clone, but that'll only match types for which there's an explicit impl !Clone.

Otherwise you have a massive stability problem, because if where T: !Display matches things that don't today implement Display, that code breaks if the upstream type adds a Display trait implementation, which we absolutely want people to be able to do without it being breaking.

Thus you'll only be able to where a negative bound when the type in question makes a semver promise to not implement it ever, rather than just it hasn't implemented it yet.

4 Likes

oh yeah hmmm, semver is a valid argument :confused:

1 Like

A demonstration of how explicit negative impls are less troublesome and not specialization-like: they can be treated like they are weird positive impls, because they are explicit and mutually exclusive with positive impls. In particular, I think that you can in principle translate

trait Foo {
    fn foo();
}

impl !Foo for DoesNotImpl {}
impl Foo for DoesImpl {
    fn foo() { todo!() }
}

fn wants_positive<T: Foo>() {}
fn wants_negative<T: !Foo>() {}

into

trait Foo {
    const POSITIVE: bool;
    fn foo()
        where Self::POSITIVE == true;
}

impl Foo for DoesNotImpl {
    const POSITIVE: bool = false;
}
impl Foo for DoesImpl {
    const POSITIVE: bool = true;
    fn foo() { todo!() }
}

fn wants_positive<T: Foo<POSITIVE = true>>() {}
fn wants_negative<T: Foo<POSITIVE = false>>() {}

You can even get it to compile on stable with enough tricks:

struct False;
struct True;
trait Foo {
    type Positive;
    fn foo()
        where Self: Foo<Positive = True>;
}

struct No;
struct Yes;
impl Foo for No {
    type Positive = False;
    // for<'a> bound works around lack of https://github.com/rust-lang/rust/issues/48214
    fn foo() where for<'a> Self: Foo<Positive = True> { unreachable!() }
}
impl Foo for Yes {
    type Positive = True;
    fn foo() { todo!() }
}

fn wants_positive<T: Foo<Positive = True>>() { T::foo() }
fn wants_negative<T: Foo<Positive = False>>() { /* T::foo() */ }
1 Like

@kpreid this is what I've tried here

The compiler can’t fully handle associated constants, especially not anything based on exhaustiveness of separate impls (impl for true and impl for false β‡’ impl for all). My post was expanding on the sub-topic that @scottmcm mentioned, that negative impls are already a thing, not solving your problem.

1 Like

Yup, exactly. They're just a simpler form of mutually-exclusive for coherence to understand than making a fully general "this trait is disjoint from that one" system.

One option is to lean on typenum to assign your own pseudo-type-id:

use typenum::{self as tn, type_operators::Cmp};

pub trait UniqId {
    type Id;
}

trait Pick<Id> {
    type Output;
    fn pick(&self)->&Self::Output;
}

struct Empty;

impl<X> Pick<X> for Empty {
    type Output = Self;
    fn pick(&self)->&Self { self }
}

impl<T,U:UniqId,X:UniqId> Pick<X> for (T,U) where 
    X::Id: Cmp<U::Id, Output: PickBranch<X,Self>>{
    type Output = <<X::Id as Cmp<U::Id>>::Output as PickBranch<X,Self>>::Output;
    fn pick(&self)->&Self::Output {
        <X::Id as Cmp<U::Id>>::Output::branch(self)
    }
}

trait PickBranch<X, Ctx> {
    type Output;
    fn branch(_:&Ctx)->&Self::Output;
}

impl<X:UniqId, T:Pick<X>, U> PickBranch<X, (T,U)> for tn::Less {
    type Output = T::Output;
    fn branch(ctx: &(T,U))->&Self::Output { ctx.0.pick() }
}

impl<X:UniqId, T:Pick<X>, U> PickBranch<X, (T,U)> for tn::Greater {
    type Output = T::Output;
    fn branch(ctx: &(T,U))->&Self::Output { ctx.0.pick() }
}

impl<X:UniqId, T:Pick<X>, U> PickBranch<X, (T,U)> for tn::Equal {
    type Output = U;
    fn branch(ctx: &(T,U))->&Self::Output { &ctx.1 }
}

impl<Id, T: Pick<Id>> Pick<Id> for &'_ T {
    type Output = T::Output;
    fn pick(&self)->&Self::Output { (**self).pick() }
}

pub trait FromCtx<T> {
    fn from_ctx(ctx:&T)->&Self;
}

impl<X:UniqId, T:Pick<X, Output=X>> FromCtx<T> for X {
    fn from_ctx(ctx:&T)->&Self { ctx.pick() }
}

impl UniqId for u128 { type Id = tn::U1; }
impl UniqId for u64 { type Id = tn::U2; }
impl UniqId for u32 { type Id = tn::U3; }
impl UniqId for u16 { type Id = tn::U4; }
impl UniqId for u8 { type Id = tn::U5; }

fn print_ctx<C>(x:&C) where
    u128: FromCtx<C>,
    u64: FromCtx<C>,
    u32: FromCtx<C>,
    u16: FromCtx<C>,
    u8: FromCtx<C>,
{
    let a = u8::from_ctx(x);
    let b = u16::from_ctx(x);
    let c = u32::from_ctx(x);
    let d = u64::from_ctx(x);
    let e = u128::from_ctx(x);

    println!("{a} {b} {c} {d} {e}");
}

fn main() {
    let x = (((((Empty, 5u128), 4u64), 3u32), 2u16), 1u8);
    print_ctx(&x);
    print_ctx(&(&x, 42u32));
}
1 Like

Regarding the initial problem, this is something I've been thinking a lot as it enables compile-time hierarchical dependency injection. You can have a base layer defining your dependencies, and then a child layer for local partial overrides. The compiler automatically picks values from the top of the current stack.

A particularly important use case for me are RPC clients. You may want to configure your client, for example with a base URL, auth token, timeout, etc. Code using it can then create queries where you are able to override these settings. I want two things at the handler level:

  1. Ensure at compile time that the base URL is defined at least in one place in the stack
  2. Resolve at compile what is the top-most value for the base URL

Here is simplified implementation from a project I've been working on: Rust Playground

The try_merge_triple_cx* test cases in particular should show you how it looks from a user point of view. There are also some commented tests showing rejection when the base URL is not set. The values to use are resolved at compile time.

Further below there is a more concrete example showing how it can be used for my use case involving compile-time DI for an RPC client with override support.

The original code is more complex and I don't have too much time to polish the Playground example, but it should show the core ideas with compile time TyNone, TySome, TryGetBaseUrl, GetBaseUrl and TupGetBaseUrl types/traits. This can be adapted to work on references or have a more generic getter trait. My real code uses for<'a> (&'a QueryCx, &'a ClientCx): Get<BaseUrl> + Get<AuthToken> as the trait bound for handlers for example. (You can get to a point where there's basically no clone or dynamic lookup.)

I should probably take some time to write a more in-depth description of this approach, but I hope that the playground link will help you.


I'm pasting the most important part as part of the post, this how the hierarchy is handled, using a case distinction on associated types. Since Rust does not let me check for !Trait, the workaround is to match on an associated type where one represents impl Foo and the other impl !Foo. TryGetBaseUrl<Output=TyNone> is the negative version, and TryGetBaseUrl<Output=Url> is the positive version.

trait TupGetBaseUrl<Cx: TryGetBaseUrl, Out = <Cx as TryGetBaseUrl>::Output> {
    type TupOut;

    fn tup_get_base_url(&self) -> Self::TupOut;
}

impl<Cx0, Cx1> TupGetBaseUrl<Cx0, TyNone> for (Cx0, Cx1)
where
    Cx0: TryGetBaseUrl<Output=TyNone>,
    Cx1: TryGetBaseUrl,
{
    type TupOut = Cx1::Output;

    fn tup_get_base_url(&self) -> Self::TupOut {
        self.1.try_get_base_url()
    }
}

impl<Cx0, Cx1> TupGetBaseUrl<Cx0, Url> for (Cx0, Cx1)
where
    Cx0: TryGetBaseUrl<Output=Url>,
{
    type TupOut = Cx0::Output;

    fn tup_get_base_url(&self) -> Self::TupOut {
        self.0.try_get_base_url()
    }
}

impl<Cx0, Cx1> TryGetBaseUrl for (Cx0, Cx1)
where
    Cx0: TryGetBaseUrl,
    Self: TupGetBaseUrl<Cx0>,
{
    type Output = <Self as TupGetBaseUrl<Cx0>>::TupOut;
    fn try_get_base_url(&self) -> Self::Output {
        self.tup_get_base_url()
    }
}

You can generalize this idea by using Output=TySome<T> for the positive case. From what i remember, the context trait and values that can be extract are still somewhat coupled. Full support for negative traits would simplify the code.

1 Like

Eh. Function combinators seem more useful to me for this kind of stuff. See this blog post. Essentially, the user code builds up the "what" Endpoint and then wraps it up in "how" combinators before passing it off to a client (I typically leave authentication in the client itself as this tends to apply to everything it does whereas everything else ends up caring a lot more about the context of the actual query). For example:

let endpoint = SomeEndpoint::builder()
    .param1("foo")
    .param2("bar")
    .build()
    .unwrap();
let endpoint = paged(endpoint); // do pagination
let endpoint = sudo(endpoint, as_user); // apply sudo rights to the endpoint
let endpoint = ignore(endpoint); // ignore returned content (still parses out error states)
let endpoint = timeout(endpoint, Duration::from_secs(5)); // timeout the request after 5 seconds
// perform the query
let _: () = endpoint.query(&endpoint).unwrap();

Using function combinators or builders to build the query struct is an orthogonal concern that fits very well with context merging. I know that my code example used a simple method on the the client type, but building a generic client was not the point of the example. The point was to show how you can have config at two (or more) levels and resolve were values should be picked from at compile time.

The full client libs that I write tend to have a style closer to what's described in your linked article, with reified endpoints. These are not incompatible.

I typically leave authentication in the client itself as this tends to apply to everything it does whereas everything else ends up caring a lot more about the context of the actual query

This comment is closer to why I was looking into this issue in the first place. Having auth attached to the client level covers most simple cases and allows an easy "quick start". In more complex cases, a fully stateless client with explicit per-call auth is more useful. The use-case is when you deal with multiple authorizations dynamically. The context-merging solution that I provide allows to abstract over where you set the auth: either once at the client level, or per-call, or both.

Note also that I'm not advocating to use this for all parameters, contextual parameters for specific endpoints should only be set for specific query structs.

These are all higher level design concerns that are important for a lib, but out of scope for this topic IMO. The OP was just asking about a solution to retrieve the outer value in an (((), A), B), A) situation.

In type theory there is a similar problem with inductive type definitions, which are in some ways pretty similar to trait definitions. Theories that are sound generally require a so called "positivity checking", which requires the inductive type (in this case the trait being implemented) to appear only with positive polarities in the constructors (the trait implementation in this case) parameters (the implementation's bounds in our case). Most notably the proposition !T: Paradox in type theory is equivalent to the proposition (T: Paradox) -> False, where T occurs with a negative polarity, while simply T: Paradox has a positive polarity.

1 Like

Wow! That's amazing! I've thought about associated types, but with two traits :0 smart. I'm gonna take some inspiration from that if you're okay with that :>

1 Like