1. 49

  2. 31

    I’ve noticed that some people (like myself) have trouble reading text that has too many glyphs/components. Perhaps it’s somekind of mild cousin of dyslexia?

    For example: Foo::bar() versus Foo.bar(), the former is substantially more difficult to look at for me than the latter, or even Foo() -> Bar versus Foo() Bar.

    Now toss in pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> { and each one of those brackets, fishes, colons, (toss in an occasional ampersand, an apostrophe or two) is like a weird pang of distraction and confusion.

    Acclimating definitely helps, but changing contexts and re-acclimating is still noticeably hard. Perhaps this is why some people prefer fonts that provide special ligatures that combine characters, like Fira Code.

    1. 8


      I think my biggest gripe is the use of < and > as brackets. In most typefaces they aren’t tall enough to visually encompass text between them. And they’re also used for comparisons and ->, so it they have multiple meanings, which makes it harder for to scan code quickly.

      1. 4

        And they contain a different language (the type-level language), so in a typical Rust function signature, you’re switching back and forth between the type-level and value-level languages (sometimes more of them).

        That’s something you don’t have to do with CrabML functions. Or in Zig where there’s only one language.

        How about Ruzt:

        pub fn read(const P: AsRef(Path), path: P) io.Error!Vec(u8) {
            return inner(path.asRef());
        fn inner(path: &Path) io.Error!Vec(u8) {
            var file = try File.open(path);
            var bytes = Vec(u8).new();
            try file.readToEnd(&bytes);
            return bytes;

        (Using const P like in const generics, rather than comptime as it would be in Zig, where parameters are also const by default.)

      2. 5

        I don’t discount the glyph noise itself, but I think another source of the difficulty is that multiple levels of meaning are being mixed together.

        In straight python, for example, there is only “what does this do”, and typically that’s what we care about when reading code. Whereas here we have to extract “what does this do” from layers of type information, including concrete and generic types, as well as concepts like mut. Sometimes that information is relevant to you, but by and large you have to learn to “read through” the noise, which is tiring.

        By contrast even J, which is pure glyph soup, does not have this particular difficulty (though has its own challenges). But, for me at least, it is not hard in the same, soul-draining way that C++ is, for example. I can’t speak to Rust from personal experience.

        1. 1

          Foo.bar squad represent! (D, static access with ., implicit call.)

        2. 16

          For the context, this is specifically a HN piece, as that tends to have a “… but Rust has ugly syntax!” comment in every discussion.

          1. 7

            The “Rattlesnake” example is much easier to read.

            1. 5

              Useless return keywords though. It’s weird if you have a function not returning a value so it can be omitted. If you’re going to be doing some effect or something, you should be explicit about returning unit or void or something to put the ergonomics into doing the right thing–similar to how mutable variables aren’t the default and have the ergonomics make them less appealing.

            2. 6

              My problem with Rust is the big amount of symbols. And the general increase of symbols in languages. Which is hilarious, because at some point in the far distant past I was a Perl programmer.

              Only in Rust it doesn’t feel like it’s a trade-off for being less verbose and a lot of them seem like they are nods to language families or math.

              I am fine with braces, curly braces, etc. but colons for parameters, arrows, for what feels like no reason and :: tend to feel a bit like noise. And I have a habit of ending up skipping them only to eventually miss something important. I feel like :: is the most justified, but that might be because it’s somewhat wide-spread. I also don’t really think that it solves a practical problem. It’s usually clear when a method is static. And even where it isn’t I don’t think it usually results in a big, and even if it would, I think it would typically be easy to spot, in many situations at compile time.

              I think being explicit is good, but at times overdone.

              Since I think Rust is actually a cool project and has many cool concepts I hope they don’t end up being considered “too complicated” or “too weird” in a decade or two, just because of the syntax.

              1. 4

                I kinda like CrabML, if mainly for the name… 🤔

                1. 2

                  Someone also ported this to Russell, CrabML’s close relative: https://old.reddit.com/r/rust/comments/10mdlep/rusts_ugly_syntax/j6484od/

                  1. 1

                    Wasn’t Rust based on or originally implemented in OCaml?

                    1. 4

                      Original Rust compiler was in OCaml, yes.

                  2. 4

                    The only qualm I have with rust’s syntax is the difference between specifying fn arg types and fn return types. There’s lots of sensible alternative options. I think the simplest would be an improvement:

                    fn f(x: XType): FType { ... }

                    The current syntax sticks out not only because there is a difference but because the difference doesn’t provide anything to the user of the language. Even if you appreciate the lineage of these syntactical elements you don’t gain any insight into the intent being expressed. It feels like the only white tower nit in a language that is otherwise extremely practical.

                    1. 6

                      This has a pleasing symmetry, but is definitely harder for this human to parse, at least. The little stabby arrows stick out. I’ve actually searched documentation for -> ReturnTypeIWant, which would be harder with a unified syntax.

                      1. 4

                        I get that initial reaction but ): ReturnTypeIWant is the same amount of typing and seems just as unambiguous. In each case the only ambiguity is with closures syntax.

                        1. 7

                          But then I have to type ):

                    2. 3

                      Looks a bit like a smaller Rust, maybe?

                      1. 3

                        The CrabML example was a bit unfair. It’d be more like

                        let read (p: p' ref_of) -> u8 vec io.either.t =
                          p |> ref_op |> File.open |> Result.map (File.read_to_end)
                        1. 3

                          This would be more informative if it translated the examples into the actual languages being compared to, instead of the strawman Frankenstein languages concocted to make it look like ‘Rust isn’t that bad’.

                          1. 9

                            The point of the exercise is exactly the opposite of that: to compare syntax while holding the semantics constant.

                            1. 2

                              That’s the problem, by trying to concoct a version of the syntax which keeps the same semantics in different languages, the examples turn into chimeras that don’t really make sense. For example, what does this CrabML mean: try (file.read_to_end (&mut bytes)); Right bytes. In ML languages the try syntax is for exception handling, here it looks like it’s being rejiggered to work like Rust’s ? operation for the Result type?

                              Syntax and semantics in languages are pretty carefully designed, it’s not easy to just slice them apart and fit them back together in different ways.

                          2. 1

                            All those syntaxes are terrible.

                            But I don’t think it follows from this that the semantics force the terrible syntax, just that most languages have terrible syntax.

                            For instance, here it is in my language (somewhat ripped off from D):

                            (ubyte[] | fail Error) read(P)(P path) {
                                (ubyte[] | fail Error) inner(P path) {
                                    auto file = File.open(path);
                                    return file.readToEnd?;
                                return inner;

                            Iunno if it’s just me, but that looks better!

                            Of course, it could be replaced with (ubyte[] | fail Error) read(P)(P path) => File.open(path).readToEnd;.

                            Or, as in D, alias read = path => File.open(path).readToEnd;.

                            I mean, the examples kinda prompt questions like: why are you thinking about path references at all? Why is that a relevant part of the code? What business need does it satisfy?

                            1. 5

                              Iunno if it’s just me, but that looks better!

                              Might be just you =P

                              Seriously, though: I get the impression that there is a whole layer of semantics that you’re just skipping in your version. It’s easier to rewrite code to look simpler if you also don’t do everything that the original code did.

                              1. 1

                                I’m skipping stuff that’s not really a thing in my language, like pointers and reference parameters. I count that as fair because I exhort people to not worry about that.

                                1. 4

                                  If you’re gonna argue that some of the semantics of Rust are unnecessary, then I don’t think you can say this:

                                  But I don’t think it follows from this that the semantics force the terrible syntax, just that most languages have terrible syntax

                                  The whole point of the article is how the syntax kinda emerges from the semantics. If you’re gonna throw away the semantics, then off course the syntax is gonna be different, and perhaps simpler.

                                  1. 1

                                    That’s fair.

                                    Though I think my example still looks better than the simplified Rust examples at the end.

                              2. 3

                                What is your syntax for lifetimes?

                                1. 1

                                  I don’t have one. Purely refcounted.

                                2. 2

                                  How do you apply type level functions?

                                  1. 1

                                    Not sure what you mean? Can you state that in C++y terms?

                                    1. 1

                                      like vector or map or whatever, what’s your syntax for “a vector of ints” or “a btree where the keys are 16 bit unsigned integers and the keys are strings”?

                                      1. 2

                                        Oh, as in D, Vector!int and BTree!(string, ushort). (Though D is apparently getting named arguments for templates!)

                                        1. 2


                                            1. 2

                                              Actually no, but I can’t find where I saw it now. :( Still, once we have DIP1030 (eventually), BTree!(key: string, value: ushort) shouldn’t be far behind.

                                  2. 1

                                    For reference, here is the example Rust function in context: https://doc.rust-lang.org/1.67.0/src/std/fs.rs.html#249-257.

                                    Rust is my favorite practical programming language and I don’t mind its syntax, so I’m sorry to say that I too find this piece too unfair, “strawman”-y, and — rather than “whimsical” — unfortunately snide.

                                    One point in particular that stood out to me is that the C++ version includes all those std:: namespace prefixes, whereas the Rust version not only omits the std:: prefixes (which is fine and idiomatic) but also omits the use statements. Omitting both is common in example code but makes for an unfair comparison against an example that doesn’t omit both.

                                    1. 2

                                      One point in particular

                                      That’s a reasonable point, but I think both versions are defensible. That is, this was an explicit decision for me, and the reasoning is that Rust syntax works by importing items while C++ syntax works by opening a namespace, and usually the advice is to avoid that (https://llvm.org/docs/CodingStandards.html#do-not-use-using-namespace-std). I do feel that Vec and std::vector are the corresponding idiomatic ways to spell the same thing, I wanted to preserve that feeling.

                                      If I were to be mean, I’d insist on Rs++ using .h/.rspp file pair :P

                                      1. 2

                                        Though, this probably should be tagged satire, suggested that!

                                        1. 1

                                          I do feel that Vec and std::vector are the corresponding idiomatic ways to spell the same thing

                                          I agree — and I should note that Rust’s prelude (and, if compared to a C++ header, its module system in which use imports don’t pollute one’s dependents) gives it a concrete advantage over C++ in syntactic conciseness here — but the core of my “particular” point was that, even though it’s normal in Rust examples, the Rust example is cheating a bit by omitting its necessary use statements.

                                          Rust syntax works by importing items while C++ syntax works by opening a namespace, and usually the advice is to avoid that (https://llvm.org/docs/CodingStandards.html#do-not-use-using-namespace-std).

                                          My C++ is rusty (lower-case!), but I believe Rust and C++ have similar options for this: C++‘s using namespace std is the equivalent of Rust’s similarly disfavored use std::*, whereas the equivalent of Rust’s liberally-used use std::foo is C++ using std::foo.

                                          1. 1

                                            All languages omit import statements (in particular, C++ avoids include), so that seems fair.

                                            but I believe Rust and C++ have similar options for this:

                                            Not exactly: C++‘s version is always pub use, which is the reason why it is generally often discouraged.