Show HN: We launched an AI builders podcast (builtthisweek.com)
2 points by Jmetz1 1h ago 1 comments
Show HN: Zenta – Mindfulness for Terminal Users (github.com)
171 points by ihiep 14h ago 33 comments
Weird Expressions in Rust
133 lukastyrychtr 104 6/27/2025, 3:08:26 PM wakunguma.com ↗
To understand what evil_lincoln is doing, you have to understand very old Rust. Here's the commit that introduced it: https://github.com/rust-lang/rust/commit/664b0ad3fcead4fe4d2...
log was a keyword to print stuff to the screen. Hence the joke, https://en.wikipedia.org/wiki/Lincoln_Logs Now that log is the println! macro, the joke is lost.It doesn't say explicitly why this is "weird", but given some other comments in the file,
I am assuming that using the return value of log was buggy, and so this tested that you could save it in a variable. I don't remember the exact semantics of log, but if it's like println!, it returns (), which is useless, so binding it to a variable is something you'd never write in real code, so it's "weird" in that sense.This would be something the Boomer generation grew up with, and I think maybe the previous generation too. They're still around but they've certainly faded; they used to be Lego-level popular kids toys back then. They are named after President Lincoln, but only as a marketing tactic to use some of his reputation, there's no real connection.
I would imagine even some native English speakers are learning something with this post. I haven't seen them in a while.
> there's no real connection
Funny--I always thought it was meant to be a pun on linkin', as in you're linkin' the logs together because they have those slots that fit precisely together on the ends.
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
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.
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.
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++.
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.
[1]: https://wiki.haskell.org/Bottom
[2]: https://en.wikipedia.org/wiki/Bottom_type
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.
> 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:
That is, if return wasn't an expression, we'd have a type error: the two arms would have incompatible types.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.
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.
In your particular example, let's put your example into a context. Is
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.
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:
> 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 ().
"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."
That's exactly my point. "return <expr>" is not just an expression which can be typed. If you tell me the types of all the identifiers used, I can look at any expression in Rust which does not include a return, and tell you if it's well typed or not. If the expression includes a return, then I cannot tell you whether the expression is well-formed.
Yes, it is, and it can. It has the type !, no matter the type of <expr>.
For any expression NOT involving "return", I can write, for example:
const Z = <expr>
but I cannot if <expr> contains a return embedded somewhere. The existence of a "return" somewhere in an expression changes the character of the entire expression.
I.e. there are two classes of "expressions". Those NOT containing returns (which are equivalent to the notion of "expression" in the languages that Rust was inspired by) and those containing a return somewhere in them which are subject to further rules about wellformedness.
My point is that none of this is necessary at all - you don't need to provide type rules for every lexical feature of your language to have a language with a powerful expressive type system (like Rust's).
And this isn't really any different from variable references, if you think about it. If you have an expression (x + 1), you can only use it somewhere where there's an `x` in scope. Similarly, you can only use `return` somewhere where there's a function to return from in scope. Indeed, you could even make this explicit when designing the language! A function definition already introduces implicit let-definitions for all arguments in the body. Imagine if we redefined it such that it also introduces "return" as a local, i.e. given:
the body of the function is written as if it had these lines prepended: where "return" is a function that does the same thing as the statement. And similarly for break/continue and loops.The thing that actually makes these different from real variables is that they cannot be passed around as first-class values (e.g. having the function pass its "return" to another function that it calls). Although this could in fact be done, and with Rust lifetime annotations it would even be statically verifiable.
Also, the type of return is a separate matter from the type of the thing being returned. You obviously can't return Result from a function returning i32. The point of type coercion is that you can yield `return Err(...)` in one branch of a match and have it type check with the other branch(es).
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".
> 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?
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.):
In fact, if you're familiar with JavaScript, there I want this pattern, but it is not to be had: Similarly, loops can return a value, and that's a useful pattern sometimes: And blocks: > Even if you disagree with this argument, this design decision has resulted in all these weird/confusing but absolutely useless code examplesI think one can cook up weird code examples in any language.
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).
Syntax alone can't stop sufficiently determined fools. Lisp has famously simple syntax, but can easily be written in an incomprehensible way. Assembly languages have very restrictive syntax, but that doesn't make them easy to comprehend.
Rust already has a pretty strong type system and tons of lints that stop more bad programs than many other languages.
"Expressibility" and "expressive power" are vague and subjective, so it's not clear what you mean.
I suppose you object to orthogonality in the syntax? Golang and Java definitely lack it.
But you also mention C in the context of "maximum possible flexibility"? There's barely any in there. I can only agree it has mistakes for others to learn from.
There's hardly any commonality between the languages you list. C# keeps adding clever syntax sugar, while Go officially gave up on removing its noisiest boilerplate.
D has fun stuff like UFCS, template metaprogramming, string mixins, lambdas — enough to create "incomprehensible" code if you wanted to.
You're talking about modern languages vs relics of the past, but all the languages you mention are older than Rust.
Rust does not claim to be particularly security-focused, only memory safe.
Also, this means that you'd consider any expression-based language to be inherently a security problem.
Rust is not written as a pure expression based language. And as we all know very well from the experience with C and JS, any unexpected and weird looking code has the potential to hide great harm. Allowing programmers to stray too much from expected idioms is dangerous.
It’s not purely expression based but it is very close to it, there’s only a few kinds of statements, the vast majority of things are expressions.
The submission shows weird program snippets. I don’t think it shows weird snippets that can also easily hide bugs?
But also, all examples in TFA are very artificial convoluted code. Meaning that you can write things like these just like you can write something like &&&...x - but why would you? Actual real-world uses of this feature are all quite readable.
You can have UB in "safe rust".
https://github.com/Speykious/cve-rs
You can even disable the Type check, trait check and borrow check in "safe rust"
And all of this is unsound.
https://users.rust-lang.org/t/i-finally-found-the-cheat-code...
The reason you want return to be coercible to any type is so that you can write something like
And you pick the return value of ! because return never actually produces a value that is propagated on at runtime, it immediately exits the function.(Note this all works even with returning a value)
That '!' type seemed weird in the first few examples but starts to make sense later on.
It's essentially a "pseudo type" for everything that is syntactically an expression, but will never return anything, because evaluating it causes the entire statement to be canceled.
Is that correct?
It's also useful in more places than return expressions -- for example, you can make a function return ! to indicate that it's a non-returning function, which is useful for expressing, say, an error handler that must crash the program; or a main loop that must never return. It also can help the compiler generate more compact code when a function is known to not return.
There's currently work in progress to allow you to specify ! as a type everywhere, not just as function returns. This is useful where some generic code expects a function to return a Result with an implementation-specified error type, since an infallible implementation can specify ! as the error type. Then, the type checker can allow the programmer to unwrap a Result<T, !> without checking for errors, and the optimizer can remove the error-checking branches from generic code: https://doc.rust-lang.org/std/primitive.never.html
This has taken a very long time to implement, because of some very subtle implications on type inference that made it difficult to stabilize without breaking compatibility -- but the 2024 edition finally figured out a way to make it possible.
Which might make more sense when you remember that the only statements in Rust are various declarations (`let`, `type`, `fn` etc) and macro invocations. Everything else is an "expression statement", including blocks and loops. Thus you can do stuff like:
Note that `break` never leaves the let-statement here - it just terminates the loop expression and forces it to yield a value (`break` without arguments yields (), and ditto for loops without break).You can also break out of regular blocks if they are labelled and you use the labelled form of break:
This all can very easily lead to convoluted code if not used sparingly, but sometimes a mutating loop with mutable data encapsulated within and a break to yield it once the computation is complete is genuinely the most straightforward way to write something.Rust programs that give unintuitive outputs or compile errors.
Most contextual keywords in other languages come from either:
1. Features that were added after the language was in wide use and can't add keywords without breaking existing code.
2. Features where the word is particularly useful elsewhere, so would be painful to reserve (like `get` and `set` in Dart).
But neither of those seem to apply to Rust. As far as I know, it's always had ML-style unions, and "union" doesn't seem to be a particularly useful identifier otherwise.
Why isn't `union` fully reserved?
[1] https://doc.rust-lang.org/stable/std/collections/struct.Hash...
https://rust-lang.github.io/rfcs/1444-union.html#contextual-...
if you extend it to the most cursed ~6 lines of code, you really can obfuscate what you're doing in a way that's fiendishly hard to debug.
Here's my favourite on that theme, which I was missing from the list:
Seriously though, I love "abusing" programming languages in unexpected ways. My favorite so far is: https://evuez.net/posts/cursed-elixir.html. Reading this made me realize Elixir is literally macros all the way down, and it's a Lisp!
you still can create some more confusing things by idk. overloading some operators (but luckily not `=` and similar crazy C++ things) adding recursive macros and maybe combining it with lifetime variance and coercion edge cases, maybe sprinkle in some arcane `#[]` annotations and people with be very confused, more so then in the article
https://github.com/denysdovhan/wtfjs
https://github.com/satwikkansal/wtfpython
In Rust the return type of this function is the unit type, the empty tuple (). So, the variable has this type, there's no problem with this in Rust, even though some lesser languages can't handle the idea of a type this small.
Or did you mean a function which never returns, like std::process::exit ? In Rust this function's return type is ! the Never type, an empty type that you ordinarily can't name in stable Rust.
Because this type is empty, a variable of this type will evaporate, the compiler knows that we can't bring values into existence if there are no values of that type, the code paths in which this variable exists will never be executed, so no need to emit machine code.
In a language with generic programming like Rust this isn't an error, it's actually a convenience. We can write generic error handling code, and then for cases where there will never be an error our error handling code doesn't even compile, it evaporates entirely, yet for cases which can have actual errors, the error handling code is emitted.
because it doesn't compose well with generics, macros, proc-macros etc.
e.g. if you have this dump way to wrap clone: `fn foo<T: Clone>(v: T) -> (T, T) { let new = v.clone(); (v, new) }` it would implicitly not work with T = (), because then `v.clone()` would be a "function which returns nothing".
In isolation this might seem fine but if you compose abstractions you sooner or later run into an edge case where it isn't fine.
And this becomes even more a problem when macros/proc-macros are involved.
It also makes changing code easier, lets say you have something like `let x = foo(); dbg!(x);` and you change the return type it will keep compiling as long as the type implements `Debug`, even if you change the type to `()` (i.e. return nothing). And again for normal code that is a minor nit pick, but for macros, proc macros, generic code of sufficient complexity sooner or later you run into edge cases where it really matters that it's allowed. Not often but often enough.
Lastly and most importantly assigning `()` to a variable hurts no one, you won't see any such code in normal PRs.
So it it doesn't hurt anyone but can be quite use full in edge cases.
Lastly linters (mainly clippy) do warn or error for some of this nonsensical things, depending on the lint-set you enabled.