steveklabnik a day ago

This post is missing my favorite one!

    fn evil_lincoln() { let _evil = println!("lincoln"); }
What's weird about this?

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...

    fn evil_lincoln() {
        let evil <- log "lincoln";
    }
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,

    // FIXME: Doesn't compile
    //let _x = log true == (ret 0);
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.
  • mbStavola 20 hours ago

    A dog entered a tavern and said: "I cannot see anything, I'll open this one!"

  • ibotty a day ago

    What's the joke exactly? English is not my native language.

    • jerf a day ago

      https://www.basicfun.com/lincoln-logs/

      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.

      • drfuchs a day ago

        There’s a strong connection between President Lincoln and log cabins. He grew up in a series of log cabins, and this fact was widely known during his campaign.

      • ranguna a day ago

        Yes, but why is it evil?

        • Analemma_ a day ago

          I think that part is a reference to a Futurama episode where a holodeck malfunction materialized several villains, including "Evil Lincoln".

          • ranguna 7 hours ago

            Ah if that's the case, then that makes more sense. Thanks!

      • kelnos 10 hours ago

        They were still pretty common when I was a kid in the early '80s. genX'ers and older millennials born in the US are likely to know about them, or perhaps even have had a set of them (I did).

      • saghm a day ago

        I'm a late millennial, and I'd sometimes see them as a kid too. I'm not sure about more recent generations, but I think that they might have stuck around longer than you might think.

        • iforgotpassword a day ago

          I saw some kids in a park a few years ago in Beijing playing with those. First time I saw them. Didn't know the name until now though. :)

      • bennettnate5 a day ago

        > They were named after President Lincoln, but only as a marketing tactic

        > 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.

        • saghm a day ago

          I think it's both that and the popular tale of Lincoln having been born in a log cabin (which for some reason I thought I had heard wasn't actually true, but from looking into it now, it seems like a lot of sources say it is, so maybe I heard wrong?)

      • Timwi 13 hours ago

        > This would be something the Boomer generation grew up with

        ... in your country I assume. I've never heard of these, but from the looks of it, they look like an American version of Playmobil or Fisher Price.

  • rendaw a day ago

    What's the joke exactly? English is my native language.

    • steveklabnik a day ago

      log "lincoln" is a reference to the toy "lincoln logs"

  • san1927 13 hours ago

    love this one thats a really underrrated fact

b0a04gl a day ago

they exist because whole language built to treat expressions as firstclass citizens : blocks, ifs, matches, even macros as expressions that return values. so once you internalize that, all these weirdo one liners are artifacts. just artifact of a system where expressions compose infinitely. the syntax tree runs deeper than most people's habbits allow. you hit that depth and brain says this is wrong but compiler's allowing.

  • nine_k 25 minutes ago

    This is logically sound, but pragmatically not so. I wish the compiler could issue a warning or even an error if an expression of type `never` is used in a logical condition, like that of an `if`. While such cases might have rare legitimate uses (e.g. some edge cases of macro expansion, etc), I'd like it to be marked explicitly, similar to `unsafe`, e.g. with some `allow_never_as_condition` marker.

    Likely the same should apply to expressions of type `()`.

  • derriz a day 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 a day ago

      Respectfully, "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.
      • nine_k 22 minutes ago

        This 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.

      • derriz a day ago

        See 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 a day ago

          I replied over there, let's keep it to that sub-tree so we both don't have to duplicate comments :)

    • bobbylarrybobby a day ago

      The 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 a day ago

        I'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 a day 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 ().

          • derriz a day ago

            Sorry for the confusion - I meant to use ! and not ().

            "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.

            • steveklabnik a day ago

              > "return <expr>" is not just an expression which can be typed.

              Yes, it is, and it can. It has the type !, no matter the type of <expr>.

              • derriz 21 hours ago

                It only has type !, if the return type of the lexically enclosing function declaration has the same type as that of <expr>, otherwise it's illformed.

                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).

                • kelnos 9 hours ago

                  > For any expression NOT involving "return", I can write, for example:

                  > const Z = <expr>

                  > but I cannot if <expr> contains a return embedded somewhere.*

                  Sure, but that's not special about this case at all. I also can't write 'break' or 'continue' when I'm not inside a loop. When declaring a 'const', I am lexically not inside a function body, so I can't use 'return', which makes sense (the compiler will even tell you, "return statement outside of function body").

                  Particular statements being allowed in some contexts but not in others is entirely normal.

                  > My point is that none of this is necessary at all

                  Maybe it's not necessary, but I like the consistency this provides ("everything has a type"), and I imagine the implementation of the type checker/inferer is more straightforward this way.

                  Sure, you could define the language such that "a 'return' in a position that expects a typed expression will not affect other type that need to match with it" (or something else, in better, formal language). Or you can just define those statements to have the 'never' type, and not worry about it.

                  But ok, let's agree that it's not necessary. Then we're just talking about personal preferences, so there's no right or wrong here, and there's no point in arguing.

                • int_19h 20 hours ago

                  You can write it just fine if `const Z` is itself nested inside a function definition.

                  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:

                     fn foo(x: i32, y: i32) -> i32 {
                       ...
                     }
                  
                  the body of the function is written as if it had these lines prepended:

                     let x = ...;
                     let y = ...;
                     let return = ...;
                     ...
                  
                  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.

                  • kelnos 9 hours ago

                    > You can write it just fine if `const Z` is itself nested inside a function definition.

                    You can't, actually: 'const' is special in that it's not considered by the compiler to be inside a function definition, even if it is (and the compiler will tell you, "return statement outside of function body").

                    But that doesn't invalidate your point; in a way it supports it: 'return' can only be used in function contexts, just like 'continue' or 'break' can only be used in loop contexts.

                    • tialaramex 5 hours ago

                      You can do this:

                         const ONE: i32 = { const fn foolish() -> i32 { return 1 } foolish() };
                      
                      But yes, your larger point is exactly correct, the constant, even if it happens to be defined inside a function body, is not itself inside a function body and so we obviously can't return from it. It is also not inside an expression we can break out of (Rust allows you to break out of any expression, not just loops). It's a constant, like 5 is a constant, or 'Z' is a constant - this is not C or C++ where "const" means "actually a variable".
                • steveklabnik 21 hours ago

                  Okay, I think we are indeed talking past each other and I see what you are saying here. I am not sure that I agree, exactly, but I appreciate your point. I'm going to have to think about it a bit more.

            • Timwi 12 hours ago

              The same is true if `return` is a statement, so this doesn't seem to have anything to do with `return` being an expression.

        • bobbylarrybobby a day ago

          The type of return is !, not (). Meaning there are zero instances of this type (whereas there is one instance of ()). ! can coerce to any type.

          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).

        • rtpg 15 hours ago

          When discussing well typed-ness, and with more complex languages where you have weird undecideable components, you can end up with a notion of "Well typed" as follows:

          e: T is well typed _if_ the end result of e would be of type T

          (end result being hand-wave-y)

          It's not a guarantee that e is a value of a certain type, but a guarantee that if e is a value in the first place, then it will be a certain type. You sidestep having to prove the halting nature of e.

          This leaves a nice spot for computation that doesn't complete!

              let y = return 1
              f(y)
          
          y could be any type, and it's well typed, because you're never in a secnario where f(y) will be provided a value of the wrong type.

          Well-typed-ness, by my understanding in more complex type system, is not a guarantee of control flow, but a guarantee that _if_ we evaluate some expression, then it will be fine.

          And so... you can put `!` as a type in your system, treat return as an expression, and have a simpler semantic model, without really losing anything. Less moving parts, etc.... that's my read of it anyways.

        • efnx a day ago

          Well now we’re just talking about personal preferences, then.

    • NobodyNada a day ago

      In 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")
          };
    • pornel a day ago

      This 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".

    • deathanatos a day ago

      We 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 21 hours ago

        I appreciate, so much, that rust is slowly evolving into perl.

        • trealira 5 hours ago

          What Rust's syntax really reminds me of is Algol 68, or BLISS, both of them being these old procedural languages where everything is an expression. The "loop { ... break expr; ... }" thing reminds me of BLISS's "exitloop expr" construct.

    • dathinab a day ago

      it 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).

  • AIPedant a day ago

    Hmm my read is this is a slight overstatement - Rust was always built with the idea of expressions as first class citizens, but practicality and performance requires expression-breaking keywords like “return” which don’t fit neatly in an ML-ish language and have a few plain old hacks associated with implementing them (not “hack” as in lacking robustness; I mean theoretically/formally inelegant). Likewise there’s some stuff (u8) which is a simple syntax quirk. But a lot of these return/etc oddities are because Rust is ultimately an imperative language with strong influence from functional programming.

    • steveklabnik a day ago

      return is an expression in Rust, and it fits in well.

      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

      • AIPedant 21 hours 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 21 hours ago

          I 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 16 hours ago

            I 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.

            • valenterry 12 hours ago

              Scala 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 12 hours 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 10 hours 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_...)

                  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.

                  • octachron 8 hours ago

                    You are misreading the quantification, a value l of type List a means that for all type a, the element of the list has type a. In other words, this is an universal quantification whereas your interpretation is an existential quantification.

                    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 *)
            • kuschku 13 hours ago

              Kotlin also has this type, it's called "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 12 hours ago

                I 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/Pr...

        • int_19h 21 hours ago

          Pure 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 20 hours ago

          Return (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.

      • missinglugnut 21 hours ago

        Steve, I know you're an authority on the language but you've dismissed the point being made here without engaging with 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.

        • steveklabnik 21 hours ago

          > Return is a statement in the minds of most programmers

          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.

          • hansvm 20 hours ago

            > 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.

            • saghm 11 hours ago

              The original claim that the was responding to in this thread was that `return` as an expression didn't fit in well with Rust, and he said that it did. He also cited how far more things in Rust are expressions than statements, so it stands to reason that people who program in Rust are familiar with those styles of language. It sounds like you're arguing that it makes more sense to judge whether return makes sense as an expression in Rust based on the expectations of people who aren't as familiar with expression-based languages (and therefore aren't super familiar in Rust), which doesn't make a ton of sense to me.

        • int_19h 21 hours ago

          We've been going down this road for a long time now. E.g. "throw" is a (void-typed) expression in C++ already for similar reasons, although it doesn't go far enough without a proper bottom type. C# took it further and added the type so that you can write things like e.g. `x = y ?? throw new Error(...)`. There's no obvious reason why "return" should be conceptually different.

          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++.

          • Timwi 12 hours ago

            I considered whether to mention C# in this thread but initially decided against it because it doesn't actually have a bottom type. You can't assign a throw expression to an implicitly-typed variable, or anywhere else where it is needed to infer a type. You can only use it in places where a type is already known so the throw expression can be coerced to it.

            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.

    • efnx a day ago

      I’ve found rust to be an ML in C’s clothing.

      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

    • saghm 11 hours ago

      I'm confused about your parenthetical about "u8". What does the name of the unsigned 8-bit integer type have to do with whether the language is imperative?

    • GrantMoyer 21 hours ago

      Haskell has `bottom`[1] (see also [2]), which acts like Rust's `return` from a type checking perspective.

      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

  • throwawaymaths a day ago

    its not just that some things you would usually think are control flow are expressions, its also that there are unusual rules around coercing the `noreturn` type.

    • tialaramex 17 hours ago

      The only "unusual" rule here is that Rust offers the zero type addition, but does not provide the (much more complicated) other type additions

      So Rust does have: String + ! = String

      But Rust doesn't have: String + i32 = Either<String,i32>

      Note that the never type ! isn't special here, Rust will also cheerfully: String + Infallible = String or if you were to define your own empty type like so:

          enum MyEmptyType {} // MyEmptyType has no possible values
      
      Now under type arithmetic String + MyEmptyType = String and indeed that works in Rust.

      Edited: Syntax fix

  • gmueckl a day ago

    If a language that claims to be security focused is easily able to express constructs that human minds find barely comprehensible, or worse, then this is itself arguably a security issue: it's impossible to check the correctness of logic that is incomprehensible.

    • pornel a day ago

      What's the threat model? If you're reviewing untrusted or security-critical code and it's incomprehensible, for any reason, then it's a reject.

      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.

      • gmueckl a day ago

        Many modern language designers focus on shaping expressibility rather than providing the maximum possible flexibility because their designers learned from C, Lisp and other languages that made mistakes. Examples lamguages are Java, C#, D, Go... some arguably with more success than others. But language design that gave ultimate expressive power to the the programmer is a relic of the past.

        • pornel 20 hours ago

          ???

          "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.

          • gmueckl 18 hours ago

            Have you ever seen submissions to IOCCC or Underhanded C Code Contest? That is what too much syntactic flexibility looks like (if taken to the extreme).

            If you want your code to be secure, you need it to be correct. And in order for it to be correct, it needs to be comprehensible first. And that requires syntax and semantics devoid of weird surprises.

      • kelnos 9 hours ago

        Eh, I'm not sure I agree here. This feels sort of along the lines of, "well C is safe because someone can review your code and reject it if you try to dereference a possibly-NULL pointer".

        The point of a language that is "safe" along some axes is that it makes those unsafe things impossible to represent, either by omitting an unsafe feature entirely, or making it a compile-time error to do unsafe/unsound things.

        I will admit that this is something of a grey area, since we're talking about logic errors here and not (for example) memory-safety bugs. It's a bit muddier.

        In general, though, I do agree that people should write code that is reasonable to read, and if a reviewer thinks some code in a PR is incomprehensible, they should reject it.

        • pornel 7 hours ago

          I think these situations are very different, because Weird Rust affects only weird code, while unsafety of C affects regular C code.

          The difficulty in reviewing pointer dereferences is in reasoning about potential program's states and necessary preconditions, which C won't do for you. You can have neatly written C using very simple syntax, and still have no idea if it's safe or not. Solving that lack of clarity requires much than syntax-level changes.

          OTOH the Weird Rust examples are not a problem you get in your own code. It's a local syntax problem, and it doesn't require complex whole-program reasoning. The stakes are also lower, because you still have the same safety checks, type checks, automatic memory management, immutability. The compiler aggressively warns about unreachable code and unused/unread variables, so it's not easy to write undetected Weird code.

          Rust tried having Underhanded Code Contest, but it has been very Underwhelming.

    • steveklabnik a day ago

      > If a language that claims to be security focused

      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.

      • gmueckl a day ago

        "Memory safety" is an aspect of computer security. And security is the first listed value in rust's mission statement.

        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.

        • kelnos 9 hours ago

          I think you're looking at it a little backward. Or rather, with superset/subset confusion. Rust can say "we care about memory safety" without being security-focused. But Rust cannot say "we are security-focused" without also caring about memory safety.

          Being security-focused requires you to care about a laundry list of things, including memory safety. But on its own, caring about memory safety just means... you care about memory safety.

        • steveklabnik a day ago

          It is an aspect, but it Rust does not promise your core is secure.

          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.

        • keybored a day ago

          We would need an example of puzzling code that can easily hide (security) bugs.

          The submission shows weird program snippets. I don’t think it shows weird snippets that can also easily hide bugs?

    • int_19h 20 hours ago

      "never" is an easily comprehensible concept once you start asking the right questions.

      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.

    • PaulHoule a day ago

      It is a critique of macros. 500 lines of Common Lisp replaces 50,000 lines of C++ but those 500 lines make no sense at all the first time you see them.

matt_kantor 3 hours ago

Many moons ago[1] I wrote this, which seems appropriate to share here:

    fn main() {
      println!(r#"{:X?}{}"#,
        __=(|&__@_:&'_ _,|->_{[(|(..,_,__,_,):(_,_,((),),)|__..__)(__)]})({&(!(({"\"__'\\\\\
        \'";(||{('\"');()})();}>=*&())|(|__|__||__|__)((()<())&(()>()))),&[..=..],({0__.%-//
        0.;(|_:[();0],|{})([[],[],][0]);},),)}),_={(|_0_:[_;0],_:&[()]|{;_0_})({{[0;0]}},&[[
        ]][(0..)][{..}][0],);""},);
    }
[1]: https://www.reddit.com/r/rust/comments/8p013f/comment/e094qj...
ramon156 a day ago

Note that for Rust devs these are also weird syntaxes. I feel like some people assume that an experienced dev can read these, but it takes a while to get what's going on.

  • ChuckMcM a day ago

    Kind of like obfuscated C code I suspect.

    • dathinab a day ago

      yes, but less risky (and less power full) because you often very fast can conclude that "whatever it does it's safe, sound and doesn't affect unrelated code"

  • 01HNNWZ0MV43FF 21 hours ago

    Yeah. I've been doing Rust for a few years and when I look at these I just see "Failed PR review, unreadable to humans"

lacker 21 hours ago

I think there's a mistake in the explanation for "bathroom_stall". When describing the guard in this expression:

  if (i+=1) != (i+=1)
The post says, "The if statement is always going to be false because the right expression will always be one more than the left." But it's a not-equals. The if statement is always going to be false because in Rust "i += 1" doesn't return an integer value, it returns a (). So comparing any two += statements, they are always equal. Since the guard is a != comparison, the if statement is always false.
xg15 a day ago

Rust noob here.

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?

  • NobodyNada 21 hours ago

    Yes, exactly -- it's called the Never type.

    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.

  • int_19h 20 hours ago

    Not necessarily the entire statement, just some outer expression.

    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:

        // Compute the first Fibbonaci number >10
        let n = {
            let mut x1 = 0;
            let mut x2 = 1;
            loop {
                let x = x1 + x2;
                if x > 10 { break x }
                x1 = x2;
                x2 = x;
            }
        };
    
    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:

       let x = 'label: { ... break 'label 42 ... }
    
    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.
  • Analemma_ a day ago

    Yes. If you look at steveklabnik's example with the match statement elsewhere in the comments, it makes sense that '!' is the "never" or "unreachable" type, not because the return expression isn't run, but because its value will never be assigned to a variable, since it causes an unconditional exit from the function.

arjvik a day ago

I figured out how return-as-a-value made sense only upon realizing that in the following code,

    fn funny(){
        fn f(_x: ()){}
        f(return);
    }
f() is never called because funny() returns before it gets called.

The reason you want return to be coercible to any type is so that you can write something like

    let x: i32 = if y {
        4
    } else {
        return; // type ! coerced into i32
    }
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)

munificent a day ago

Does anyone know why `union` isn't a reserved word in Rust?

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?

pluto_modadic a day ago

I've absolutely seen some /cursed/ rust one liners.

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.

kzrdude a day ago

Many of them are on the same theme - the theme is `return -> !`.

Here's my favourite on that theme, which I was missing from the list:

    return return return return return return return return return 1
  • wredcoll 21 hours ago

    Is there some meanining to the number of returns?

    • kzrdude 21 hours ago

      No, it's just nonsense. Same result with fewer or more returns..

behnamoh a day ago

Great, now this stuff gets fed into LLM trainings and we'll see them in the next-gen model outputs.

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!

Sniffnoy a day ago

I don't understand the one with dont(). Why does i.get() end up false at the end? Shouldn't it be true after having been set by dont()?

  • LegionMammal978 a day ago

    The final line "assert!(i.get());" asserts that i.get() is true in the end. The ! character here belongs to the assert! macro, not a boolean negation. (This unfortunately gets a bit weird-looking when you want to write sensible stuff like "if !matches!(x, Some(5..=10)) { ... }".)

    • Sniffnoy 20 hours ago

      Oops, I see, thanks!

IshKebab a day ago

Honestly I'm surprised how not weird these are. Way less WTFy than Javascript, PHP, C or C++.

  • dathinab a day ago

    yes but to be fair the blog is focused on unusual aspects of everything being an expression

    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

bensons1 20 hours ago

I am sort of amused, a memory safe language deploys this sort of juggling

npalli a day ago

yeah this is good, but now add lifetime annotations to have fun.

  • steveklabnik a day ago

    Lifetimes are not expressions and therefore can't really be put into odd places.

Thaxll a day ago

Why assigning a variable to a function that returns nothing is not a compilation error?

  • tialaramex a day ago

    Do you mean a function which returns "nothing" in the sense that it does return but it has no particular value to return, like Vec::clear which gets rid of the values but preserves the capacity of the container ?

    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.

  • dathinab a day ago

    assuming you mean returning () (the empty tuple/void type)

    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.

  • assbuttbuttass a day ago

    There's no such thing as a "function that returns nothing" in Rust. Unit, written as (), is a first class value and can be assigned to a variable, although there's not much point

JoeOfTexas a day ago

Bruh, I started learning Rust yesterday. Why do you do this to me. Now I don't know anything I studied.

  • steveklabnik a day ago

    You don't need to know any of this. It's just a parsing stress test, with meaningless programs. It's fun trivia.

keybored 21 hours ago

Once I tried how many `{{{...}}}` I needed to make javac crash. It wasn’t that many.

xyst a day ago

These “weird expressions” probably get used code golf.

  • steveklabnik a day ago

    Basically none of them are actually useful, or even do anything, it's mostly a parser stress test.

nikolayasdf123 a day ago

this is why I like Go

  • nemo1618 a day ago

    I wonder, what's the "weirdest" expression in Go? Here's one:

       type Foo struct{}
       func (Foo) Bar() { println("weird...") }
       func main() {
        ([...]func(){^^len(`
       
       
       `): (&Foo{}).Bar})[cap(append([]any(nil),1,2,3))]()
       }
    • assbuttbuttass a day ago

      Makes sense to me, [...]func() is an array of functions, and [...]T{index: value} is uncommon but still perfectly comprehensible

      • nemo1618 a day ago

        Many people aren't aware that you can use key: val declarations in arrays

      • jenadine a day ago

        That's why I like Rust /s

    • nikolayasdf123 13 hours ago

      this the worst? not too bad. fairly comprehensible.

  • techbrovanguard 13 hours ago

    this is not the gotcha you think it is, you just dropped your gluestick

  • timeon a day ago

    It does not have stress tests for parser?