Bastion of the Turbofish (2020)
github.com
github.com
Ordering the events on @varkor's attempts to eliminate the turbofish:
* RFC 2527
* Bastion of the Turbofish
* RFC 2544
If you'd like to know why Rust really still has a turbofish, the huge RFC 2544 thread has the full story: https://github.com/rust-lang/rfcs/pull/2544
Important points in that discussion (I'm only linking to the comments summarizing other comments):
* Sep 2018: Lang team rejects the change for 2018 edition due to lack of time: https://github.com/rust-lang/rfcs/pull/2544#issuecomment-423...
* Jan 2019: Lang team proposes to merge the RFC (accept the change): https://github.com/rust-lang/rfcs/pull/2544#issuecomment-453...
* A bunch of people objected, people can't agree on subjective points: https://github.com/rust-lang/rfcs/pull/2544#issuecomment-453...
* The discussion got derailed and closed as too heated: https://github.com/rust-lang/rfcs/pull/2544#issuecomment-453...
And that's where the project to eliminate the turbofish died. Nobody in the know wants to deal with that much drama, so AFAIK no one has tried again.
New keywords are a relatively minor inconvenience so long as there aren't too many.
New keywords are small breaking changes; I'm saying that the breakage when eliminating the turbofish is even smaller!
If you read RFC 2544, some team members were considering removing the turbofish without a new edition, because a crater run did not find any real-world code that would break. The "Bastion of the Turbofish" might literally be the only Rust code in existence that would be broken.
Vec::<u8>::new()> let x: Vec<u8> = Vec::new();
let x: Vec<u8> = vec![]; // spelled Vec<u8> in "type context"
let x = Vec::<u8>::new(); // spelled Vec::<u8> in "expression context" #include <Eigen/Dense> // a library where I know it's required
using namespace Eigen;
int main() {
VectorXd zeros = VectorXd::zeros(5);
VectorXf zeros_float = zeros.template cast<float>();
return 0;
}C++ compilers used to be liberal about accepting unnecessary instances of the `typename` keyword so lots of people would just sprinkle them everywhere. Modern versions of both Clang and GCC call this out.
The actual rule: https://en.cppreference.com/w/cpp/language/dependent_name#Th...
And i came from being a Python "kill all syntax!" zealot back in the day, hah. Funny how in both Types and Syntax i have migrated from minimalist to explicitness over the years. I was such a zealot on both fronts in those Python days.
fn a[T](_: T) -> &'static str { "type" }
struct b;
fn ambiguous() -> &'static str {
let a = [|_: b| "expression"];
let b = 0;
let c = b;
a[b](c) // "type" or "expression"?
}
So you very probably want to unify indexing and function-calling, which becomes very difficult for Rust because of the whole lvalue/rvalue situation.Because manual implementation of the Fn traits is unstable and no generic implementations exist, I do actually think that this could technically be done (a new edition could go to [] for generics sans turbofish), but it covers a lot of other hazardous ground too (e.g. adding maybe-uninitialised reference types) so that it couldn’t be done for still some years, and I doubt very much that it ever will be (because I doubt many people think it would be worthwhile even if it was free to do, let alone when there’s major implementation cost).
I and a few others did try a bit to convince people to shift to square bracket generics in late 2013–mid‐2014, but failed. Certainly the language was in no shape to unify indexing and calling (the best I could come up with was to keep them separate for now, just using the same syntax with the compiler selecting between the mutually-exclusive implementations, with the potential for merging the two in future), so at that time it would probably have retained the turbofish, which didn’t help with convincing people of the superiority of square brackets (no compelling benefit—switching to something unfamiliar, and remember Rust’s weirdness budget was already about expended, without fixing what many felt was the biggest problem with angle brackets).
That’s not entirely true: with [] your parse is unambiguous, the node contains everything up to the matching ], doesn’t matter whether it’s an index, a literal, a generic, … the resolution of what’s what requires more work, but you can at least build your tree.
With <> however you can’t even parse without knowing whether < is a comparison operator because the knowledge of where its rhs ends depends on that information. This issue is compounded by the right-shift problem.
If anything, I’d say that this is much more trivially problematic than angle brackets, because on angle brackets you have to write quite convoluted code before the actual ambiguity arises; with square brackets, syntactic ambiguity arises much more easily and commonly—I can think immediately of a few examples of code that I’ve seen or written that would be ambiguous.
Some languages have been willing to compromise on this (C and C++ certainly do, and Perl is famously unparseable), but it always causes a great deal of pain, as you can’t build any sort of isolated tooling around the language any more, but must use an actual compiler to even try to parse the thing properly if you want to be robust; static analysis is thus made more difficult, especially as you must now look at entire projects and can’t properly look at individual files—and worse, you must be able to compile the project to be able to analyse it.
So Rust has deliberately gone the other way: you can parse the language easily and completely, so any form of static analysis doesn’t need a real compiler to back it and can act on files in isolation.
In short, Rust is philosophically obdurately opposed to needing to do disambiguation in the parsing phase.
So it’s actually not really about the actual ambiguity, which might not even be a thing—I can’t remember whether {function + type parameter} would be looking up in the type or value namespaces, so my example might actually be wrong there—but rather that you can devise supporting code that would seem to make `a[b](c)` mean two different things, and that’s untenable for Rust, because it refuses to let scope contents influence the parse.
fn a<T>(_: T) -> i32 { 0 }
fn main() {
let a = |_: i32| 1;
let c = 0;
a(c);
}
except that a[b](c) requires that an expression can be a type.And semantically they’re quite different, as is always the case with types versus values: one is a function call with generic type parameters, which are resolved at compile time, while the other is doing indexing at run time, and then calling it. For them to have the same AST, even were it possible, would be making the AST hollow.
Anyone care to spell it out for me?
WOW that's certainly a `wat`
EDIT: ok - now that I look at it, I understand. My brain was very, very primed to see the <woe, is> as type parameters - and it's late
https://www.destroyallsoftware.com/talks/wat
Just some context.
fn main() {
let (oh, woe, is, me) = ("the", "Turbofish", "remains", "undefeated");
let _: (bool, bool) = (oh<woe, is>(me));
}
So we have four strings, one named "oh", one named "woe", one named "is" and one named "me". (varA, varB)
is a tuple in Rust that contains two variables, named varA and varB. oh < woe
Is checking to see if "oh" is less than "woe" is > (me)
is checking to see if "is" is greater than "me" (ignore the extra parentheses.So:
(oh<woe, is>(me))
with some more spacing to make it easier to see, and with the extra parentheses removed is: (oh < woe, is > me)
Basically it creates a tuple with the result of the above comparisons.A full explanation of the ambiguity is in the linked issue:
let a = (b<c, d>(e));
This can either mean a tuple
let a = ((b < c), (d > e));
Or a pair of generic arguments.
let a = b::<c, d>(e);Just to elaborate (not troll!) I believe C with tools[1] makes more sense economically than Rust. Rust is popular not because it's groundbreaking but because it's fun.
[1] Such as: https://compcert.org/ https://frama-c.com/ https://www.cprover.org/cbmc/
Then there's the problem of where to put the lifetime parameters.
Those are really common terms which have existed long before Rust though. And if you don't know them already, they are easily googleable. The documentation also assumes the reader know what heap and stack are but surprisingly (not) it doesn't bother you.
> The compiler has opinions about what should be written in each form.
Those are mere lints though. If you don't like compiler's opinion on that subject, you can disable them (with `#[allow(non_camel_case_types)]` for instance, see https://play.rust-lang.org/?version=stable&mode=debug&editio...))
> Then there's the problem of where to put the lifetime parameters.
Lifetimes are the one novel concept Rust introduced, then obviously it feels alien at first, because it is. But you end up getting used to it.
Not sure if it counts as documentation, but the Rust Book actually has a very good explanation of stack and heap. I know because that's how I learnt about these concepts myself.
And, if you're just starting out, is Rust really more alien than C?
C, quite famously, needs you to understand at least double indirection of pointers (pointer to pointer of type) to be even mildly useful in the language. You can go a long way in Rust before needing to hit that level of complication.
I still wouldn't recommend C or Rust for a first programming language, but Rust really isn't that bad compared to the alternatives.
> Rust code uses snake case as the conventional style for function and variable names. In snake case, all letters are lowercase and underscores separate words.
... how is that assuming? I will agree that the camel case explanation is much shorter: https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in...
> Rust’s type-naming convention is CamelCase.
By Chapter 10 you’ll have seen way more code already.