With clauses
Status
Crazy new idea that solves all problems
Summary
- Introduce a
with(x: T)
clause that can appear wherever where clauses can appear- These variables are in scope in any (non-const) code block that appears within those scopes.
- Introduce a new
with(x = value) { ... }
expression- Within this block, you can invoke fuctions that have a
with(x: T)
clause (presuming thatvalue
is of typeT
); you can also invoke code which calls such functions transitively. - The values are propagated from the
with
block to the functions you invoke.
- Within this block, you can invoke fuctions that have a
More detailed look
Simple example
Suppose that we have a generic visitor interface in a crate visit
:
#![allow(unused)] fn main() { trait Visitor { fn visit(&self); } impl<V> Visitor for Vec<V> where V: Visitor, { fn visit(&self) { for e in self { e.visit(); } } } }
I would like to use this interface in my crate. But I was hoping to increment a counter each time one of my types is visited. Unfortunately, the Visitor
trait doesn't offer any way to thread access to this counter into the impl. With with
, though, that's no problem.
#![allow(unused)] fn main() { struct Context { counter: usize } struct MyNode { } impl Visitor for MyNode with(cx: &mut Context) { fn visit(&self) { cx.counter += 1; } } }
Now I can use this visitor trait as normal:
#![allow(unused)] fn main() { 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); } }
How it works
We extend the environment with a with(name: Type)
clause. When we typecheck a with(name: value) { ... }
statement, we enter those clauses into the environment. When we check impls that contain with
clauses, they match against those clauses like any other where clause.
After matching an impl, we are left with a "residual" of implicit parameters. When we monomorphize a function applied to some particular types, we will check the where clauses declared on the function against those types and collect the residual parameters. These are added to the function and supplied by the caller (which must have them in scope).
Things to overcome
Dyn value construction: we need some way to permit impls that use with
to be made into dyn
values. This is very hard, maybe impossible. The problem is that we don't
want to "capture" the with
values into the dyn
-- so what do we do if somebody
packages up a Box<dyn>
and puts it somewhere?
We could require that context values implement Default
but .. that stinks. =)
We could panic. That kind of stinks too!
We could limit to traits that are not dyn safe, particularly if there was a manual impl of dyn safety. The key problem is that, today, for a dyn safe trait, one can make a dyn
trait without knowing the source type:
#![allow(unused)] fn main() { fn foo<T: Visitor + 'static>(v: T) { let x: Box<dyn Visitor> = Box::new(v); } }
But, now, what happens if the Box<dyn Visitor>
is allowed to escape the with
scope, and the methods are invoked?
Conceivably we could leverage lifetimes to prevent this, but I'm not exactly sure how. It would imply a kind of "lifetime view" on the type T
that ensures it is not considered to outlive the with
scope. That doesn't feel right. What we really want to do is to put a lifetime bound of sorts on the... use of the where clause.
We could also rework this in an edition, so that this capability is made more explicit. Then only traits and impls in the new edition would be able to use with
clauses. This would harm edition interop to some extent, we'd have to work that out too.