1. 42
  1.  

  2. 17

    Rust has some nice quality-of-life improvements that make up for the toil of memory management. Even when I don’t strictly need a systems programming language, I still use Rust, because I like sum types, pattern matching, foolproof resource cleanup, and everything-is-an-expression so much.

    So for glue code in GUI apps or CRUD webapps I’d love some Rustified TypeScript, Rustscript on the Golang runtime, or a Rust/Swift hybrid.

    1. 10

      Pretty much all of the ‘quality-of-life improvements’ you mention came to rust by way of ocaml, and are also present in other functional languages like haskell and scala. For webapps, try purescript or possibly elm.

      1. 5

        ocaml won’t be a “higher level rust” until multicore ocaml has matured.

        1. 3

          To any other readers: I wouldn’t ignore the language based solely on this, especially since multicore parallelism wasn’t one of the criteria in the top comment. Lwt and async give you concurrency if you want it.

        2. 5

          Pedantic nitpicking: resource cleanup (RAII) is a big one, and it‘s from C++. Everything else is indeed from ML.

          1. 4

            And traits are from haskell, and single ownership is from cyclone.

            But a large part of rust was directly inspired by ocaml.

            1. 5

              I was specifically referring to the list in the top comment. For fuller list, I would consult the reference.

              Cyclone doesn’t have single ownership/affine types. It has first-class support for arena-based memory management, which is a weaker form of rust‘s lifetimes, and is a different feature.

              See http://venge.net/graydon/talks/rust-2012.pdf for list of influences for both ownership and borrowing parts.

                1. 2

                  I stand corrected, thanks a lot!

          2. 1

            Or reasonml since it is a front end for ocaml. Or bucklescript since it is ocaml that targets Javascript.

            Disclaimer: never used them myself.

          3. 3

            Rustified TypeScript

            I write a lot of TypeScript but have only written a few hundred lines of Rust. I’m curious, which features of Rust do you miss when using TypeScript? The obvious one is traits, but I’m curious if there’s anything else.

            1. 5

              Not the person you replied to, but true sum types, especially Option. It’s much cleaner than undefined, even with all of the nice chaining operators. And not having to worry about exceptions thanks to Result.

              1. 3

                Ah, I see your point. TypeScript discriminated unions are used frequently to overcome this limitation, but I agree it would be preferable to have proper sum types with pattern-matching.

                type Square {
                    type: "square";
                    width: number;
                    height: number;
                }
                
                type Circle {
                    type: "circle";
                    radius: number;
                }
                
                type Shape = Square | Circle;
                
                1. 2

                  Oh god I miss TypeScript’s sum types so much when I’m writing Rust. When I context switch back to Rust after writing some TypeScript I with I could do

                  type Foo = Bar | Baz;
                  

                  Eventually I get over it but I find TypeScript so much nicer for describing the types of my programs.

                  1. 3

                    Oh yes. I wish TypeScript had more Rust, but I also wish Rust had row-level polymorphism.

                    1. 2

                      Maybe someday https://github.com/tc39/proposal-pattern-matching will happen. That will be a great day.

                    2. 1

                      same here, but on the other hand, those cool union types without pattern matching makes the call site uglier, and almost removes the nicety of the union type declaration

                      1. 1

                        Yes, you can relax a function that returns Foo | undefined to return Foo only, but in most “maybe” systems you can’t return a naked Foo without the Maybe<Foo> box

                  2. 1

                    In that Rust/Swift hybrid, what parts of Rust would you want added to Swift to make the hybrid? I read this article thinking Swift wouldn’t be a bad start.

                    1. 6

                      In Swift I miss “everything is an expression”. I have mixed feelings about Swift’s special-casing of nullability and exceptions. Terser syntax is nice, but OTOH it’s more magical and less flexible/generalizable than Rust’s enums. Swift inherited from ObjC multiple different ways of handling errors (boolean, **NSError, exceptions) — that’d be an obvious candidate for unification if it could start from scratch.

                      And if it was a “smaller Swift”, then I’d prefer a mark-and-sweep GC. With memory management Swift is neither here nor there: you need to be careful about reference cycles and understand lifetimes for bridging with FFI, but it doesn’t have a borrow checker to help.

                  3. 5

                    I would target WASM, and only WASM, for this language.

                    I was onboard until I got here. I didn’t want a Web Browser to be an application requirement when Electron did it, and I don’t want it now.

                    1. 9

                      You don’t need a web browser to use WebAssembly as a virtual machine though

                      1. 3

                        Are you talking about feeding a WASM blob into Node or something? Does a “JRE” exist for WASM? If so I would be interested in that. I just assumed a browser was required.

                        1. 6

                          There’s a few implementations out there. Here are two that I know of:

                          https://wasmtime.dev/

                          https://wasmer.io/

                          Here’s a fun thought: if you have a program that wants to allow scripting, what if it embedded a wasm runtime, and hooked up the WASI bindings for the script integration points? Then instead of dictating a single language, you can allow scripting from any language that can compile to WASM.

                            1. 2

                              WASM is sandboxed to maximally hermetic paranoid level. It doesn’t support any communication with the outside world, except callbacks exposed by the interpreter. You can’t write WASM for your operating system. There is an emerging abstraction layer called WASI, which is like a very basic tiny abstract operating system.

                              In practice that means you can’t use use your language’s standard library, not even printf, unless it’s been rewritten for the WASI “operating system”.

                              1. 1

                                Thanks. That was exactly my reservation about using WASM, and you confirmed it. Thats the same reason I dont use JavaScript, because my understanding is the language specification itself has those same limitations. It is only the runtimes (Node, Deno) that have developed workarounds for file system access, but I think those are still not defined in the spec.

                                So for Cadey or whoever to say that a browser is not required for WASM - yeah, thats technically true, but it still doesnt solve the problem of WASM programs being crippled in what they can do versus a language like Go or Rust.

                                1. 4

                                  WASM shouldn’t be any different than Lua in this regard. Theoretically nothing stops you from adding an escape hatch to your WASM interpreter, like ability to make an arbitrary syscall or FFI call.

                                  If it hasn’t been done already, it’s probably only because WASM created a niche for itself among people who want the sandboxing (e.g. running untrusted code on a CDN edge, or portable plugins for programs).

                                2. 1

                                  You can’t write WASM for your operating system.

                                  https://github.com/wasmerio/kernel-wasm

                                  I realize this is somewhat beside what you probably meant - but one possible benefit of a sandboxed spec is that things can run sort of safely in ring 0.

                                3. 2

                                  Looks like golang support for wasm is only for tinygo - with the runtime (the “libc”) only available as a Javascript implementation?

                                  At any rate, yet another option would be: https://github.com/bytecodealliance/wasm-micro-runtime

                                  But I expect it would fail in the same way (for golang).

                                  Ed: i guess the canonical “not web browser” wasm runtime/vm for go is node:

                                  https://github.com/golang/go/wiki/WebAssembly#executing-webassembly-with-node-js

                                  Ed2: but should also work with deno, which provides some sandboxing: https://dev.to/taterbase/running-a-go-program-in-deno-via-wasm-2l08

                                  1. 2

                                    Looks like golang upstream is considering wasi (ed: https://wasi.dev/) support:

                                    https://github.com/golang/go/issues/31105

                                    There’s also this: https://github.com/go-wasm-adapter/go-wasm/issues/5

                                    Found via: https://github.com/wasmerio/wasmer-go/pull/95

                                    And: https://github.com/wasmerio/wasmer-go/issues/18

                                    (also see my sibling comment)

                                  2. 2

                                    Other good ones: life and lucet

                                  3. 1

                                    I’ve actually had multiple projects to do that and even did a talk on it.

                              2. 4

                                I’d probably fix the kinda-broken Ord and Eq hierarchies that Rust inherited from Haskell. That would be an easy win.

                                Simplicity-wise, it would probably make sense to move the decision of reference type vs. value type to the type definition site, and not make it a choice in e. g. method signatures.

                                Syntactically, getting rid of the distinction between :: and . (by removing ::) and not using <> for generics gives probably the biggest gains there. Also, replacing -> with : in function result type signatures.

                                Oh, forgot: Drop mandatory semicola – it’s 2020.


                                Looks like I’ll be collecting lots of flags from Rust fans for this … ;-)

                                1. 10

                                  Drop mandatory semicola – it’s 2020

                                  Except semicolons in Rust aren’t abritrary, they have semantic meaning. An expression followed by a semicolon behaves differently than one without.

                                  1. 5

                                    Agreed Semantic meaning aside it’s like writing sentences without periods I find it hard to parse “idiomatic” Python / JavaScript code when it doesn’t have any semicolons There are many languages where it’s valid to continue a statement/expression after newlines The semicolons guarantee that “this statement doesn’t continue past here, and you can stop reading” When I have to write code in semicolon-free style I always find myself wishing for a way for my IDE to insert “phantom” semicolons wherever they are valid.

                                    1. 1

                                      I agree (and great illustration of your point!). I remember feeling a little bit disappointed when I learned that Swift allows semicolons at the ends of lines, but they’re not idiomatic :/

                                      1. 1

                                        When I have to write code in semicolon-free style I always find myself wishing for a way for my IDE to insert “phantom” semicolons wherever they are valid.

                                        Yeah, that’s exactly what I’d expect from a modern IDE. I mean that’s what niche IDEs for niche languages already managed to do in the 2010s.

                                        But that’s only possible if the placement of semicolons does not carry semantic meaning.

                                      2. 3

                                        Kotlin manages to handle this correctly though, doesn’t it?

                                        1. 3

                                          Kotlin and Swift both do. It’s a matter of defining your grammar carefully, unlike Javascript.

                                        2. 3

                                          Not only that, I don’t want to have to keep all the various edge cases and conditions for semicolon removal insertion in my head while I’m reading or writing code.

                                          1. 3

                                            Except semicolons in Rust aren’t abritrary, they have semantic meaning. An expression followed by a semicolon behaves differently than one without.

                                            Yep. Which is rather ridiculous.

                                            Thankfully, it’s an idea which almost all other language put promptly on the garbage pile of design ideas, where it rightfully belongs.

                                            1. 6

                                              Initially I thought so too, especially given experience with JS ASI mess, and uselessness of optional semicolons in Golang and Swift. But it’s a completely different thing in Rust, and it works surprisingly well.

                                              The combination of strict syntax, strong typing, and helpful compiler with auto-fixes ensures that accidentally inserted or forgotten semicolon isn’t a problem.

                                              Then it works beautifully with the “last expression in a block is the block’s value” rule. It makes the syntax so much more elegant and orthogonal: no need for ternary operators. let val = { block } makes immutable bindings practical.

                                              CoffeScript tried having something like that, and it was neat when it worked, but CoffeeScript lacked the other ingredients that made it work well in Rust.

                                              1. 6

                                                This is a much more eloquent expression of exactly what I had in mind with my original comment. The way semicolons work in Rust means that, rather than being an arbitrary line ending marker that serves as a crutch for the parser, they’re just another piece of syntax that expresses a concept in the language. A semicolon at the end of a line means “this line as a whole should not resolve to a value”, whereas no semicolon means the opposite.

                                                1. 3

                                                  ; is too nimble for a symbol to carry so much meaning.

                                                  I’d rather have something more visible like () for the uncommon case of discarding values than being “trained” by the language to put ; everywhere and then having to go back and fix the places where it was unintended.

                                                2. 2

                                                  Maybe taking the absolute worst implementation of semicolon inference is a bad idea to base a general judgement on.

                                                  Semicolon inference works and any language that forces me to write them manually has to have a lot going for it elsewhere for me to look over this design mistake.

                                                  1. 1

                                                    Semicolons are useful for oneliners; that’s it.

                                                3. 2

                                                  “almost all other languages” don’t have proper expression blocks, outside the functional ones. The ; in rust makes a lot of sense if you realize it’s a separator, not a terminator. a; b is just evaluating a, discards its results, and evaluates the same as b. OCaml does the exact same thing. You’re allowed to write { … a; } as a shortcut for { …; a; () } but that’s just sugar.

                                                  1. 2

                                                    Having an expression-oriented language does not require semicolons.

                                                    1. 2

                                                      I never said that. I said it made sense to consider ; as a separator in an expression language, in which blocks can (and often do) return values. It’s the case in at least Rust and OCaml, and works perfectly fine.

                                              2. 3

                                                What problems do you have with Ord and Eq? If it’s about how they work with floats, I for one appreciate how that works, due to the number of times I’ve caught folks using equality testing on floats in unit tests.

                                                1. 1

                                                  What problems do you have with Ord and Eq? If it’s about how they work with floats […]

                                                  I prefer designs where collection operations work reliably, regardless of the type of the values I put into them.

                                                  So not this:

                                                  Scala:    List(Double.NaN).contains(Double.NaN)     // false
                                                  Rust:     &[0.0/0.0].contains(0.0/0.0)              // false
                                                  Haskell:  elem (0.0/0.0) [0.0/0.0]                  // false
                                                  

                                                  It’s easy to get right, and there is little excuse to get it wrong for languages designed in the 21st century.

                                                  […] I for one appreciate how that works, due to the number of times I’ve caught folks using equality testing on floats in unit tests.

                                                  Not sure what’s the connection here, could you expand?

                                                  1. 2

                                                    Ah, so you’d prefer that collection operations depend on Eq instead of PartialEq?

                                                    This is one of those things where 50% of the people expect it to work one way, and 50% expect the other. If you enforce correctness with Eq, then people will complain about the lengths they have to go through to check that there’s not a NaN somewhere. If you allow flexibility with PartialEq, then you get the confusing behavior your illustrated.

                                                    This is why I push back against the “principle of least astonishment”. And it’s why there’s no such thing as languages “getting [this] wrong”.


                                                    I assumed you disliked the separation between PartialEq and Eq, whereas I like having the distinction. So I wanted to provide a real-world use case where too many people fall back on strict equality checking on floats, instead of doing a comparison with an acceptable range. But it appears you do like the distinction. And not only that, rust’s == works on PartialEq, when I thought it was based on Eq. Whoops.

                                                    1. 2

                                                      No, the problem is the way Haskell and Rust put partial and total orderings into a hierarchy, probably enticed by the extremely poor naming choices they made along with that.

                                                      Instead of Rust’s/Haskell’s approach, have two different kind of traits/typeclasses that are completely separate.

                                                      I named them Identity and Equality. Then you can provide two different methods that offer both suggested behaviors in the case of “is x in thing”?.

                                                      1. 1

                                                        Ah, I like that solution.

                                                        1. 1

                                                          If you are interested, here is an article series on this topic, and here is an example implementation of the concept.

                                              3. 3

                                                Strongly agree with the basic premise that more of Rust than is commonly assumed makes sense for non-systems programming. I don’t know even a tiny fraction of what Saorise does about how language design decisions play out, but randos at least have data about how languages feel to newbies, so scattered thoughts:

                                                • Agree lifetimes make sense. With current elision rules they’re rarely painful, they could supercharge escape analysis in a language with GC, and they avoid some kinds of aliasing errors.

                                                • I’m sort of for declaring in source what is potentially shared vs. private (with a copy able to turn private to shared), because it’s a relatively human-understandable distinction and unlocks some cool options for GC (per-thread GCs) and concurrency (elide some locks/atomics).

                                                • C-style “infectious” UB (as put into practice with LLVM poison values, etc.) is terrifying. (Whether unsafe should look different in an apps language is an interesting question.) Things to minimize how often it comes up, like a default initialization where possible and perhaps less-aggressive UB-based optimizations, seem interesting.

                                                • Understandably, Rust forces you to be explicit and sometimes repetitive about your dynamic memory management/concurrency strategies. This post describes it decently, and notes how im and im_rc have to be distinct crates despite their similarity. For an apps language I think you take as given the need to avoid much of that repetition (see Saorise’s mention of implicit unlocks), then do your best to minimize how often it turns out suboptimal (as Swift devs had to with ARC) or surprising (“I released and reacquired a lock there??”).

                                                • In the little Rust I’ve written, wrangling traits felt like lots of paperwork. You could alleviate some of the pain with trait aliases and tooling for “I used an extra operator so now I have to update a stack of declarations”. I also liked Go interfaces, though I know the fact that they’re structural not nominal will squick some folks out.

                                                • Monomorphization and heavy optimization are great because they help you build affordable abstractions without baking everything into the language – they make iterators work in Rust for instance. I think the line is that you apply it for fundamental tools in hot paths, but (for the sake of compile time and binary size) not all the time. Depending on the environment, JITs could help with decisions like this too.

                                                1. 2

                                                  What is meant by “control-flow-capturing closures” in this article?

                                                  1. 5

                                                    Ability to use break and continue inside the closure to affect outer loops.

                                                    1. 2

                                                      An alternative is to remove break, continue, return and throw altogether and benefit from control-flow that is vastly easier to follow due to all the things that cannot happen.

                                                      It’s a bit like not having null as an accepted value for references, as nulls doubles the amount of branches you have to consider every time you touch a reference.

                                                      1. 1

                                                        This sounds fairly radical (and hence interesting), any pointers to examples?

                                                        1. 1

                                                          A language I’m contributing to is hopefully moving into this direction.

                                                          throws is already gone, return might not be necessary anymore soon (and never worked that way in closures), loop was dropped, hopefully continue and break will go in the future, too, but some other things need to work first for that to happen.

                                                          (The lang had never ternary operators, and I’m working on making if-expressions general enough to not need separate pattern matching syntax.)

                                                      2. 1

                                                        Wouldn’t this mean that you can no longer count on applying any closure to actually return?

                                                    2. 1

                                                      ‘fixing’ rust by adding wasm does not seem like a smart idea. Now you just have two complicated problems.

                                                      1. 2

                                                        What does this comment even mean?

                                                        1. 1

                                                          It means you think you had one problem, rust being too large … To fix it you make it larger by dragging in the whole wasm ecosystem and making it mandatory. Now instead of just having compilers, you have JIT compilers, which are more complicated.

                                                          1. 2

                                                            “rust being too large” is about complexity in the language, not the toolchain.

                                                            1. 1

                                                              So a simpler language with a more complex toolchain is going to be better?

                                                              1. 2

                                                                Your personal feelings about WASM are overall irrelevant to the topic of this submission.

                                                                1. 1

                                                                  The submission specifically says removing support for anything that is not WASM in order to reduce scope. My comment is attempting to say I think this will have the opposite to the intended effect.

                                                                  Consider a C compiler that just emits assembly code directly like https://github.com/rui314/chibicc. I would argue this has a smaller scope than a C compiler that emits WASM which must be passed to a runtime compiler which must then convert the result to assembly.

                                                                  Feel free to disagree, but it is relevant to the post.

                                                        2. 2

                                                          The goal isn’t to fix Rust, it’s to make a language optimized for applications (where, for the sake of speed of development, we can accept some overhead and the compiler or runtime can make more choices about memory) where Rust is optimized for systems (precise control over memory with zero overhead), while carrying over some things about Rust the author sees as useful in both domains.

                                                          Efficiently executing WASM is indeed a complicated problem, but it’s a problem for WASM runtime implementers. It’s similar to how many other new languages compile to JVM or CLR bytecode, LLVM IR, or JavaScript: it helps the language implementer(s) focus on what they’re doing new while deferring part of the work to pre-existing projects. Since it’s a lot of work to write a new competitive optimizing compiler these days, targeting a virtual platform like that is a common choice.