Ask HN: What's the best “higher level Rust” these days?
To me, these are the things that I like about rust that I'd want to see:
* Getting nullability right, not making the "billion dollar mistake". An integer shouldn't be able to be "null" unless it's part of the type.
* "Algebraic" (Sum and Product) data types. I love Typescript's "integer | null" syntax, but Option<int> or something like that is fine, too. Typed structs/records or tuples.
* Errors as values. Goes naturally with the ADTs, and it feels very robust having the compiler enforce handling all the "Ok(..) | Err(..)" results.
* Focus on the data, defining structs and functions that work with structs. "Traits" as a means of abstraction. Not much in the way of inheritance.
* A nice development experience with a good package manager or other way of finding and using open source code.
* Vibrant and growing community.
* Can develop comfortably on Mac, deploy to Linux. Windows support a "nice to have".
Rust just nails all these things so well, it makes me want to use it even for things that don't seem like a good fit to me, like command line tools or web servers or scripts.
Here are the languages I can think of that might work, but I think they all have some issues:
* Ocaml - Rust's heritage, so it makes sense. But I feel like it's been around forever and hasn't really taken off. And doesn't it still have some sort of concurrency issue?
* Swift - So close to the ideal language, but does it work well outside of the Mac ecosystem?
* F# - Conversely, another great language, but does it work well outside of Windows?
* Java - The last time I used Java in earnest (many, many years ago) it still had the nullability issue, and was too class/inheritance focused for my tastes. I'm not sure if modern Java has improved on this yet.
* Typescript - Is this it? As a language, it actually ticks all the boxes, I think, which surprises me. But as a runtime it's nonexistent, and so you have to compile to Javascript and run on node and all that. I use it and really like it on the frontend. But how is it on the backend for ordinary non-web applications? How's its performance? It feels fiddly to get all the build stuff running, but maybe Deno or Bun is an option here.
* Nim - Kind of off the wall here, but it looks like it might actually be a contender from perusing its docs. But its community is pretty niche, and I don't have a good sense on if its growing or not.
62 comments
[ 2.7 ms ] story [ 115 ms ] threadAnd I appreciate that it has the "errors as values" like I was asking about, though, the types don't really force you to handle them, I don't think?
But it's definitely no Rust. It's a very small language by comparison. That's a positive for me, but you can't have all the Rust things and that at the same time.
I'm not a "1 language for everything" guy. I tend to choose for the task if given the choice or choose for the team so I'm not stuck being the only one that can maintain it. And for anything more than a toy for myself, I always pick something mainstream.
If I'm slapping something together as a 1-off, I tend to go with Python. If I'm writing a CLI, I tend to use Go and I'm using it more for backend stuff. I'll use Java if I have to, because of the team thing. But I won't like it. For toy stuff, I'll pick a language out of a hat just to learn something new. Every few years I'll use a LISP/Scheme.
1. https://deno.land/
You are probably referring to stack, which is a just wrapper around cabal that makes some decisions for you. For instance, it pins a set of packages to known-to-work-together versions. It also provides a different CLI (cabal's is a bit verbose).
Today, cabal has improved quite a bit (Nix-style builds, for instance), so the pain stack was invented to help with isn't really there anymore. If you invented stack today, it probably wouldn't get the same level of adoption.
But there are plenty of good packages out there. There often are many different packages for the same thing with different approaches (best part of Haskell!), but for core infrastructure the ecosystem tends to settle and rally around one.
[1] https://github.com/clash-lang/clash-compiler
But if Windows support is more important than a little bit of cognitive overhead? Just stick with Rust. It works, it's all the good things with few of the bad, it's more maintainable than anything mentioned, and the best language to write anything in is the language you already know.
It convinced me the language doesn't really support Posix systems after all.
(Obviously, Linux software that's been written in C# exists; maybe I was using the wrong standard library or something. I went with whatever Microsoft's documentation suggested.)
Other than that I'm these days only mildly interested in languages that do not compile to WASM or transpile to JS because NodeJS and the browser have become my main platforms. IMO browsers offer an unprecedented way to develop GUIs for applications. People are unhappy with the perceived bloat of it but there's a web browser on almost each single consumer-facing (i.e. non-backend / server) machine these days and those things have become very, very capable. I just feel deprived of something when I go back to, say, Python.
I'm not very interested in TypeScript anymore b/c for the troubles and the benefits that typing gives you I want to have a solution that works at run time and ideally at compile time, too—I'd rather forgo the latter but not the first b/c of user input validation. Having to entertain that whole fancy and complex beast that is a good type system only to not be able to use it for run time checks is not what I want.
Just a few thoughts.
Personally speaking, if Swift’s Windows/Linux support improved and a cross platform UI framework were built for it, I’d have a hard time imagining using anything else for cross platform desktop development. Swift gets a lot right for that use case and strikes a good balance between ergonomics and safety.
Furthermore swift's lack of macros means you're at apple's behest to have important changes to the language made. Rust didn't come up with serde, arguably one of Rust's "killer apps"; it's a third party package that the language merely makes possible. But in swift, such a thing would be impossible to do at compile time without explicit support from apple. This means that language can only provide functionality blessed by apple.
Swift is a neat language but it's got a huge uphill battle if it wants to become widespread and I just don't see it happening when all it offers over rust is that you don't have to worry about lifetimes.
And as alluded to in my post, Rust isn't particularly pleasant to use for a UI use case anyway.
Of course there is no cross-platform UI framework for Swift either, but if such a thing were to come into existence I think it'd quickly come to enjoy decent popularity for desktop application development.
I don’t really agree that macros make or break a language though. And swift’s codable generation was a community driven proposal; like all language evolutions have been
* Getting nullability right - Tick.
* "Algebraic" (Sum and Product) data types. Tick.
* Errors as values - Tick.
* Focus on the data - Tick.
* A nice development experience - Tick (has got a lot better in recent years).
* Vibrant and growing community - Tick. Hard to know if it's growing, but it feels like it has a very stable niche imho. I see a lot of dynamism in the communities I follow like Typelevel.
* Can develop comfortably on Mac, deploy to Linux. Windows support a "nice to have. Tick. I've developed on all of these environments.
Is it a "higher level Rust?". It's a very different beast but, based on your checklist at least, it fits the items them than most other mainstream languages imho.
On top of that, the classloader makes an open world assumption, at least with the JVM, so the compiler can never prove me wrong on any of the above points, regardless of what program it's been handed.
Anyway, I've been there and done that with Java, and I'm hoping to never put up with that ecosystem again. I'd much rather fight the borrow checker than deal with null pointer and class cast exceptions in production.
Once you tap into the Java ecosystem, you can often wrap things into the typelevel ecosystem and work with effects. Generally, Scala with cats/cats-effect is quite pleasant.
Your criteria are so specific that it will be easier to just learn how the borrow checker works.
TypeScript would have my vote. I have my eye on Deno and Bun but they aren't there yet, Node gets the job done every time.
Reference counted languages provide 99% of the benefit of full GCs for simple programs, and I think those two suggestions are enough to mindlessly appease the borrow checker in most circumstances.
Rust can be a simple(r) language if you're willing to set aside the idea that you need to write performance-critical systems code from day one - just use the basics and optimize if/when needed.
If you wanted to optimize a file, then just run the "eject" command on a file and then replace the extraneous Rc<>/Arc<Mutex<>> stuff with smart people borrow checker stuff yourself.
I like crystal and nim, but it would be better to have such a language deeply just actually be rust under the hood and use rust libs natively and use crate literally instead of creating a whole new ecosystem.
That said I think it has many parallels with Rust and I enjoyed reading this comparison between the two https://github.com/Dhghomon/rust-fsharp
It shows how strikingly similar the two languages are, at least on the surface.
I'm building my first library in OCaml for a number of great reasons: 1. the type system we all want 2. multicore is here (but in RC) 3. algebraic effects is here (but in basically RC) 4. build system is pretty good after you warm yourself up a bit. i ran into some weird dep issues recently but overcame it by vendoring deps directly.
A hard requirement for me is to be able to generate native executables, and only OCaml / Rust / Swift has this with a more modern type system. If I wasn't so focused on learning in an fp language, I'd use Rust for my needs.
If you asked for a vote, I'd say give 2 weeks for each of your top 3 languages. Opinions and anecdotal experience will always be beaten by personal experience.
Companies like Azure and Meta now recommend Rust as either one of several viable choices, or THE recommended option, for the use cases you describe, including CLI and web servers.
Personally at this point, just about anything that's not a frontend (android, ios, web), I think Rust is the best language for the job. It's the language I prefer, and feel most productive in, for the use cases you're describing (web server, CLI, small scripts and utils).
There's a learning curve, but at this point, I almost entirely forget about the lack of GC. It feels like writing code in any other language - except of course with all the benefits you call out (no null, ADT, typed error values, trait-based OO system, the best tooling of any language I've used, expansive 3rd party libraries).
I would say that I've tried most of them and they are all varying levels of aweful right now.
If, self-deception aside, you really are a superior programmer, it's an excellent tool, a joy to use, and is going to be a good choice for an increasingly wide spread of domains.
Like you I've been on a basically identical search, but instead of coming up from Rust I've come down from dynamically typed languages on the backend. I just couldn't bear another run-time error that would have been easily prevented with Typescript.
I've come to the conclusion that the palette of ergonomic and statically typed languages for the backend is surprisingly quite sparse (when one includes the requirement of a relatively sizable community) if you don't include Java/Go.
Languages I personally crossed off:
* Java - Just much too verbose for my taste, too mutable, too nullable, and I don't find "Kingdom of Nouns" elegant to program in. (The JVM is quite magnificent though..)
* Golang - I get why people like it, and I have absolutely nothing against it. For me it's not expressive enough.
* Typescript - Pretty much 100% what you said. The language itself is wonderful, but it really shines when confined to the browser. The runtime differences between the Browser/Node cause all sorts of problems. Also compiled binaries are nice, which are out here.
I see why somebody might choose each of those languages, and be highly justified in doing so, but for me they didn't fit the bill.
That leaves:
* Scala - Only have very light experience. Really strong support for functional/immutable style programming, and checks all the boxes as another commenter pointed out. Community seems a bit complex at times. I haven't had time to really dive into it, but it's been on my mind.
* Rust - Seems like it nails a lot of things really well. Haven't ever touched it, so I can't give an educated appraisal of its merits. The only real obvious con is that it's "overkill" for systems dealing primarily with business logic, i.e. where GC isn't a big issue.
* Swift - Seems quite excellent, but I had written it off as unsuitable for general purpose stuff. Perhaps a premature take?
* Nim - The one on the list I have played around with the most. Seems to have a very powerful macro system which I have yet to take full advantage of. Feels like a statically-typed python, with a bent towards an imperative style over a more functional one. I am personally thrilled they went with the decision to make "func" a fist-class keyword, with special compiler significance. Still a bit niche however.
It has a pretty big ecosystem for all sorts of stuff, but on top of that it has native Java interop, giving you access to a huge selection of libraries.
There's also Kotlin Native, which compiles to native code using LLVM and ways to compile it to JS.
Basically, it runs pretty much anywhere.
According to dev surveys, usage is pretty evenly split between mobile and backend, but you could even do desktop or web apps with it if you wanted.
Fixing this is pretty much the major pitch of Deno.
F# is cross-platform with excellent tooling from vscode via ionide extension, jetbrains rider, and visual studio, it also ticks several boxes from above :)
Feel free to chime in twitter there's the #fsharp hashtag, The F# Org slack https://fsharp.org/guides/slack/ and the unnoficial F# discord server https://discord.com/servers/fsharp-196693847965696000
F# if you want functional C#.
Scala if you are adventurous.
F# is awesome if you ignore the fact that it's so easy to shoot yourself in the foot from a performance standpoint with all the lazy constructs. It sure is awesome though to have such a strict type system, though I do sometimes miss features that Typescript has, such as structural typing and mapped types.
In general I do thing the dotnet ecosystem offers a really good set of compromises.
* It's garbage collected
* Has algebraic types (enums similar to rust's)
* Pattern matching as good as rust's
* Null check strictness can be configured with the compiler
* Package manager
It's somewhat OOP geared though.
I’ve been working to build more libraries and community for embedded software. It’s been slow going but a couple of companies are using it in shipping products. Nim leverages existing C code very well though.
Though Nim does have its downsides, primarily a smaller community. It’s small but active and not going away anytime soon. I think the ecosystem is slowly building a solid foundation of core libraries since the language reached 1.0 and found its feature set. But it doesn’t have a “killer” feature or single library to drive it. So hopefully the slow and steady buildup can keep going. For example, one big investor in Nim ecosystem for example just hired someone to work full time on the language server / tooling. There’s more work going on with numerical processing too (eg native graphing libraries, etc).
For me in many areas the language more than makes up for the smaller community. I can wrap C/C++ libraries quickly and I don’t need to do borrow checker. Though I would say it sounds like Go or Swift might fit some of your preferences too. Rust is great in many ways but I am more productive with “compile time duck typing” while Rust favors traits and fixed predefined abstractions.
I think you've mixed yourself up somewhere here. A garbage collector doesn't protect you from stale references, it just makes sure that it results in a program crash (or exception of some sort) rather than letting your program run off into fantasy lands where pigs fly and everyone casts magic spells. Rust still protects you by turning these errors into compile time errors, saving debugging time.