> So... a three-character capture list is ugly
IMO, it's 'ugly' not because "[=]" is inherently syntactically ugly, but because of all the variations of capture lists and the subtlety of the differences.
> but that extra impl clause is... fine?
It's not great - in fact, 'impl Trait' is a pretty subtle feature as well. But I think it's more principled.
> would you at least agree that (relative to python) both language have significantly more verbose syntax
Yes…
> owing to the interaction between closures and the languages data models?
…Not really. I'll elaborate later in the post.
> so if you want to pass one around you need to specify its whole type signature twice, once in the expression itself and then again in the signature of the function that will receive or return it.
No, you don't. Generally speaking, it needs to be specified at most once, sometimes zero times. In my example:
fn adder(amount: u32) -> impl Fn(u32) -> u32 {
move |x| x + amount
}
the type is only written once. Admittedly, the expression has its own list of arguments ("|x|" versus "(u32)" in the type), but in the expression, only the names are specified, not types.
The caller of `adder` generally wouldn't need to specify the type:
let a = adder(1);
println!("{}", a(2)); // 4
…but if it's stored in a struct or passed across additional function boundaries, it may have to be repeated.
When would it have to be specified zero times? Well, for one case, if there aren't any function boundaries involved:
let amount = 1;
let adder = |x| x + amount;
println!("{}", adder(amount));
But also, higher-order functions are often defined once and used many times. For example, the `map` method on Iterator is defined in the standard library with a full (generic) type signature, but I don't need to declare any types to use it:
let v = Vec::from_iter(vec![2, 3, 4].iter().map(|x| x + 2)));
println!("{:?}", v);
Admittedly there's a lot more noise there in general than Lisp or Python, but a lot of that is a desire to be explicit about allocations, not directly related to anonymous functions.
FWIW, not doing type inference across across function boundaries is a semi-artificial limitation. Haskell can do it just fine despite being statically typed, and although Haskell has very different implementation strategies, that's not related to type inference. C++ now sort of does it with `auto`, but only does 'forward reasoning' - i.e. it can deduce the type of an operation's result from the types of its operands, but not vice versa. Rust has true type inference, but it eschews global type inference (across functions) mainly because of a tendency to cause hairy/confusing errors in larger programs. (There are also compilation time concerns, but they're not the primary reason AFAIK.) I suppose you can say that dynamically typed languages are easier to understand when they go wrong, compared to hairy Haskell errors - but I'm not sure to what extent that's actually true, as opposed to functional programmers just tending to use more complex types.
By the way, C++(14) actually doesn't require the lambda argument type to be specified in this case. This works fine:
auto adder(int amount) {
return [=](auto x){ return x + amount; };
}
(but not because of type inference; rather, the returned closure implements operator() for any argument. Also, 'amount' can't be 'auto', which is also an arbitrary limitation, but the rationale is pretty confusing to me considering that the return type can be 'auto'.)