Settings

Theme

Rust's Ugly Syntax (2023)

matklad.github.io

161 points by nequo a year ago · 170 comments

Reader

sedatk a year ago

I think the article makes a good point, but the actual example isn’t Rust’s worst, not even close. It gets really hard to follow code when multiple generic types are combined with lifetime markers. Then it truly becomes a mess.

  • WhyNotHugo a year ago

    Here's a nice example of a Trait that has async functions:

        fn list_items<'life0, 'life1, 'async_trait>(
            &'life0 self,
            collection_href: &'life1 str,
        ) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + 'async_trait>>
        where
            Self: 'async_trait,
            'life0: 'async_trait,
            'life1: 'async_trait,
    
    
    Rendered docs: https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorag...

    Source: https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta...

    • the_mitsuhiko a year ago

      It's worth pointing out here for people not familiar with Rust, that this is the result of code generation by a third party crate to enable async methods on traits.

      • Filligree a year ago

        Which isn't even needed anymore; now the compiler accepts it without any macros.

        • Dagonfly a year ago

          Well, you can't use the `async` keyword version if you need the `Send` bound.

          Imo, Rust should introduce some syntax like `async(dyn+Send)` that desugars to `Box<dyn Future<Output = bla> + Send>`. This solves most of the async wards if you don't care about heap allocation and perfect performance.

        • WhyNotHugo a year ago

          The compiler only supports these for static dispatch, the above use case relies on dynamic dispatch.

    • mrweasel a year ago

      The use of ' as a symbol that has any meaning on it's own has got to be one of the most stupid choices I've seen in a language. It's made worse by the fact that you can still use '...' as s character literal.

      Not only is is incredibly ugly it's also rather confusing, but it fits well into how I view Rust, complicated for the sake of making the developers look smart.

      It wouldn't fit the syntax of the language obviously, but why not simply have the developer prefix the variable with the keyword "lifetime", rather than assigning a symbol. It seems a little like starting functions in Go with a upper case letter to export them, dude just give us an export keyword, it's fine.

      • thesuperbigfrog a year ago

        >> The use of ' as a symbol that has any meaning on it's own has got to be one of the most stupid choices I've seen in a language.

        ' has a long history of use for various purposes in programming language syntax not derived from C.

        In Ada, attributes are a single quote followed by the attribute name.

        If I have an enum called Fruit then Fruit'First would give the first value in the Fruit enum definition.

        http://www.ada-auth.org/standards/22rm/html/RM-4-1-4.html

        Attributes provide meta information about types and are very useful when working with custom integer types and ranges:

        https://learn.adacore.com/courses/advanced-ada/parts/data_ty...

        Using ' for Rust lifetimes or Ada attributes is just a sigil https://en.m.wikipedia.org/wiki/Sigil_(computer_programming)

        It is not too different from:

            & for addresses / references in C, C++, and Rust, 
        
            * for dereferencing in C, C++, and Rust  
        
            $ for value substitution in shells and scripting languages
        
            : to mark keywords in Clojure and some Lisps
        • kazinator a year ago

          It may not be too different from references, but Rust separately has those also, separately from lifetimes.

          In modern languages, we don't have to think about lifetimes most of the time, let alone notate them.

          Rust has too much stuff converging into the same context.

      • xelxebar a year ago

        Wait. Are you a Cobol programamer? Your argument has nothing to do with ' per se; it's completely generic for every "symbol" in a language.

            https://www.mainframestechhelp.com/tutorials/cobol/arithmetic-statements.htm
        
        "Confusing" is mostly a question of familiarity; "ugly" one of taste. When you're designing a language's syntax, there is a tension between making the language feel recognizable to beginners/non-users and communicating important information saliently to experts. The former errs on the side of least-common-denominator symbols and explicit constructions, while the latter errs on the side of expression density and implicit understanding.

        Language features that appeal to beginners and outsiders naturally aid in language adoption, even if they actively work against expert practitioners. So, funnily enough, we should a priori expect the zeitgeist opinion to favor lowest-common-denominator languages features and shun high-utility but "complex" ones.

        That is a real shame, however. As a business or whatever, instead of maximizing for ease of onboarding, I want to maximize for facility in exploring the end-goal problem domain, i.e. expert work. Instead of picking a "readable" language, I want to pick one that increases our ability to find simple solutions to complex-seeming problems, conventionally readable or not.

        IMHO, baseline languages like Python are great for writing down what you already think but terrible for iterating on your understanding, and 95% of our work as engineers is (should be?) changing our understanding of the problem to fit reality as it bumps us in the face.

        • prirun a year ago

          > IMHO, baseline languages like Python are great for writing down what you already think but terrible for iterating on your understanding, and 95% of our work as engineers is (should be?) changing our understanding of the problem to fit reality as it bumps us in the face.

          I have to disagree. I've been working on HashBackup for 15 years now, and believe me, my understanding of backups has grown immensely over those years - with Python. Python may have some things I have to work around, as all computer languages do, but after all this time I still love working on and just reading through the code that has resulted from over 3200 commits.

          Python's simple, easy-to-read syntax, encourages me to change things, even in a complex section of code that I haven't looked at in years. For me, Rust's emphasis on multi-character syntax vs keywords makes it visually unappealing and ugly, and I don't think I'd enjoy working on ugly code for 15 years, even if it does run fast. Not intending to start a huge language discussion, but I do think the specific point of Python not being suitable for experts in a problem domain is not true, at least not for me.

      • swiftcoder a year ago

        Lisp variants have used the ' prefix as shorthand for the quote operator (interpret whatever comes next as a literal, instead of evaluating it) since about the time C became popular...

        • kazinator a year ago

          I don't know exactly when 'X denoting (QUOTE X) appeared, but I can see that it is described in a paper about the Lisp 2 system, from 1966. So it is at least 9 years before the B and NB languages evolved into C. The Lisp 1.5 Programmer's Manual from around 1962 doesn't mention it yet, using (QUOTE ...) for all occurrences of quote.

      • lifthrasiir a year ago

        Many languages (mostly functional) allow trailing apostrophes after alphanumeric identifiers, which mirror primes in mathematical and technical notations, so there are surely precedents.

        Rust doesn't allow multi-letter character literals for the usual reason, so there is no real ambiguity. Some old editors do only have a very-limited syntax highlighter with fixed rules (e.g. no regexp), so such editors will be indeed unable to handle this, but they won't be able to support many other languages if that's the case.

      • toolslive a year ago

        in OCaml, the single quote is a valid char for an identifier:

            let f = ... and 
            let f' = ...
        • mrweasel a year ago

          I did not know that, and I question that choice as well. Why would you use that?

          It's a little less bad, because you can just have your own style guide that says "Don't do that".

          • steezeburger a year ago

            It's common math notation to have f and f prime to represent something derived from f.

          • Degorath a year ago

            As an OCaml beginner I've mostly seen it used for inner functions in lieu of calling the inner function `f_impl`.

      • the_mitsuhiko a year ago

        > The use of ' as a symbol that has any meaning on it's own has got to be one of the most stupid choices I've seen in a language

        Can you clarify why you have that opinion? What would your syntax suggestion have been?

        • mrweasel a year ago

          Because pretty much any other language has '...' for strings, or at least something to do with text. It's also a character that in all other languages (that I know of) must be closed with another '.

          Now you could say, we don't close it in contractions in English, so there's a case where one ' can just exist on it's own. That's sort of fine, a bit outside of the realm of programming, but fine, but then I think you should remove then '...' usage. It's really confusing that the same character has two different meanings depending on something that happens later. Rust does this with ! as well if I understand correctly, so it's NOT like everywhere else, but something!() is macro expansion... Why not just prefix with "macro" so macro something()

          So you have something that has a ' in front, is that a lifetime, or a missing ' later? The compiler will catch it, so it not a problem in that sense, it just makes it hard to read the code.

          Personally I would almost always prefer a keyword. For Rust I think my problem is that the language is a little to happy with symbols and operators being one character picked for the none numbers and letters and the choice of ' makes it seem like they are running out of characters to choose from. Like we just one or two features away from assigning meaning to § and €.

          • lifthrasiir a year ago

            > Because pretty much any other language has '...' for strings, or at least something to do with text.

            I think you didn't use that many languages to see other forms [1]. LISP and Scheme don't have single-quoted string literals for example. Double-quoted strings are not really universal either, for example SQL uses single-quoted strings and double-quoted names.

            [1] https://rigaux.org/language-study/syntax-across-languages.ht...

          • kitkat_new a year ago

            > Because pretty much any other language has '...' for strings, or at least something to do with text.

            The ' aren't used in places where strings occur (strings just don't make sense there anyways), don't take up to much space (i.e. give more space to the name).

            I am not a Rust pro, but this has never been an issue for me, same for ! for macro expansions.

          • the_mitsuhiko a year ago

            > So you have something that has a ' in front, is that a lifetime, or a missing ' later?

            Not once has this come up for me. They are in completely different places syntactically and can never overlap.

            Sure, `'` might be text related in a lot of languages but definitely not universally. In LISP 'foo is shorthand for (quote foo) and also does not have a second character. Ocaml uses 'foo for types and foo' is just a valid identifier. Standard ML also has 'foo for type variables and I believe also allows identifiers named foo'. Haskell allows identifiers named foo' as well.

            Maybe it's odd coming from languages you are familiar with, but it's not at all something that is unique to Rust.

            > Rust does this with ! as well if I understand correctly

            I am not sure how the case with ! is similar. Macros just end with ! to make them clearer visually, it's not part of an operator. There can never be any syntax ambiguity with them, neither visually or lexically. Also what would be the point. Take this example:

                try!(do_something(...)).further();
            
            Do you really think this would be more readable?

                (macro try(do_something(...))).further();
            • mrweasel a year ago

              > Do you really think this would be more readable?

              Much more readable, I can just browse the code, and see: Hey a macro.

              I mostly write Python, but sometimes have to read, debug and modify C, Java, PHP, JavaScript, Bash and so on. Having code being easily readable and obvious is a huge win.

              Very often your code has to be read, and reasonably understood by someone who doesn't exactly know the language, or at least is much less proficient. They don't need to be able to do complex task or debug the inner most workings of your code, but they do need to be able to reason about it, quickly, if you want them to contribute with patches or detailed bug reports.

          • GoblinSlayer a year ago

            In rust "macro" would be abbreviated to "mac".

        • lelanthran a year ago

          >> It wouldn't fit the syntax of the language obviously, but why not simply have the developer prefix the variable with the keyword "lifetime", rather than assigning a symbol.

          ...

          >> The use of ' as a symbol that has any meaning on it's own has got to be one of the most stupid choices I've seen in a language

          > What would your syntax suggestion have been?

          Is the syntax suggestion he provided not applicable?

          • Ygg2 a year ago

            > Is the syntax suggestion not applicable?

            Lets look at proposed syntax

              fn list_items<lifetime life0, lifetime life1, lifetime async_trait>(
                     &lifetime life0 self,
                     collection_href: &lifetime life1 str,
                 ) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + async_trait>>
                 where
                     Self: life0,
                     life0: async_trait,
                     life1: async_trait,
            
            I'm not going to pretend I understood what mrweasel meant fully, so I assume we can either omit generic or in parameter declaration (so I went with omitting lifetime keyword in parameters):

              fn list_items<lifetime life0, lifetime life1, lifetime async_trait>(
                     &life0 self,
                     collection_href: &life1 str,
                 ) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + async_trait>>
                 where
                     Self: life0,
                     life0: async_trait,
                     life1: async_trait,
            
            
            I guess you might be able to omit the "generic part" like so (it might be impossible, lifetime are just generics useful for lifetime tracking):

              fn list_items(
                     &lifetime life0 self,
                     collection_href: &lifetime life1 str,
                 ) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + async_trait>>
                 where
                     Self: life0,
                     life0: async_trait,
                     life1: async_trait,
            
            In both cases, you get a huge verbosity increase, and mix between not knowing if a value like `Self: x` is a trait with lower case or a lifetime.

            So you trade verbosity for more ambiguity and programmer confusion, and possibly worse error reporting (is &a b a lifetime or a missing comma e.g. &a, b).

      • needlesslygrim a year ago

        I assume it's derived from the OCaml generic type syntax (the first Rust compiler was written in OCaml after all), for example the definition of the type `Option.t` in OCaml:

          type 'a t = 'a option = 
          | None
          | Some of 'a
        
        I think this is the case because IIRC this function:

          fn slice<'a>(s: &'a str) -> &'a str { &s[0..2] }
        
        Is generic over the lifetime of `s`, and it I assume it would have been difficult to distinguish generic types and lifetimes.
      • atoav a year ago

        I agree that this is a stupid choice. Yet I wrote many reliable and performant programs without using lifetime specifiers once. One of those programs was a text tokenizer that got second place on performance and was only beaten by a highly optimized piece of assembler.

        This is not nothing if you ask me. Rust is a language within which your shifty naive program will still outperform many other solutions, all while being reliable as heck if you wield the type system the right way.

        The only thing that I dislike about it is that certain code becomes unreadable. As this article says it often becomes unreadable for a reason — a reason which you would just not think about in other languages — but unreadable is still unreadable.

      • jeroenvlek a year ago

        A lifetime keyword would actually go a long way in improving ergonomics. You could even make it synonymous with '. Then people can choose. Maybe one will get much more traction and the other can be deprecated.

        • treyd a year ago

          The only calls to change the lifetime syntax have been coming from "outside the house". Rust developers are fine using the ' as the lifetime sigil and there are no calls from within the Rust community to change it. Adding a keyword would increase the verbosity substantially.

          • jeroenvlek a year ago

            When is someone a Rust developer and why do you assume that I am not one?

            edit: -4 points on my parent comment for a totally valid opinion, not violating any commenting guide line (please enlighten me). However, for the down voters: "Please don't use Hacker News for political or ideological battle. That tramples curiosity."

            • nequoOP a year ago

              I don't think that you violated any guidelines. I think that the downvotes for your parent comment are only an expression of disagreement with what you propose. If I can suggest, don't take it personally.

              Turning `'` into `lifetime` would make Rust code that needs explicit lifetimes extremely verbose. That would make code harder to keep on the same line, for no added benefit in expressiveness.

              Allowing both `'` and `lifetime` in the syntax would make the language more confusing to anyone who is learning it, and it opens up unproductive debates about syntax in code bases if two authors have conflicting preferences.

              • jeroenvlek a year ago

                Your explanation is appreciated. I naively assumed HN moderation was more meta than that. It boils down to taste, ultimately. I'm really a fan of Rust, and it is sobering to now experience this gatekeeping that the Rust community is often accused of first hand.

                Of course, mixing keywords single quotes is confusing, but currently the single quote is also confusing.

                Just to illustrate: The keyword could just be called lif, which has the same length as dyn.

        • kitkat_new a year ago

          why?

          It makes type information unnecessarily longer without adding information, and feels like writing "end_stm" instead of ";" after every line

          • mrweasel a year ago

            That's the trade off isn't it. ' is unnecessarily short and doesn't convey any information at all, or worse, the wrong information. There are other comments that point out that ' is valid in identifier, or used to indicate that something is derived from something else.

            Some will prefer the short nature of just typing ', where people like me would prefer that you just add a few more characters so it reads more easily.

            • kitkat_new a year ago

              It is, but not a trade off many would make.

              ' itself (!) may not convey information, but it's existence does convey information.

              While making it a keyword may help beginners to understand code and wish they could see the meaning directly, because they don't know the meaning of certain symbols yet, people who know the language and productively produce code are more concerned about content rather than syntax.

              Content meaning the name of the life time, the name of the types, the name parameters, the structure of the types, the constraints on the types, etc.

              Especially for Rust, there is a lot of the things mentioned above, and since the concepts in Rust aren't easy, it's important that these things get as much space as possible from the limited space on the screen, instead of wasting it by self explaining syntax elements, which are trivial to memorize.

              Thus { instead of begin_function_body, *; instead of statement_end, ? instead of 'if err != nil then return err' and ' instead of lifetime.

          • Brian_K_White a year ago

            It's more like if a ; was allowed somewhere else and had a different meaning there.

            enum foo; blah ...;

            • dwattttt a year ago

              Perhaps a better example would be & referring to AND operations (logical and bitwise), but also being a unary operator for taking the address of a variable.

            • kitkat_new a year ago

              like parenthesis for functions, tuples and for denoting prioritized evaluation?

    • virtualritz a year ago

      > https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorag...

      Unrelated to parent but maybe relevant to you: Rust API naming guidelines say no `get_` prefix on getters.

      https://rust-lang.github.io/api-guidelines/naming.html#gette...

    • iknowstuff a year ago

      or just

          async fn list_items(&self, colletion_href: &str) -> Result<Vec<ItemRef>, Error>
  • LoganDark a year ago

    I always, always forget what `'a: 'b` means, because I remember it always being the opposite of what I think it is, but memorizing that obviously doesn't work because then it will just flip again the next time. It's so annoying.

    • nrabulinski a year ago

      I always describe it to myself this way - T: Foo means T is a superset of Foo (because it at least implements Foo but most likely more) thus 'a: 'b means 'a is at least as wide as 'b, and possibly wider

    • littlestymaar a year ago

      I had the same problem until I realized this: for generics and traits T: A means T implements A and it's actually the same with lifetimes: 'a: 'b means lifetime 'a implements lifetime 'b, which naturally translates to objects with lifetime 'a lives at least as long as 'b.

    • mst a year ago

      I am reminded of many instances over my life of:

      1) Every time I go through a particular door I try to push it when it's a pull door.

      2) I notice this and ensconce in my brain that it's the opposite of what I think it is.

      3) After a while, my brain actually starts to remember it the correct way around to start with.

      4) But my brain doesn't then drop the 'opposite' rule so now I remember it the correct way around, then invert it, thus recreating step 1.

      I don't claim this says anything about rust, but I think it does say something about human brains (or at least mine and apparently yours).

      My sympathies.

      • LoganDark a year ago

        I don't identify human, but you're absolutely right. For me, this also happens with the direction of hot vs cold on faucets/showers. I can never ever understand. Sometimes they're relative to the top edge and sometimes to the bottom edge. Sometimes they are on the wall/sink and sometimes they are on the actual handle. Because everything is always completely impossible to define proper rules for, the only thing I can rely on is "it's different than you thought it was before" but relying on that also doesn't work!

    • runiq a year ago

      If you think of it as 'a implements b', it makes sense for both lifetimes and (other) subtypes: If lifetime `a` implements `b`, it is obviously valid for `b` (and maybe longer).

      • LoganDark a year ago

        I always do `<T, U extends T, V extends U>` etc for generics, but for lifetimes it's `<'a: 'b, 'b: 'c, 'c>` which always trips me up...

    • bobbylarrybobby a year ago

      You have to first remember what lifetimes are there for in the first place: to indicate that values live at least a certain duration. That is what the type &'a means. Then it becomes clear what the relation 'a: 'b means

    • delifue a year ago

      In lifetime, subtype means longer lifetime (it's unintuitive). 'a : 'b means 'a is a subtype of 'b, which contains 'b and can be longer.

      Rust can improve this by introducing syntax like `'a contains 'b`

    • lifthrasiir a year ago

      It was a good signal to me that you are overthinking into the architecture if that is really required. Rust makes something pretty much impossible in C/C++ possible, but not necessarily easy, and that would be one such example.

      • LoganDark a year ago

        > It was a good signal to me that you are overthinking into the architecture if that is really required.

        Sure, maybe I don't need to statically guarantee the correct execution of code that could easily just throw at runtime instead, but it sure is a fun hobby.

        • lifthrasiir a year ago

          Lifetime subtyping is normally not much necessary to ensure the correctness, provided that you are working at the sufficiently high level (so, say, std is available) and do have some freedom in the design. It often indicates that you are excessively avoiding heap allocations.

          • LoganDark a year ago

            You can't just allocate a Lua coroutine on the heap. Lua coroutines are managed by the Lua virtual machine. So that's one heap allocation "avoided". I don't remember the others off the top of my head but some functions needed to have three lifetimes for various reasons. Not sure if I eventually refactored that out or not, it's been a while. I should get back to it one day when my job isn't nothing but startup crunching

  • namjh a year ago

    IMHO the mentioned examples of complexity like multiple type variables and lifetimes with bounds are for who "really" wants compile-time contracts. These are mostly opt-in so higher level use cases(like writing backend business logics) should not care about that, just wrapping everything with Boxes and Arcs.

    Of course Rust is not perfect; there is some 'leakages' of low level aspects to high level like async caveats(recursion, pinning, etc.). I'm not sure how these can be avoided. Maybe just trial-and-errors for all..?

    • Hamuko a year ago

      I do remember the compiler constantly suggesting lifetimes to me as a newcomer to the language, so it didn't really feel that opt-in. Quite a lot of the suggestions also started to look like someone poured alphabet soup all over the code.

      • namjh a year ago

        That's because the code triggering compilation error is using reference. If you use Rc or Arc (which pays runtime cost) there should be no lifetime at all.

        Albeit I admit there somewhat exists a community sentiment like "if you use Rust, you should maximize its zero cost abstraction feature so lifetime is good and generics good", and my (minor) opinion is that, it's not always true to all users of Rust.

        And the clumsy Arc<Mutex<Something<TheOtherThing>>> makes users feel bad about using runtime cost paid types. Maybe we should introduce easy Rust dialect which transpiles into Rc/Clone everywhere but I doubt it's trivial to transpile.

        • dmurray a year ago

          > And the clumsy Arc<Mutex<Something<TheOtherThing>>> makes users feel bad about using runtime cost paid types

          Yeah, this would look worse than any of the "complicated syntax" examples in the blog post.

          A language should be designed so that the typical case is the easiest to read and write. Syntax for the most common abstractions. Rust forces you to be explicit if you want to do an Arc<Mutex<Box>>>, but lets you inherit lifetimes almost seamlessly. That means it's not idiomatic to do the first, and it is to do the second.

          Languages with a lot of historical baggage don't follow this pattern: if you want to write idiomatic modern C++ it's going to be uglier and more verbose than you see in K&R. But in Rust's case it's clear what the designers encourage you to do.

          • _answ a year ago

            Simple != idiomatic. It's perfectly idiomatic to `Arc<Mutex<Box>>` in Rust, and it is more complex because it deals with more concerns than a simple reference, namely being thread-safe. Sometimes you need that, sometimes not, but you have to be explicit about it.

          • cozzyd a year ago

            Not sure why you'd compare modern C++ to K&R which is a C book. Modern C is in fact less ugly than ancient C due to sugar like compound literals.

      • scotty79 a year ago

        That's mostly because borrows are a curiosity of Rust that newcommers are quickly introduced to while they are mostly just a perfomance gimmick.

        If you come to rust from high level language you can just do everything with Rc and cloning.

        It's still hard because Rust, in opposition to every pipular language, is a value oriented language. But at least you won't have much contact with alphabet soup.

      • estebank a year ago

        If you encounter compiler errors that are misleading in their suggestions, or have proposals for better output in specific cases, please file a ticket: https://github.com/rust-lang/rust/issues?q=is%3Aissue+label%...

  • yulaow a year ago

    throw in some async too and I really lose myself most of the times

  • nazka a year ago

    That's why I am a huge fan of Rust but at the same time at the end of the day all I want is the features of the language that Rust has minus the memory management and a GC. That would be my dream language. If only ReasonML/Rescript were more popular... Or I guess Elixir

    • neonsunset a year ago

      It's not necessarily identical to what you are looking for per se but a mix of C# and F# will be the closest overall to Rust in terms of performance, access to systems programming features, language expressiveness and tooling experience.

      Cons are:

      - C# has OOP (you don't have to use it heavily)

      - No Hindler-Milner type inference in C#, nested generic arguments may need to be specified by hand

      - Smaller amount of supported targets by CoreCLR: x86, x86_64, arm, arm64 for ISAs and Linux, Windows, macOS and FreeBSD for OSes. NativeAOT-based support is experimentally available on iOS, and is undergoing further work. As you can imagine, LLVM targets absolutely everything under the sun and above it too. no-std story in Rust is first-class. C# has bflat and zerosharp but they are niche.

      - In C#, type unions will only be available in one of the coming versions. F# to the rescue

      - Error handling is a combination of exceptions and e.g. int.TryParse patterns, there is no implicit returns with ? like in Rust

      - Lack of associated types and the types own their interface implementations unlike traits which you can introduce without control over the source. This results in having to implement wrapper types (even if they are structs) if you want to modify their interface implementations

      - You only control shallow immutability - readonly struct will not prohibit modification of the contents of a Dictionary<K, V> that it holds

      - async/await is more expensive

      - Big popular libraries often have features or implementation incompatible or not guaranteed to work with native compilation via NativeAOT

      - Object reference nullability (e.g. 'T?') happens at the level of static analysis i.e. does not participate in type system the same way Option<T> does in Rust

      Pros are:

      - Has FP features like in Rust: high order functions, pattern matching (match val -> val switch { , also 'is'), records, tuples, deconstruction

      - Fast to compile and run, AOT not so fast to compile but tolerable

      - If you know how to use Cargo, you know how to use .NET CLI: cargo init -> dotnet new {console, classlib, etc.}, cargo run -> dotnet run, cargo build -> dotnet build/publish, cargo add {package} -> dotnet add package {package}

      - Monomorphized structs generics with the same zero-cost abstraction assurance like in Rust, &mut T -> ref T, &T -> ref readonly T, sometimes in T but with caveats

      - Box/Arc<T> -> class or record, Arc<Mutex<T>> -> class + lock (instance) { ... }

      - Easy to use async/await but without ever having to deal with borrow checker and misuse-resistant auto-scaling threadpool. Task<T>s are hot started. Simply call two task-returning network calls and await each one when you need to. They will run in background in parallel while you do so. While they are more expensive than in Rust, you can still do massive concurrency and spawn 1M of them if you want to

      - Built-in Rayon - Parallel.For and PLINQ, there is Channel<T> too, you can e.g. 'await foreach (var msg in chReader) { ... }'

      - Iterator expressions -> LINQ, seq.filter(...).map(...).collect() -> seq.Where(...).Select(...).ToArray(), unfortunately come with fixed cost but improve in each version

      - Rust slice that wraps arbitrary memory -> Span<T>, e.g. can write the same fast idiomatic text parsing on top of them quite easily

      - Stupid fast span routines like .IndexOf, .Count, .Fill, .CopyTo which use up to AVX512

      - Compiler can devirtualize what in Rust is Box<dyn Trait>

      - Can make small native or relatively small JIT single-file executables that don't require users to install runtime

      - Rich and fast FFI in both directions, can statically link into Rust, can statically link Rust components into itself (relatively new and advanced)

      - Great tooling, Rider is very good, VSCode + base C# extension about as good as rust-analyzer

      - Controversial but powerful runtime reflection and type introspection capability, can be used in a very dynamic way with JIT and compile additional code on the fly

      - A bit easier to contribute to, depending on area owner and project (runtime, roslyn, aspnetcore, ...)

      - CoreLib has full-blown portable SIMD API that is years ahead of portable-simd initiative in Rust

      Because I occasionally use Rust and prefer its formatting choices, I carry this .editorconfig around: https://gist.github.com/neon-sunset/c78174b0ba933d61fb66b54d... to make formatting terser and more similar. Try it out if K&R and `I` prefix on interfaces annoy you.

    • sedatk a year ago

      check out Google’s Carbon :)

    • coryfklein a year ago

      Scala

  • hckr1292 a year ago

    Agree about the example! I can't tell if this article is tongue-in-cheek or earnest. I'm unclear on the point the author is trying to make.

    • tcfhgj a year ago

      The author explains it in the first sentence, i.e. not the syntax of lifetimes may be your problem, but the feature itself

    • zarzavat a year ago

      My reading is: people use Rust because it’s fast but then they complain about the semantics that make it fast.

      In other words, be careful what you wish for.

      Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. However, once you deviate from the goal of “as fast as possible”, then you have to choose which parts you want to sacrifice speed for productivity. Like Excel, everybody agrees that Rust is too complicated but nobody can agree on which 10% to remove.

      • sk11001 a year ago

        > people use Rust because it’s fast but then they complain about the semantics that make it fast.

        I don't think most people use Rust because it's fast - fast is nice but Rust is being thrown at a bunch of use cases (e.g. backend services and APIs) for which it replaces "slower" garbage collected languages (the language being faster doesn't always make the overall product/service faster but that's a separate question).

        What Rust gives you is a viable potential alternative to C and C++ in places where you absolutely can't have a GC language, and that's a huge deal, the problems and confusion start when people try to use Rust for everything.

        > everybody agrees that Rust is too complicated

        I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion.

        > Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity.

        True, and such languages already exist and are widely used, Rust doesn't need to fit that use case.

        • zarzavat a year ago

          > I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion

          With any language there’s an active part of the community and then there’s the “dark matter” of people who use the language but are not actively involved in shaping its direction, forums or subreddits, etc.

          Of course the people who are actively involved are likely to be of the opinion that all the complexity is necessary, but I doubt that applies to the broader Rust userbase.

        • zerodensity a year ago

          > I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion.

          Personally I feel it's not complicated enough. Where is my function overloading, variadic templates and usable compile time reflection? (Sure you can sometimes use macros but ew macros)

          • swiftcoder a year ago

            > Personally I feel it's not complicated enough. Where is my function overloading, variadic templates and usable compile time reflection? (Sure you can sometimes use macros but ew macros)

            Indeed. Rust is really crying out for a real CTFE implementation + richer macros to replace the mess that is procmacros (I really don't want to have to run an arbitrary external binary with full system access just to manipulate the AST...)

      • d_tr a year ago

        > Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity.

        D maybe? D and Rust are the two languages which come to mind when I think about "possible C++ replacements".

        • cdogl a year ago

          When GP said “most”, I interpreted it more broadly. Most applications simply do not require the guarantees of a non-GC language. When you expand that horizon, list of contenders becomes considerably larger - even when restricted to statically typed languages.

          • zarzavat a year ago

            Yes for example many Python users switched to Go, a native code GC language, and are satisfied with the performance.

            There’s also the middle ground of Swift’s memory management which uses compiler-elided refcounting - i.e. the compiler detects when a count goes up then down again and removes those operations.

            • dwattttt a year ago

              > There’s also the middle ground of Swift’s memory management which uses compiler-elided refcounting - i.e. the compiler detects when a count goes up then down again and removes those operations.

              In the face of threading that's not a safe optimisation; if another thread decrements the refcount inbetween those two removed operations, boom. The compiler will have to track every variable that crosses threads or something.

              EDIT: spelling

wiz21c a year ago

For my own situation, the articles present the right way to express all possible performance/error handling (which is expected in a standard lib) and then goes on to show how I actually code it in my own softawre where I don't really need the level of detail/finetuning of the standard lib.

Interestingly, my life starts at the end of the article, with the simple verison of the code, and as my understanding of rust widens, I go up to the beginning of the article and better define my function...

  • awesomebytes a year ago

    I've only learned a tiny bit of Rust, and I feel the same. Going from the bottom up, makes it all make so much sense. (Albeit I still like the Rattlesnake syntax haha)

tmtvl a year ago

Aw, no Rasp variant? Let's brainstorm it up...

  (defun read (path)
    (declare (generic P (AsRef Path))
             (type P path)
             (returns (io:Result (Vector U8))))
    (flet ((inner (path)
             (declare (type (Ref Path) p)
                      (returns (io:Result (Vector U8))))
             (try-let ((file (File:open path))
                       (bytes (vector)))
               (declare (mutable file bytes))
               (try (read-to-end file bytes)
                    (Ok bytes)))))
      (inner (as-ref path))))
MetricExpansion a year ago

If I understood all the semantic properties, including the separate compilation requirements, correctly, here’s how I think it would be done in Swift with the proposed nonescapable types features (needed to safely express the AsRef concept here). (Note that this doesn’t quite compile today and the syntax for nonescaping types is still a proposal.)

  @usableFromInline
  func _read(pathView: PathView) throws(IOError) -> [UInt8] {
      var file = try File(pathView)
      var bytes: [UInt8] = []
      try file.readToEnd(into: &bytes)
      return bytes
  }
  
  @inlinable
  public func read<Path>(path: borrowing Path) throws(IOError) -> [UInt8] where Path: PathViewable, Path: ~Copyable {
      try _read(pathView: path.view())
  }
  
  // Definitions...
  
  public enum IOError: Error {}
  
  public protocol PathViewable: ~Copyable {
      func view() -> PathView
  }
  
  public struct PathView: ~Escapable {}
  
  public struct File: ~Copyable {
      public init(_ pathView: borrowing PathView) throws(IOError) {
          fatalError("unimplemented")
      }
  
      public mutating func readToEnd(into buffer: inout [UInt8]) throws(IOError) {
          fatalError("unimplemented")
      }
  }
jiwangcdi a year ago

> The next noisy element is the <P: AsRef<Path>> constraint. It is needed because Rust loves exposing physical layout of bytes in memory as an interface, specifically for cases where that brings performance. In particular, the meaning of Path is not that it is some abstract representation of a file path, but that it is just literally a bunch of contiguous bytes in memory.

I can't understand this. Isn't this for polymorphism like what we do this:

```rust fn some_function(a: impl ToString) -> String { a.to_string(); } ```

What to do with memory layout? Thanks for any explanation.

  • K0nserv a year ago

    Rust needs to know the exact size, layout, and alignment of every argument passed to a function to determine how it gets passed(register(s) or spilled to stack) and used. For example PathBuf and String can both be turned into a reference to a Path, and while they have the same size their layout and implementation of `as_ref` differ.

    As for `impl`,

        fn foo(a: impl ToString)
    
    is syntactic sugar for

        fn foo<S: ToString>(a: S)
    
    The reason the standard library doesn't use this is because the code predates the introduction of `impl` in argument position.

    The reason the function takes `AsRef<Path>` instead of `&Path` is callsite ergonomics. If it took `&Path` all callsites need to be turned into `read(path.as_ref())` or equivalent. With `AsRef<Path>` it transparently works with any type that can be turned into a `&Path` including `&Path` itself.

    • jiwangcdi a year ago

      Then if Path is not about abstraction, why not use a raw byte slice like &[u8]

      • K0nserv a year ago

        That's orthogonal. If the type was `&[u8]` instead of `Path` the type signature would be:

            pub fn read<P: AsRef<[u8]>>(path: P) -> Result<Vec<u8>>
        
        The reasons for it to be generic and us `AsRef` remain. The reason for Path over &[u8] is, AFAIK, because not all byte slices are valid paths on all OSs, but also because a dedicated type lets the standard library add methods such as `Path::join`
eterps a year ago

Just give me Rattlesnake or CrabML and I'll stop complaining :-)

  • frankie_t a year ago

    Wanted to say the same. He straight up conjured a good looking code in CrabML as an example of similar level of "ugliness", while it has about three times less syntax noise.

    • hyperpape a year ago

      It ends in ";;". If that's not a typo, and it's semantically significant that there are two semicolons instead of one, that sounds quite finicky.

      • Joker_vD a year ago

             The answer to the question “When do I need the ;; within OCaml source code?” is never.
        
             It's not a part of the language and is only used by the interpreter as an end of input mark. 
        
             Historical note: In CAML Light, the predecessor of OCaml, double semicolons were mandatory. For this reason they are quite common in old code originally written in CAML Light or written in the early days of OCaml. These days they are considered a bad style. 
        
        from https://baturin.org/docs/ocaml-faq/#the-double-semicolon
  • stavros a year ago

    What's Rattlesnake? I can't find anything at all online.

    • frankie_t a year ago

      I think he's referring to Python, just like other made up names probably refer to Java, Js, C++ and OCaml.

  • baq a year ago

    Choked on my coffee when I got to Rattlesnake. Great name.

  • awesomebytes a year ago

    +1

anonymous2024 a year ago

I wonder. How does Rust syntax compares with https://www.hylo-lang.org/ syntax? That also is memory safe, typesafe, and data-race-free.

librasteve a year ago

Here's the cleaned up version of Rust from the OP:

  pub fn read(path: Path) -> Bytes {
    let file = File::open(path);
    let bytes = Bytes::new();
    file.read_to_end(bytes);
    bytes
  }
Here is is in raku (https://raku.org):

  sub read(Str:D $path --> Buf:D) {
    $path.IO.slurp: :bin
  }
[the `--> Buf:D` is the raku alternative to monads]
  • sedatk a year ago

    Then it’s just this with C#:

      public byte[] Read(string path) => File.ReadAllBytes(path);
    
    I think the article’s trying to explain a concept using an arbitrary piece of code from stdlib, not necessarily that specific scenario (opening and reading all bytes from a file).
    • neonsunset a year ago

      This. In general, standard library is a poor example as it has to serve very wide range of scenarios, be robust to many environmental conditions when IO is involved and perform optimally. If it's terse enough while doing so, it's good enough already.

    • librasteve a year ago

      well no need for the sub wrapper really

        say $path.IO.slurp: :bin
qalmakka a year ago

People that complain about Rust's syntax never have never seen C++ at its worst

  • olologin a year ago

    C++ is relatively easy to read. The only troubles I had is reading STL's sources because of all _ and __ prefixes, and understanding template errors from compiler, but that will soon be fixed with concepts.

    • qalmakka a year ago

      > C++ is relatively easy to read

      Only if you suffer from a very high level of stockholm syndrome, that is. Rust's syntax is vastly clearer than C++ in basically all circumstances.

tevelee a year ago

The article just turned Rust into Swift. Nicer syntax, same semantics

apatheticonion a year ago

Someone needs to tell them about async Rust. Big yikes.

  • carlmr a year ago

    I'm a big Rust fan, but async Rust is an abomination.

    • iknowstuff a year ago

      I love async Rust. Its implementation is marvelous. Sueper plasant to write now that

      * async closures

      * async trait fns,

      * `impl Trait` everywhere

      are in place.

      • carlmr a year ago

        Maybe I should revisit async Rust. I haven't used that part of Rust in a while.

      • apatheticonion a year ago

        Wait up, are async closures in stable now?

        Would certainly help with the wild return and type signatures I've had to write involving combinations of `Pin` `Box` & `Future<Output = Result<...>>`.

        I think Rust futures are really awesome as they expose the lower level details of how async behavior works, giving the developer the flexibility to adapt it to their use case - but the standard library really dropped the ball on standardized usage/types.

        For instance, can we please just have `AsyncRead` and `AsyncWrite` traits? Can they also be easy to implement (are just `async read()`)?

        Right now you have to use adapters between Tokio types and the Futures crate and they both offer their own non-interoperable read/write traits.

        • iknowstuff a year ago

          It’ll be a nice day when AsyncRead/AsyncWrite lands in stable but I think we should appreciate that it didn’t happen hastily. Now that Linux has uring, Windows has IOCP etc we need to pass ownership of buffers to the kernel instead of passing by reference.

          Edit: we use nightly Rust at work so I’m able to use async closures no problem but they’re not quite in stable just yet.

mgaunard a year ago

There are several problems with the C++ variant, which could have been easily avoided by just following the original Rust more closely.

AxelLuktarGott a year ago

Is it really better to remove the error case information from the type signature? Aren't we losing vital information here?

  • treyd a year ago

    The std::io error type is defined roughly as:

        type Result<T> = std::result::Result<T, io::Error>;
    
    So it's actually fine, since we're specifying it's an IO result. This is a fairly common pattern.
Woshiwuja a year ago

So you just end up with python at the end?

macmac a year ago

My hot take is that Rust should have been a Lisp. Then it could also have had readable macros.

mjburgess a year ago

Kinda disingenuous, you don't reskin one language in another to make an argument about syntax -- you develop a clear syntax for a given semantics. That's what rust did not do -- it copied c++/java-ish, and that style did not support the weight.

When type signatures are so complex it makes vastly more sense to separate them out,

Consider,

  read :: AsRef(Path) -> IO.Result(Vec(U8))  

  pub fn read(path):
    inner :: &Path -> IO.Result(Vec(U8))

    fn inner(path):
      bytes := Vec.new()

      return? file := File.open(path) 
      return? file.read_to_end(&! bytes)
      return OK(bytes)
    
    inner(path.as_ref())
  • kelnos a year ago

    Rust does allow you to split out generic specifications into a separate "declaration block" so things don't get too busy. Like you could write the original Rust code as:

        pub fn read<P>(path: P) -> io::Result<Vec<u8>>
        where
            P: AsRef<Path>,
        {
            // ...
        }
    
    Personally I don't find your example with the type signature completely separate to be easier to read. Having to look in more than one place doesn't really make sense to me.

    Funny, though, Rust's 'where' syntax sorta vaguely superficially reminds me of K&R pre-ANSI C:

        unsigned char *read(path)
            const char *path;
        {
            /* ... */
        }
  • _answ a year ago

    I have a feeling that most of the clarity you find in your example comes from better use of whitespace. Consider:

        pub fn read<P>(path: P) -> io::Result<Vec<u8>>
        where
            P: AsRef<Path>,
        {
            fn inner(path: &Path) -> io::Result<Vec<u8>> {
                let mut bytes = Vec::new();
        
                let mut file = File::open(path)?;
                file.read_to_end(&mut bytes)?;
        
                Ok(bytes)
            }
        
            inner(path.as_ref())
        }
    
    Plus, your example does not have the same semantics as the Rust code. You omitted generics entirely, so it would be ambiguous if you want monomorphization or dynamic dispatch. Your `bytes` and `file` variables aren't declared mutable. The `try` operator is suddenly a statement, which precludes things like `foo()?.bar()?.baz()?` (somewhat normal with `Option`/`Error`). And you weirdly turned a perfectly clear `&mut` into a cryptic `&!`.

    Please don't assume that the syntax of Rust has been given no thought.

  • tcfhgj a year ago

    To me this example is not more clear than normal Rust

    • mjburgess a year ago

      If you've programmed a lot in Rust, then that's a win -- since its "not more clear", and yet, you've no experience in this syntax.

      • amenhotep a year ago

        He said "not more clear" and yet you're responding as if he'd said "not less clear" or "exactly as clear"? This seems strange?

  • blksv a year ago

    I strongly support your point, but the example is still sand-in-the-eyes for me. I hold that one symbol should not alter the semantics of a program and there should never ever be sequences of one-symbol syntactic elements.

    In an Ada-like language, it would be something like

      generic
          type Path_Type implements As_Path_Ref;
          type Reader implements IO.File_Reader;
      function Read(Path: Path_Type) return Reader.Result_Vector_Type|Reader.Error_Type is
          function Inner_Read(P: Path) return Read'Result_Type is
          begin
              File:  mutable auto := try IO.Open_File(P);
              Bytes: mutable auto := Reader.Result_Vector_Type.Create();
              try Reader.Read_To_End(File, in out Bytes);
              return Bytes;
          end;
      begin
          return Inner_Read(Path.As_Ref());
      end Read;
    • mst a year ago

      Huh. My brain says logically I should find that better, but the -lack- of punctuation is making it really tricky to skim read the way I do most languages.

      I'm not arguing the example you found sand-in-the-eyes is necessarily good but my mental skim reading algorithm copes with it much better.

      • blksv a year ago

        It is indeed harder to skim, and I find myself much more relying on syntax highlighting and file outline when working in Ada than in C++. Not due to the lack of punctuation, though, which is in place but serves the guiding role only (it is MLs that tend to abolish all the unnecessary punctuation), but because of the overall dense style.

        But while it is harder to _skim_, it is easier to _read_, as you don't have to concentrate on and decipher the syntax, risking to miss some of the crucial elements (oh, how do I hate missing ampersands in C++!).

  • degurechaff a year ago

    Rust target user is C/C++ developer. not using brace is out of options.

  • demurgos a year ago

    People may disagree on specifics, but you're definitely right that being able to separate the function signature from its definition would be very helpful in complex cases.

    • scotty79 a year ago

      Why? You can easily find parameter names in the signature if you just put them on separate lines. For me there's very little reason of putting them all together on separate line after the signature. And then when you look for a type of a parameter you know the name of, it gets difficult.

  • timeon a year ago

    Now there are too many colons.

    • bvrmn a year ago

      Original code contains more. (::). Personally I don't understand why there are `::` and `.` to access deeper namespace levels. It's a static language, compiler should know how to resolve attributes and `.` should be enough for all cases.

      • hmry a year ago

        It's because then you could have name collisions between variables and modules. E.g. it's ambigious whether io.Error is in a module called io, or a field of a variable called io. Which means you can no longer use fully qualified names regardless of context.

        • bvrmn a year ago

          > Which means you can no longer use fully qualified names regardless of context.

          How about don't shadow module names with variables if you want to use it? It works amazingly well for a bunch of other languages.

remcob a year ago

Why stop there and not go all the way to

    pub fn read(path: Path) -> Bytes {
      File::open(path).read_to_end()
    }
  • oneshtein a year ago

    How to return an error in your example?

    • gary_0 a year ago

          pub fn read(path: Path) -> Result<Bytes> {
            File::open(path)?.read_to_end()
          }
      
      isn't so bad either.
      • remcob a year ago

        Exactly, and this is in my experience what most Rust code ends up looking like.

        It compromises a bit on generality and (potential) performance to achieve better readability and succinctness. Often a worthwhile trade-off, but not something the standard library can always do.

    • tcfhgj a year ago

      Throw an exception

      proving the point of the article even further

      • wtetzner a year ago

        You would actually use a Result type:

          use std::io;
          
          pub fn read(path: Path) -> io::Result<Bytes> {
            File::open(path)?.read_to_end()
          }
        • tcfhgj a year ago

          Sure, if you are allowed to change the signature, makes it look more ugly than just returning Bytes though

singularity2001 a year ago

   "I think that most of the time when people think they have an issue with Rust’s syntax, they actually object to Rust’s semantics."
You think wrong. Rust syntax is horrible because it is verbose and full of sigils
oguz-ismail a year ago

The final version is still ugly. Why `pub fn'? Why is public not the default and why do you have to specify that it's a function? Why `: type' and `-> type', why can't type go before the identifier? Why do you need `File::' and `Bytes::'? What is that question mark? Why does the last statement not need a semicolon? It's like the opposite of everything people are used to.

  • kelnos a year ago

    > Why `pub fn'?

    I prefer it this way; defaults should be conservative or more common: I write many many more private functions than public. I'm not sure what your objection to 'fn' is... seems like a superficial problem. It likely makes the language easier for the compiler to parse, and to me, it makes it easier to read.

    > Why `: type' and `-> type', why can't type go before the identifier?

    Because putting the type afterward is more ergonomic. If you're used to C/C++/Java/etc. it feels weird, but once you start writing code with the type after the declaration, it feels much more natural.

    > Why do you need `File::' and `Bytes::'?

    I'm not sure what you mean here. They're types. You have to specify types.

    > What is that question mark?

    The question mark is basically "if the Result is Ok, unwrap it; if it's an Err, return it immediately."

    > Why does the last statement not need a semicolon?

    Leaving off the semicolon returns the last expression from the block.

    > It's like the opposite of everything people are used to.

    Maybe if your experience with programming languages is fairly limited...

  • pta2002 a year ago

    Short answer for the type ordering and `fn`: because C/C++/Java tried that type of syntax and the result was an ambiguous grammar that is way too hard to parse, not to mention C's overly complicated pointer syntax.

  • atoav a year ago

    As someone who doesn't think it is pretty, but knows Rust I went through all your points and let me assure you except for the one where you wonder why the syntax can't be more like C/C++ where it comes down to taste, all of your questions have an answer that really makes sense if you understand the language.

    E.g. making pub default is precisely the decision a language would make that values concise code over what the code actually does.

    • janalsncm a year ago

      Definitely agree, “pub” was one of the design decisions I loved learning Rust. If you forget to add it, you’ll get a compiler error. But if pub was default, I’d be exposing code unnecessarily. And no need for a separate private keyword, the absence of pub is sufficient.

      The same reasoning works for “mut” as well.

      That said, I don’t like Rust’s syntax. Especially once you get to lambdas, things get hard to read.

  • uasi a year ago

    Your points have nothing to do with ugliness.

    > Why `pub fn'? Why is public not the default and why do you have to specify that it's a function?

    If public were the default, you'd end up having to make other functions `priv fn` instead.

    > Why `: type' and `-> type', why can't type go before the identifier?

    It's easier to parse, and most major typed languages other than C/C++/C#/Java put the type after the identifier.

    > Why do you need `File::' and `Bytes::'?

    Seriously?

    > What is that question mark?

    The final version doesn't use a question mark.

    > Why does the last statement not need a semicolon?

    This is a legitimate question. In Rust, the last statement without a semicolon becomes the return value.

    • mFixman a year ago

      > If public were the default, you'd end up having to make other functions `priv fn` instead.

      My guilty pleasure is Go's visibility system, where all functions that start with lowercase are private to the scope of the current class/file and all Functions that start with Uppercase are public.

      It doesn't look but it would work and it's a mess when you need acronyms, but it somehow works great and the result looks nice.

      • kelnos a year ago

        I actually really dislike Go's system! Casing, to me, shouldn't be significant. (Just like I dislike how whitespace is significant in Python.) And to me it just doesn't look nice, but I also prefer snake_case over camelCase and PascalCase.

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection