There are very few statements: https://doc.rust-lang.org/stable/reference/statements.html
and a lot of expressions: https://doc.rust-lang.org/stable/reference/expressions.html
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.
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)
There's issues around doing this with mutable collections (i.e. the value restriction) but that's not what you're referring to...
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_...)
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.
This is obviously only possible if the list itself has no elements, and indeed a simple proof is that the statement above is valid for the empty type: all elements of a list of type List a have type empty (among other types). Thus there are no elements in this list.
And both Haskell or OCaml can prove it:
type empty = | (* this is defining a never type *)
type polymorphic_list = { l: 'a. 'a list }
(* OCaml require to explicit construct polymorphic type *)
let polymorphic_lists_are_empty ({l} : polymorphic_list ) =
match (l:empty list) with
| [] -> () (* this is the empty list *)
| _ -> .
(* this clause requires to the OCaml typechecker to prove that the remaining cases are unreachable *)Indeed, I stand corrected. Interesting! I'll look into that a bit more, thank you.
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.
[1]: https://leanprover-community.github.io/mathlib4_docs/Init/Pr...
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.
enum Never {
}
Languages like OCaml, Haskell as well as Rust have types with no values (called uninhabited types)Return is a statement in the minds of most programmers, but an expression in the language. That was a very pragmatic decision that required an unintuitive implementation. As a result, we've got this post full of code that is valid to the compiler but doesn't make a lick of sense to most programmers reading it.
I would take issue with this, sure, for a lot of people, they may be bringing assumptions over from languages where assignment is a statement. That doesn't make them correct.
> required an unintuitive implementation
To some people, sure. To others, it is not unintuitive. It's very regular, and people who get used to "everything is an expression" languages tend to prefer it, I've found.
This feels awkward as my mental model is that in "everything is an expression" languages you simply DO NOT offer "return" (and, if you do, it must be mapped to bottom and do something insane like throw an exception... but, like, if you are really used to using such a language, you'd never let yourself type a "return", as the entire concept feels icky and wrong in such a language).
It is of course not statically typed.
I.e., if we bias our sample to the data points proving our point then our point is proven. It's like that quip about how every car insurance company can simultaneously claim "people who switched saved hundreds of dollars in average."
I also like "everything is an expression" languages, but I don't think that's a fantastic argument.
A better question at this point, arguably, is why there should even be an expression/statement distinction in the first place. All imperative statements can be reasonably and sensibly represented as expressions that produce either () or "never". Semicolon then is just a sequencing operator, like comma in C++.
In fact, I recently ran into the finding that you can't use it with logical operators either: `return myBool && throw...` doesn't work. I assume that's because && can be used with many types even if the first operand is a bool, but the compiler error message doesn't explain that, it just says throw is an invalid token here, and if you parenthesize it, it says a throw expression can't be used in this context. I was very surprised by this seemingly arbitrary limitation.
The main difference from other MLs is the lack of higher kinded types, so it’s difficult to express things like Functor, Monad, Arrow, etc
I wouldn't call using a uninhabited type for the type of a return expression theoretically inelegant. On the contrary, I find it quite pleasing.