Lately I’m writing a lot of code with “context” arguments. This seems to happen more in Rust, because the language does not like object-graph soup. Objects in a tree can’t contain up/parent pointers as easily as, say, Java. So code that looks like this in another language:
button.render()
label.render()
becomes something like this in Rust:
let ctx: RenderCtx = ...
button.render(&ctx);
label.render(&ctx);
I haven’t used Scala in years, but I remember Scala version 2 had “implicits” and now version Scala 3 has something similar with the keywords ‘given’ and ‘using’. Rustified, it would look something like:
let given ctx: RenderCtx = ...
button.render()
label.render()
impl Button {
fn render(&self, using ctx: &RenderCtx) { ... }
So a value in the static scope of a call, marked a certain way, like ctx here, becomes available to implicitly pass for certain parameters.
I’m curious if this is, or ever was, a proposal for Rust.
One of the core values of the Rust language is "explicit over implicit", so a new feature that promotes implicit code is quite unlikely to be accepted into the language.
I mean how is this any more "implicit" than async, reborrows, coercions, drops, copies, auto traits, ?, etc.? They at least proposed a keyword, using, to annotate a function's parameter making it somewhat explicit. I'm not saying I agree or disagree with this proposal, but I don't think it's necessarily against one of the "core values".
Async for example is quite explicit, as async code as well as await sites are explicitly marked as such.
I'm not a gatekeeper, I'm simply conveying the likely outcome of trying to get implícits into Rust.
But if you feel it's an arbitrary boundary, feel free to write a RFC for it. See how it goes, as well as why that process ends up at its particular destination.
Indeed. The point was to show that "implicit" vs. "explicit" is more a continuum than some discrete thing. Unless explicitly called, drops (i.e., Drop::drops) just happen without any special annotation at the call site just like this proposal where instead of a function being implicitly called happens a function is explicitly called where argument(s) are implicitly passed (i.e., I'd argue an implicit call to a function is "more implicit" than an explicitly called function with implicitly passed arguments). Similarly reborrows just happen. Of course reborrows don't let you pass in fewer arguments, so I'd argue it's not "as implicit" as this.
Again depends on what you mean by "explicit". An async fn and this both have to be explicitly marked: async preceding fn and using preceding the parameter(s). While normally an async fn is called inside another async fn which itself uses await internally, one can call an async fn in a normal fn without await. Of course doing so requires you to manually control the Wakers and Contexts making it even more "explicit" in the sense of verbosity and complexity but not in the sense of special keywords/operators that signal its use at the call site.
I don't care enough about this or even at all, so I'll pass. The existence of such desires by people on the actual language team—as the aforementioned links illustrate—is enough "proof" that something like this isn't necessarily against the core values. Changes to a language are hard though and can take a long time if they ever are made, so I wouldn't expect this to be added to the language in stable for many years even if it were to be accepted.
I would generally avoid phrasing it that way, because of course there's a ton of implicit stuff in Rust -- the obvious example being type inference. It would be more "explicit" to type-annotate every local, for example, but we don't do that.
I'm concerned about borrowing of the context and its lifetimes being invisible.
Rust had problems with invisible lifetimes before. Rust allows omitting elided lifetimes from types used in function signatures, but in function signatures like fn do(a: &arg) -> Ref it ended up hiding an important detail. Rust now suggests writing fn do(a: &arg) -> Ref<'_>. Similar with impl Trait capturing.
This isn't exactly the same problem, but more like a reverse of it: the function definition is clear, but the function use isn't.
So you might have a function that returns something borrowed from &ctx, but you won't see the connection to ctx from the function call, and it might be surprising why your returned value doesn't live long enough. Maybe good enough compiler errors can solve it.
Maybe people will just use one big ctx for everything that will always be in scope? OTOH this makes me worried it would encourage poorly-separated kitchen-sink God-class contexts.
Please don't, implicits is easily the worst feature of scala. The second you wanna have two separate implicits of the same type (or a subtype and super type), everything falls apart and you need to change all of your code to explicitly specifying implicits.
Many problems Scala solved with implicits, rust did with Self typing and static methods in traits, so the type itself has info about constructing it's instances. In all the others cases, I prefer to propagate things manually.
People here are asking where's the line between too implicit and still OK - for my money, if I can't middle click on something in the expression and get to the code that's running here, it's too implicit. And yes, that includes v.into::<T>() in most cases. If I could dissalow it in projects in favor of T::from(v), I'd do it.
I think there were multiple features around “implicits”. If I remember correctly, implicit conversions (imagine into() without calling into()) were too much for me.
But I don’t have the problem you describe above with a more limited idea of implicit arguments. If I needed to pass something explicitly in one place, due to ambiguity, I don’t mind that.