Unrelated, this conversation reminded me of the fact that I implemented the _ feature in Rubinius at Golden Gate Ruby Conference in 2011 after Jose Valim (who went on to create Elixir) discussed the benefits of _ over &: in a talk about creating your own programming language.
I showed an implementation in a lightning talk the next day. Fun times! (I miss Rubinius )
Without even any syntactic changes we could get something sort of like _ by modifying the inference algorithm so that it could look up the missing type in chars().any(<_>::is_whitespace) (call it reverse turbofish). Currently it doesn’t successfully infer that _ is char here.
Even when the typename is long, I like the bare function style more than the closure/method call style because it feels more lightweight to me. YMMV.
I mostly agree -- it feels elegant whenever I use it! However, it doesn't work for methods with arguments, which is where things like Scala's _ start to shine.
If we really want to replace emplacement syntax, we need to allow scope exits (btw, box try!(foo) destroys the point of box - we need some ABI magic if we want to make it zero-copy). That introduces a significant amount of implementation complexity, and precludes using the standard Fn traits.
I feel that zero-argument closures are not important enough to get a magic special case. "point-free" closures would be a nice improvement over the current situation:
Type-directed lookup - an obvious advantage over UFCS functions
Coercions - often you get a &&String or something and want to call something that takes an &str. That requires a shim to do the deref-coercion.
Bound methods - writing |foo| self.is_xyz(foo) is annoying and completely unnecessary.
Yeah, that's the main argument for me of _ over something equivalent to Ruby's Symbol#to_proc. Promoting specifically 0 argument functions called on the closure argument makes more sense in a purely OO language, but _ can do everything it can, and is more flexible.
JavaScript makes this kind of thing pretty easy (obj.map(fn) and obj.map(obj.method) both work, but even in JavaScript I quite often get pinned into using arrow syntax (obj.map(o => o.method(arg))) and wish for a more compact form.
One point of possible difference, in case we wanted to explore method extraction as a way to shorten some scenarios: in JavaScript, obj.method produces an unbound function, which means that obj.map(obj.method) doesn't usually work. If Rust had a method extraction syntax that produced a bound closure instead as a shorthand, it would support many more cases without needing to resort to full closures.
On the other hand, the fact that fields and methods are in different namespaces makes the most obvious candidate for extraction (obj.method) non-viable, which complicates the design space somewhat.
It depends how we implement it, really. One option is to use distinct return values that the compiler understands, such as ControlFlow<T>, when processing the sugar. ControlFlow looks kind of like:
Basically the closure is telling you whether it returned normally (NormalReturn) or whether some "early break" was used (e.g., a continue, a break, etc). In the latter case, the intermediate stack frames would want to propagate that break up until the loop that expects to handle it. This very simple enum winds up requiring some local stack slots to track what kind of early break occurred: more elaborate versions might minimize that.
Something like this actually used to be builtin to the compiler instead of iterators, though we only supported ControlFlow<()> (and we just used bool for it). (Early return did require generating a local slot to store the return value.)
Another option is to have distinct traits, and maybe a distinct syntax, though I'd hope we can infer it.
I'm definitely interested in pursuing some improvements to our closure syntax, though I think that something like _ would be a simpler addition than trying to maximize our "Tennent's Correspondence Principle"-ness.
I agree though with @arielb1 that if we want to add implicit closure sugar we should think carefully about TCP.
Big -1 for autoclosures. Obscuring evaluation order at the call site just to save, what, 3 characters? That’s a bad tradeoff. It’s my least favorite part of Swift, and I don’t like it in Scala, either (though it doesn’t seem to get used very frequently there).
I’m a bigger fan of the compact closure form, and there’s been at least a little discussion about it in the past. Wasn’t very well received then.
It feels better received in this thread. I, for one, am a fan of it if we can find a better sigil than _ (already taken imo) and which doesn't look like a regular identifier (it). I don't think I like $0 (partial application of multiple args is more rare, and it starts to obscure more than the compactness benefit affords).
I strongly agree, but have the (perhaps controversial) opinion that ? tips us from "TCP would be nice" to "we probably need TCP" for more closures than just the compact ones.
One of the most frustrating things for me about our error story is that it becomes extremely awkward to use (even in ? form) once closure-taking APIs get into the mix. And I really enjoy using those APIs. In practice, this quite often pushes me into using for loops and match statements when map would do.
In the past, I've privately floated the idea of syntax like this, which would introduce a final-position closure syntax that allowed returns to retain their out-of-closure meaning:
This is definitely controversial, because it adds a new case of (bounded) unwinding, and almost certainly wouldn't be possible to add transparently. But I think it's worth strongly considering.
Because of the implementation of FromIterator for Result, once you add necessary iterator memory management boilerplate, that does work.
I feel very ambivalent about TCP. On the one hand, when I think of lambdas as lambdas, it seems much more intuitive to me that control flow within a lambda behaves in reference to that lambda, rather than to its lexically containing scope. On the other hand, when lambdas are being used to create fancy DSLs - as they are often in Ruby, for example - TCP (and, incidentally, paren-free syntax) make a lot of sense. DSLs like rspec use lambdas to give the appearance of a new control flow construct, you don’t really think of it as an anonymous function per se.
I’m in favor of making it easier to create DSLs in Rust, but I wonder if procedural macros aren’t the approach for that purpose that is more consistent with the existing ontology of the Rust language, which seems to prefer metaprogramming be restricted to compile time. (Not that a shift couldn’t be made.)
I could perhaps imagine that working, but the overhead of the ! suffix, especially given how "loud" that sigil feels, can quickly overwhelm many kinds of DSLs.
That's more-or-less why I prefer a new syntax do |...| { (and compact closures) to making every closure automatically TCP.
The major difference for me between Ruby and Rust is that Ruby provides a number of built-in control flow structures that can be used in various ways as escape continuations:
break escapes out of the nearest lexical call, allowing general-purpose break in custom iterator-like methods
next "returns" from a closure, allowing general-purpose "continue" in custom iterator-like methods while leaving return to always mean "from a method"
return returns from the nearest method
raise raises an exception, which is caught by the dynamically nearest rescue clause that handles the relevant exception type.
throw is a general purpose unwinding mechanism, that is unrelated to exceptions or errors:
o = Object.new
p = Object.new
v = catch(o) do
catch(p) do
throw o, "hello"
end
end
v #=> "hello"
In contrast, Rust has very few mechanisms for controlling escaping through closures, but relies heavily on the return control flow for error propagation. It's not fatal, but it's frustrating sometimes
Interesting! This doesn't work in all cases (I most often find myself frustrated by the closure-taking methods of Option/Result) but I wonder if there are more general-purpose solutions that avoid needing TCP than I expect.
Seeing that this has morphed into a discussion of control flow more generally:
the existing ontology of the Rust language, which seems to prefer metaprogramming be restricted to compile time.
The fact we get so far with closures not using trait objects made me realize that for all my FP habits I am more of a first-order thinker than I'd like to admit. That's not a very decipherable sentence, so let me expand :). When data flow is higher order, e.g. Vec<Fn()>, I: Iterator<Item=Fn()>, one basically has to resort to using trait objects for the clsoures. Since we (or at least I) don't usually need them, it means the higher-order functions I use most of the time are being routed a single closure. Just as the compiler can inline the (static) closure and HOF so easily (and hey, it might as well given it is already monomorphizing the higher order function for the closure type), so my brain can also inline them fairly readily, and I suspect that it in fact does, working backwards from a first-order intent to a higher order means of expressing it in the name of concision or bringing out the first-order data flow. The maps/filters/folds might as well be really well-done macros given how they are used.
This leads me to suspect while ruby control flow, or monadic do notation, or "scoped continuations", or whatever we want garbles the program's run time, a restricted variant that tried to inline the hell out of everything to something more first order would be a lot more practical than theory would lead us to believe.
Specifically I've long been fascinated with higher-order IR like Thorin (and its intended purposes is after all DSLs). I wonder if we could stick thorin before the MIR, and then do any or all of the fancy things in the preceding paragraph on top of Thorin. the fancy features and this extra pipeline stage would be opt-in per function (at least to start). We would then supercompile the hell out of the Thorin higher-order CFG until one gets a single first-order CFG to trivially lower into a single MIR function. Because all of this happens before borrow checking, closures and their worst nemesis are kept apart. Now this a huge loss for the sane composability of any abstractions used within the function, and the resulting borrow errors would be quite far removed from the surface syntax, but if everyone is inlining in their brains anyway, maybe those won't be quite so bad :).
Ah this would be before the closures are desugared to structs and impls—the closures would be become just more thorin CFG along with functions that they are passed to. [Closures that are returned wouldn’t be inlined, but an Fn or FnMut that was used locally and then returned could be inlined in its local use-sites.]