Super quick idea overview:
impl Visitor for MyNode
with(cx: &mut Context)
{
fn visit(&self) {
cx.counter += 1;
}
}
fn process_item() {
let cx = Context { counter: 0 };
let v = vec![MyNode, MyNode, MyNode];
with(cx: &mut cx) {
v.visit();
}
assert_eq!(cx.counter, 3);
}
Then the supposed issue is
fn foo<T: Visitor + 'static>(v: T) {
let x: Box<dyn Visitor> = Box::new(v);
}
For this to be a problem, we fist have to even just allow
fn foo<T: Visitor + 'static>(v: T) {
v.visit();
}
with(cx: &mut Context { .. }) {
foo(MyNode)
}
which is not simple itself! MyNode
implements Visit
"when in a with
context", so there's two ways I can see this realistically semantically done:
- this is effectively sugar for thread locals, or a similar mechanism, and it fails at runtime if the with context isn't actually present, or
- this is effectively sugar for anonymous ad hoc newtyping, or a similar mechanism, and lifetimes are inherited that way.
For an even simpler illustration of the root issue:
let mut cx = Context::new();
let visitor: impl Visitor;
with(cx: &mut cx) {
visitor = MyNode; // MyNode: Visitor
}
visitor.visit(); // MyNode: !Visitor
I think my latter provision here makes more sense. Utilizing an imaginary structural types to illustrate, with
context could semantically behave as (and derive borrow rules from) roughly the following:
// with clause
impl Visitor for MyNode
with(cx: &mut Context);
// explicit structural newtype
impl Visitor for
WithContext<
MyNode,
{ cx: &mut Context, .. },
>;
// with block
with(cx: &mut cx) {
foo(MyNode)
}
// explicit structural newtype
{
__with_context = AddStructure![
__with_context
& { cx: &mut cx }
];
foo(WithContext {
0: MyNode,
1: SelectContext![
__with_context
| { &mut cx }
],
});
}
That is, the requested context is carried along as a tuple, copying/moving/reborowing the context as required when coerced from a concrete type with the context into a impl Trait
that is no longer asking for context.
Yes, this means that boxing it would not be Box<MyNode>
, it'd be Box<WithContext<MyNode, { cx: &mut Context }>>
. And it carries the lifetime (and isn't 'static
).
Personally I don't really like with
/implicits, but I do see the value they can provide for easily adding in some more context to existing trait signatures. I think if Rust were to have them, it'd be as I've described here as essentially sugar over newtypes that carry the context, and easily/quickly constructing them just-in-time as-needed.
Here's another, more closure-like syntax (inspired also by Java anonymous classes) to try to help illustrate how I think it could work:
foo(impl Visitor {
visit: |&self| {
cx.counter += 1;
},
})
Also of note is that this wouldn't provide with
context to "static methods" (that is, ones without self
arguments). Also, you couldn't pass two arguments that were with
-dependent on the same with
context, because they can't both borrow the same context, it has to be passed in.
The issue is that the called code doesn't know about the context, so it can't pass it.
Even before considering dyn
, a with
proposal needs to consider and explain exactly how the with context is passed around through code that knows about it (has the with
bound) and is packaged for code that doesn't know about the with context (just uses genetics without a with
bound).
What does it "desugar" as? The answer to that question makes the answers to the rest of the questions much easier to answer.