3 ms·
We're speaking past each other since there's "expression" as defined in the Rust specification vs "expression" as in ordinary computer science, and Rust's use o
by AIPedant 1y ago
We're speaking past each other since there's "expression" as defined in the Rust specification vs "expression" as in ordinary computer science, and Rust's use of return is certainly not an expression in the latter sense. It is shoehorned into being called an expression but it has no semantically meaningful type, it is an effect. A type is (carefully but somewhat arbitrarily) assigned to it, which is why some of those examples involving "return" are particularly goofy. It is not material for most programs since it only comes up with intentional misuse of the keyword. But "return" does not make sense in functional languages with true first-class expressions - functions don't return values, they get evaluated and the frame destruction / etc are all abstracted away. It makes sense in Rust because expressions in the CS sense of the term are ultimately not first class.
- steveklabnik 1y agoI do think we're speaking past each other. I don't fully agree with your "CS sense of the term," as Rust does have a semantically meaningful type: !. This is all pretty bog-standard stuff. Rust isn't doing anything weird or novel here.
- rtpg 1y agoI do wonder how many languages have the "never returns" type explicitly available. Typescript and Rust.... Haskell has bottom but I wonder semantically how much space there is between bottom and "never return". Obviously laziness makes things weird. This is what I find interesting in this generation of languages though. Any C programmer understands the notion of an infinite loop, and the value of conditional expressions like ternary ops. But now languages are realizing that when you start treating more and more things as expressions, you really want to start giving names to things that you wouldn't name in the past.
- kuschku 1y agoKotlin also has this type, it's called "Nothing". https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-nothing/ https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-nothin... As you can never get a value of type nothing, it can coerce into anything, just like rust's ! or () or typescripts never.
- nialv7 1y agoI recently realized (by playing with Lean) that coercing of "!" is because of the principle of explosion [1]. Basically propositions are types in Lean, and proofs are instances of those types. A proposition that is false doesn't have any instances, so they are like "!". Principle of explosion says ∀ P, False -> P, which is exactly the type signature for "!" coercing. [1]: https://leanprover-community.github.io/mathlib4_docs/Init/Prelude.html#False.elim https://leanprover-community.github.io/mathlib4_docs/Init/Pr...
- valenterry 1y agoScala and Haskell are there and I think they inspired this in Kotlin and Rust. In Haskell it's "bottom" and in Scala it's "Nothing". In Scala no one uses "return" (mostly because we don't care about performance in the same way), but if you do, the way it is internally implemented is by throwing exceptions, so in a sense it suffers from the same problems as Rust. It's actually very important to have that type in a language that uses immutable collections. Imagine this pseudocode: // List() creates an immutable list let emptyList = List() let listWithAnInteger = emptyList.add(42) let listWithAString = emptyList.add("foo") This works in Scala. But how can the compiler know that `emptyList.add(42)` is allowed? After all, you can only add things to a list where the added element matches the type of the other elements right? The reason this works is because the type of emptyList will be List<Nothing> and since Nothing a subtype of every other type, the type of listWithAnInteger will become List<Integer>. You can annotate these types explicitly if you want. Every language without such a bottom type has a failed type-system in my opinion. (looking at you Golang and many others)
- wk_end 1y ago? This works fine in a type system without an explicit bottom type. In Haskell or ML or whatever `emptyList` would be given a polymorphic type, `List a` or `'a list`. There's issues around doing this with mutable collections (i.e. the value restriction) but that's not what you're referring to...
- valenterry 1y ago> In Haskell or ML or whatever `emptyList` would be given a polymorphic type, `List a` or `'a list`. That alone would not work. Think about it: `List a` means "A list that contains values of type `a` and `a` can be any type whatsoever". Now imagine you combine that list with a list of integers. That obviously cannot work, since `(++) :: [a] -> [a] -> [a]` as you see, the types must align. The way Haskell fixes that is (apparently) by doing something called `Let-generalisation` (https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/let_generalisation.html https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/let_...) To me that feels like hacky way to exactly resolve the problem that I described, and if you turn it off then that code would stop working and fail to compile as expected.
- int_19h 1y agoPure functional languages have the equivalent of "never" - it's the bottom type. Indeed, the return type of `error` in Haskell is that, but also cases like predictable infinite recursion. But this semantics works great for cases like "return" and other forms of control transfer - the expression in which they appear also "never finishes" (but some other expression which contains that one as a subexpression does). Now, yes, ideally you'd have effects in the type system so that you can express this kind of stuff with more precision. But if you restrict this to stuff like return/break/continue where the destination is statically known and can be validated, you can treat those effect types as been there, just inferred for all expressions and forbidden to cross the function boundary. For exceptions specifically this trick no longer works because the whole point is for them to cross that boundary. But the amount of complexity this stuff adds to typing even trivial generic code is arguably too much for practical use (see also: checked exceptions in Java). In any case, in Rust you use Result types instead so those exceptions produce regular values. And although panics can be handled, they are certainly not meant to be used as a generic mechanism for transfer of control, so adding effect types for them alone is just not worth it.
- octachron 1y agoReturn (or other effects) does make sense as an expression in a functional language. Typically, OCaml has `raise Exception` which is also an expression, with the same type as `return` or any never returning function. And exceptions can also be used to implement a user-defined `return` function.
- nextaccountic 1y agoReturn has the ! type. It's a type with no values, similar to an enum without variants, like this: enum Never { } Languages like OCaml, Haskell as well as Rust have types with no values (called uninhabited types)