4 ms·
That sounds superficially reasonable to me and I'm all for regularity in programming language semantics but on thinking about it further, I actually think it's
by derriz 1y ago
That sounds superficially reasonable to me and I'm all for regularity in programming language semantics but on thinking about it further, I actually think it's a design flaw.
It makes no more sense to me for "return <expr>" to have a type than it does to make "if <expr>" or "break" or "{" or any other keyword to have a type. These are syntactic elements.
Rust's type system is clearly inspired by Hindley-Milner and most languages using such a type system either don't even have a return keyword.
Even if you disagree with this argument, this design decision has resulted in all these weird/confusing but absolutely useless code examples and there is no upside that I can see to this decision in terms of language ergonomics. What practical value is it to users to allow "return <expr>" to itself be an expression? That you can use such an "expression" as arguments to function calls with hilarious wtf consequences? It's a piece of syntactic sugar.
- steveklabnik 1y agoRespectfully, "it makes no sense to me" isn't an argument. if and break both have types in Rust as well. > don't even have a return keyword. This is because they are not procedural languages, it has nothing to do with the type system. > there is no upside that I can see to this decision in terms of language ergonomics. There's tremendous upside! That's why lots of languages choose this. For example, there is no need for the ternary in Rust: if can just do that. > What practical value is it to users to allow "return <expr>" to itself be an expression? Code like this just works: let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => return, }; That is, if return wasn't an expression, we'd have a type error: the two arms would have incompatible types.
- derriz 1y agoSee my comment above, your example only "just works" if the enclosing function has the appropriate return type (in this case none). So the syntactic element "return" is not just an expression - unlike other sub-expressions, it involves action at a distance - i.e. it must not just agree with it's context as part of an expression but it must agree with the enclosing fn signature.
- steveklabnik 1y agoI replied over there, let's keep it to that sub-tree so we both don't have to duplicate comments :)
- nine_k 1y agoThis makes sense because the `match` returns a union of u32 and `never`. Assigning values of expressions that are purely `never`, or having values that are purely `never` or `()` as the condition in a conditional operator, should be marked as an error, like unreachable code.
- NobodyNada 1y agoIn practice, it's quite a useful feature, because you can write things like this: let day_number = match name { "Sunday" => 0, "Monday" => 1, "Tuesday" => 2, "Wednesday" => 3, "Thursday" => 4, "Friday" => 5, "Saturday" => 6, _ => return Err("invalid day") };
- bobbylarrybobby 1y agoThe issue with `return expr` not having a type is that you lose the ability to write something like let y = match option { Some(x) => x, None => return Err("whoops!"), }; Without a type, the None branch loses the ability to unify with the Some branch. Now you could say that Rust should just only require branches’ types to unify when all of them have a type, but the ! never type accomplishes that goal just fine.
- derriz 1y agoI'm responding here because so many replies are making the same point. In your particular example, let's put your example into a context. Is fn foo(option: Option<i32>) -> i32 { let y = match option { Some(x) => x, None => return Err("whoops!"), }; return 1; } well typed? It should be if we are to believe that "return <expr>" is an expression of type () - but, naturally, it causes a compilation error because the compiler specifically treats "return <expr>" unlike other expressions. So there is no improvement in regularity, while it admits all sorts of incomprehensible "puzzlers". I don't see why you'd lose this ability if you removed the claim that "return <expr>" is itself an expression. Most/many languages have mechanisms to allow expressions to affect flow control - e.g. with exceptions, yield, etc. - which do not these constructs (for example "throw x") to have a type. Rust could just as easily supported the syntax you use above without making "return <expr>" a tapeable expression.
- steveklabnik 1y ago> Is ... well typed? It's not, but not due to the return, it's because you're trying to return a Result from a function that returns an i32. This works: fn foo(option: Option<i32>) -> Result<i32, &'static str> { let y = match option { Some(x) => x, None => return Err("whoops!"), }; return Ok(1); } > It should be if we are to believe that "return <expr>" is an expression of type () It is not, it is an expression of type !. This type unifies with every other type, so the overall type of y is i32. return is not treated in a special way. > if you removed the claim that "return <expr>" is itself an expression This code would no longer work, because blocks that end in an expression evaluate to (), and so you would get the divergent, not well typed error, because one arm is i32 and the other is ().
- deathanatos 1y agoWe can use match to do pattern matching: let name = match color_code { 0 => "red", 1 => "blue", 2 => "green", _ => "unknown", }; The RHS of the `=>` has to be an expression, since we're assigning it to a variable. Here, you should already see one "useful" side-effect of what you're calling "syntactic elements" (I'd perhaps call them "block statements", which I think is closer to the spirit of what you're saying.) The whole `match … {}` in the example above here is an expression (we assign the evaluation of it to a variable). > What practical value is it to users to allow "return <expr>" to itself be an expression? Now, what if I need to return an error? let name = match color_code { 0 => "red", 1 => "blue", 2 => "green", _ => return Err("unknown color"), }; The expression arms need to be the same type (or what is the type of `name`?). So now the type of the last branch is !. (Which as you hopefully learned from TFA, coerces to any type, here, to &str.) There's more ways this "block statements are actually expressions" is useful. The need not be a ternary operator / keyword (like C, C++, Python, JS, etc.): let x = if cond { a } else { b }; In fact, if you're familiar with JavaScript, there I want this pattern, but it is not to be had: const x; // but x's value will depend on a computation: // This is illegal. if(foo) { x = 3; } else { x = 4; } // It's doable, but ugly: const x = (function() { if(foo) { return 3; } else { return 4; }})(); // (Yes, you can do this example with a ternary. // Imagine the if branches are a bit more complicated than a ternary, // e.g., like 2 statements.) Similarly, loops can return a value, and that's a useful pattern sometimes: let x = loop { // e.g., find a value in a datastructure. Compute something. Etc. if all_done { break result; } }; And blocks: let x = { // compute x; intermediate variables are properly scoped // & cleaned up at block close. // // There's also a slight visual benefit of "here we compute x" is // pretty clearly denoted. }; > Even if you disagree with this argument, this design decision has resulted in all these weird/confusing but absolutely useless code examples I think one can cook up weird code examples in any language.
- wredcoll 1y agoI appreciate, so much, that rust is slowly evolving into perl.
- 1y ago
- dathinab 1y agoit doesn't need to make sense on a higher abstraction level of logic/semantics I mean you don't see any of the nonsense in the blog post in any realistic PR (so they don't matter), but you would run into subtle edge case issues if some expressions where more special then other expressions (so that does matter), especially in context of macros/proc macros or partial "in-progress" code changes (which is also why `use` allows some "strange" {-brace usage or why a lot of things allow optional trailing `,` all of that makes auto code gen simpler).
- pornel 1y agoThis makes the language more uniform. Instead of having a special ternary ?: operator for if-else in expression position, you have one syntax everywhere. It makes generic code work without need to add exceptions for "syntactic elements". You can have methods like `map(callback)` that take a generic `fn() -> T` and pass through `T`. This can work uniformly for functions that do return values as well as for functions that just have `return;`. Having nothingness as a real type makes it just work using one set of rules for types, rather than having rules for real types plus exceptions for "syntactic elements".
- deleted 1y ago[deleted]