Almost five years ago, Saoirse “boats” wrote “Notes on a smaller Rust”, and a year after that, revisited the idea.
The basic idea is a language that is highly inspired by Rust but doesn’t have the strict constraint of being a “systems” language in the vein of C and C++; in particular, it can have a nontrivial (or “thick”) runtime and doesn’t need to limit itself to “zero-cost” abstractions.
What languages are being designed that fit this description? I’ve seen a few scripting languages written in Rust on GitHub, but none of them have been very active. I also recently learned about Hylo, which does have some ideas that I think are promising, but it seems too syntactically alien to really be a “smaller Rust.”
Edit to add: I think Graydon Hoare’s post about language design choices he would have preferred for Rust also sheds some light on the kind of things a hypothetical “Rust-like but not Rust” language could do differently: https://graydon2.dreamwidth.org/307291.html
I didn’t love that article - Rust isn’t strictly a systems language. It’s general purpose, and a lot of the mechanics are very useful for general programs.
I feel like you may have missed the point, then? Or at least interpreted the article very differently? Rust isn’t “strictly” a systems language, but neither is C or C++; people use them for application development all the time. But all three languages have very specific limitations (most obviously, that adding a garbage collector would be an unwelcome change) imposed by the need to fulfill the “systems” niche.
Compare Golang: it can’t replace C++ for every use-case, because it has a garbage collector, and because you need cgo to use FFI. But it’s otherwise a very flexible language that can be used for most types of software.
What I would like to see is something that shares these advantages with Go:
- quick to build
- easier to teach & learn than Rust
- easier to quickly prototype with than Rust (though of course it’s debatable how well Go does at this one)
…but I don’t like the actual language design of Go, and I think it’s possible to design a language that’s more Rusty but still simpler than actual Rust.
For instance, error handling in Rust is both more ergonomic and more rigorous than in Go. That’s huge! A language like Go but with sum types,
Result
, and the question-mark operator would be leaps and bounds nicer than Go itself.To be clear, I don’t imagine that a “smaller Rust” would replace Rust. But I also don’t think we’ve reached optimal language design when the language I’d pick to write an OS is also the language I’d pick to write a small CLI app.
The designers of Go actually discussed civilised error handling (like Rust and others), but in order to make it useful they’d have to include other features like ADTs and something like an
Either
monad (Result type), which they felt would make Go too difficult to learn for the developers they envisioned Go to be used by.One of the most important reasons for the popularity of Go is exactly that it is extremely limited, and can be picked up by any developer in a few hours. It takes no time to learn because there is nothing there that would need to be learned. This isn’t a limitation, it’s a feature (opinion of Go designers, not mine).
The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
- Rob Pike
Jeeze, I knew that simplicity was the goal (and I think they largely succeeded in that), but that quote is so explicitly condescending. “They’re not capable of understanding a brilliant language”?
I disagree slightly with the need to add full ADT support to the language to implement that style of error handling, because Pike et al. had no problem adding “special cases” to the language: in particular, return values are essentially tuples, but that’s the only place in the language with that concept. So they wouldn’t need to introduce user-definable enums and full pattern-matching to have better error handling. I can think of a couple approaches they could have used:
- Use compound-return-values as they currently exist, but have additional compiler-enforced restrictions:
- There can be at most one
error
value in the return types, and it must be the last element in the “tuple” - when returning a non-nil
error
, the other values must be zero/nil - the compiler would require all errors to be checked, never ignored (Go should have at least done this, even without the other stuff)
- Add the question-mark operator, which would do basically the same thing as in Rust: check if the error value is
nil
, discard it if so, return early if not
- There can be at most one
- Have a special “result” type that is quasi-generic (like slices and maps) and treated as a sum-type by the compiler, but which can only be used as a return value from functions. Provide some special variant of the
switch
statement to destructure it (akin to how type switches have their own bespoke syntax/keyword).
- Use compound-return-values as they currently exist, but have additional compiler-enforced restrictions:
Rust is a groundbreaking language, but it’s not without tradeoffs. There are loads of things it makes extremely difficult compared to slightly higher level languages.
I used it happily for years, but I wouldn’t recommend it for any project that didn’t explicitly need the low-level performance optimisation.
Extremely hard disagree on the last statement. It certainly has tradeoffs, but they are almost all very valuable to many general applications which don’t need performance at all. I’ve been using it professionally for a very long time now and migrated multiple companies from JS, Python, Java, and C# to Rust and it brought huge advantages.
I believe you, but for it to be a fair comparison you’d need to compare to an alternative rewrite, not to the original software.
Rust has plenty of merits. It has a very readable functional style, single aliasing to reduce complexity, powerful libraries for stuff like generating serialisation code, and cargo is incredible.
However, expressing any complex graph structure in Rust is just painful, and so is refactoring code. Small changes in intent require complex reworking of data structures, because Rust forces you to be extremely specific about data layouts at all times. These issues crop up constantly in any complex project, and they really slow things down.
Although Rust is a nice language, you can now write functional code with immutable data structures in pretty much any modern, statically-typed language. C#, Kotlin, Scala, Swift, etc. It will be concise, quick to write, easy to modify and pretty fast at runtime.
Perhaps I’m mistaken in some way, but this has been my honest experience after many years using Rust.
That hasn’t been my experience at all, and it’s been for both large refactors as well as complete rewrites.
Rust does care about some things like not having self referential structs or recursive types, but those are super easy to fix. Rust pushes you to not write code in the same way as other languages, and IMO that’s a very good thing. It’s not at all about systems stuff or memory layouts.
Rust’s ownership system is used to simply enforce correct usage of APIs. Memory safety is simply a subset of correctness. Many other languages, Java for example, don’t enforce thread safety, so you have to be really careful when parallelizing your code. In Rust, you could hire an intern fresh out of high school and I can know 100% that they’re not making mistakes with sending data across threads that isn’t thread safe.
Another example is file handles. Rust is the only mainstream language where it’s not possible to read from a file handle after it’s been closed. That has nothing to do with memory layout or systems concerns. That’s a basic invariant across all languages, and Rust stops you from making a mistake. Same with things like mutating an iterator during iteration and all kinds of other stuff.
That does mean it is more painful upfront, but that’s a good thing. You’ll run into many of the same problems in other languages, but at runtime, which is much worse.
As for graphs, I doubt the vast majority of programmers need to build custom graph structures.
You’re of course free to disagree. Just weighing in with my perspective.
I appreciate your perspective, thanks for taking the time to share it!
I also agree with most of your points in favour of Rust. It is clearly the biggest programming language design breakthrough in decades.
Why would you want that? What is wrong with python if you want an interpreted language with garbage collection? By contrast what is wrong with rust + a lot of crates (or C++/Ada/…) if you want a compiled language?
Zero cost abstractions are great because speed is very important for complex problems. Little things here and there make for modern computers that feel slower than my old 8 bit atari when trying to get work done.
There is a huge and valuable possibility space between python and Rust. We know this because it is already occupied by many extremely successful languages (Java, C#, Swift, etc).
The value of a language that sits between C# and Rust also seems pretty obvious at this point; a language that gives you Rust’s memory management tools for optimisation, but doesn’t force you to use them for all of your code.
It exists, it’s called Rust with lots of
Arc
,.clone()
, andBox<dyn Trait>
. You don’t have to borrow if you prefer to have a slower, easier program.I know Rust has these features, but they are intended to be features of last resort and it shows. It’s not a criticism; Rust pushes people relentlessly towards safety and performance, and is extremely successful at that.
I am imagining a language more like Pony, but with less of a focus on the actor model. I could use a language like that to write a high performance game engine with no GC pausing issues, and then write very high level gameplay scripts on top of it in the same language.
You could do that in Rust, but the Rust game engine space has already made it clear that most people feel the need for a scripting language.
Box<dyn Trait>
Now try to do that with a trait that isn’t object-safe…
I get your point, these things make fighting with the borrow-checker a little bit less annoying, but Rust is complex. I’ll happily accept that because I value high code-quality (to that point that I rather invest more time to get things right) but when that is not the goal and you want something higher-level and strongly-typed there are alternatives that work better (I’m just talking about the language itself, ecosystem alone for me is yet another pro-Rust thing)
Copying what I wrote in another comment:
What I would like to see is something that shares these advantages with Go:
- quick to build
- easier to teach & learn than Rust
- easier to quickly prototype with than Rust (though of course it’s debatable how well Go does at this one)
…but I don’t like the actual language design of Go, and I think it’s possible to design a language that’s more Rusty but still simpler than actual Rust.
For instance, error handling in Rust is both more ergonomic and more rigorous than in Go. That’s huge! A language like Go but with sum types,
Result
, and the question-mark operator would be leaps and bounds nicer than Go itself.To be clear, I don’t imagine that a “smaller Rust” would replace Rust. But I also don’t think we’ve reached optimal language design when the language I’d pick to write an OS is also the language I’d pick to write a small CLI app.
Learning a programming language is not hard. there are thousands of choices. Before you ask for another one, first please check that there isn’t already one that meets your needs. Fragmentation of languages is not useful in general. It is rare to have an idea that hasn’t been tried before, so find someone who already has done that idea.
… I am currently asking if there is a language that fits the description. And I’m clearly not the only one interested in such a language.
I personally have not found Rust onerous to learn, especially by comparison to C++. But a lot of people do find the learning curve steep.
Coming from some one who used 4 different languages (C#, C++, Python, and G’MIC), I just feel more comfortable when there’s a explicit end blocks, which is why I don’t like Python. Of all of those languages, only Python does not make that explicit end block which is off-putting in my opinion, and there isn’t any other options with the similar role to Python.
Ruby often comes up as a python alternative. There are a lot of other lesser known choices - but any of them will still be more popular (at least at first) than something you come up with and thus any of the will give more community support.
You mean a interpretative language with similar role to Python, but more like Rust/C++ style? I actually want that so that I can ditch Python even if I learned it and use this instead.
Not necessarily interpreted, but possibly. I think a more likely path is something like Go that’s compiled but still has a garbage collector.
If you use a garbage collector the whole borrow checker would not make any sense.
Do you want to have error handling and functional paradigms in go? I think you should start there and ask go Devs why their language is lacking such basic stuff.
I spend an inordinate amount of time at my C# day job adding documentation comments about exclusive access and lifetimes and ownership… things which are clearly important but which dotnet provides little or no useful support for, even though it has a perfectly good garbage collector. The dotnet devs were well aware that garbage collection has its limits, especially when interacting with resources managed outside of the runtime, and so they added language features like IDisposable and finalisers and GCHandle and SafeHandle and so on to fix some of the things GC won’t be doing for you.
I’d happily use a garbage collected language with borrow checking.
Sounds like you’re using C# for something that it wasn’t designed for. You can, of course, but it is obviously painful
Not sure if this is what OP is seeking, but I would be fine to have borrow checker removed, replaced with Garbage collector like Go/Python in such a language.
To build prototypes, I don’t want to fight with borrow checker and neither I care for efficiency much. But I do want the macro system, traits, lazily asynchronous runtime, cargo like package manager, easy build system, etc.
Rust has so many powerful features, but only because of borrow checker (IMO) we can’t use it for rapid prototyping like Python. With that replaced, this subset of Rust would be something which can be a great contender to Python/Go, etc.
The borrow checker handles more than just freeing allocated memory, it will also prevent data races and invalid concurrent access aso. I personally don’t have any issues with using garbage collected languages, but the fearless concurrency is nothing I’m willing to give up.
Oh, I agree.
My worst experiences with Python are related to running multiple processes of which share anything. Rust was far easier in that.
Looks like interpreted Rust would be my only demand for Rust to shine in prototyping world.
Honestly, prototyping is exactly the kind of thing where I don’t want to think about all the crap that Python doesn’t check itself while Rust does. In a long-term project I could begrudgingly learn every data structure and process well enough to do the compiler’s job for it but if the code is very new or changes constantly I want as much support from the compiler as possible to avoid having to remember all of that.
To build prototypes, I don’t want to fight with borrow checker and neither I care for efficiency much. But I want [rust features]
Maybe we just need a preprocessor that adds clone, reference counting and RefCell wherever needed.
To build prototypes, I don’t want to fight with borrow checker and neither I care for efficiency much.
Then use
.clone()
orArc
everywhere?Why would you want to prototype incorrect code? You don’t fight with the borrow checker. The borrow checker prevents stupid mistakes. Anything that is correct that the borrow checker rejects is almost certainly a very bad idea in a prototype
Then Rust is the wrong language for you. Use the right tool for the right job.
Yes, and that’s what the post is about.
Saying that Rust is not the right tool for this job, what other tools exist which are similar to Rust but also do the job.
I don’t have the answer though. Just came to add my thoughts.
Did you read the original “Notes” post? I thought it did a pretty good job of explaining why Rust-like ownership semantics are not necessarily at odds with having a garbage collector.
F#? It’s compiled, statically typed, somewhat fast, garbage-collected, and supports Rust-style error handling
Definitely a good pick! I haven’t learned it but I’m aware of some of its features, and it does seem promising.
I think inko is close to matching this description.
I’ll check it out; thanks!
The biggest difference I see is allowing multiple mutable borrows, which would make a lot of things easier, but aside from that it sounds like it would have most of the same usability problems as Rust.
I’m also a bit put off when I see LLVM advertised as the primary compiler back-end these days. Pretty much guarantees slow builds.
I think I saw something called ‘Rune’ that might fit the criteria pretty well. I didn’t dig really deep.
I’m not joking. If you want something that’s very similar to Rust, but doesn’t have the restriction of being a systems language, then Haskell might be the right thing for you. Unlike Rust, Haskell is Pure Functional though, so it forces you to have an even cleaner architecture.
If Pure Functional isn’t your beer, then you could also check out the language that inspired Rust: ML. If I remember correctly, Rust was started as “something similar to ML, but suitable for systems programming”. So, it only feels natural to take an ML dialect if you want “something similar to Rust, but without the restriction of it being suitable for systems programming”.
A popular ML dialect would for instance be F#, which is built on top of the .Net runtime and is therefore compatible with C# and the likes. On the other hand, in order to make it compatible with C# and the likes, there are some weird compromises in F#…
Or, if you (like me) dislike the idea of having a Garbage Collector, you could go for Lean4. That’s what I’m learning currently, and it feels a bit like a bastard child of Haskell and ML.
Im surprised you didn’t mention OCaml or Elm
I did mention ML, of which OCaml is a dialect. Afaik Elm doesn’t have type classes (aka Traits) - a property I would consider necessary to call it “similar to Rust”.
Fair points.
OCaml seems really close, but I’m told that there are problems with its concurrency story. I do think it sounds like a really good language.
I’m curious if you were told that recently. I know that there have been stable releases of major features and libraries concerning concurrency and parallelism near the end of 2022. It may be much improved since you your source last looked. Or it could be a limitation in the implementations of these.
My understanding was that there’s some ecosystem bifurcation, somewhat like Rust’s. But I’ll look into it again!
Oh, yeah. The Jane Street vs non-Jane Street library incompatibilities still exist. But there is a new concurrency library that was made such that the need to use monads has been eliminated.
have an even cleaner architecture
Although I’m fully in camp functional, I doubt that. There are problems that are inherently stateful and rely on mutability. Modelling that in Haskell often results in unnecessary abstractions. I think Rust hits a sweet spot here (when you’re that experienced to write idiomatic Rust, whatever that exactly is). Also being lazy by default has its own (performance) implications, strict + lazy iterators (like Rust) is a good default IMO.
I do want to learn Haskell some day, but it seems like it has a whole different set of reasons why it’s tricky to learn; and I hear enough about the more complex features (e.g. arrow notation) having compiler bugs that I think it really doesn’t sound like a “smaller” or “simpler” language than Rust.
That said, yeah, it definitely meets the criteria of having strong typing, a functional style, a garbage collector, and pretty good performance.
can have a nontrivial (or “thick”) runtime and doesn’t need to limit itself to “zero-cost” abstractions.
Wouldn’t that be a bigger rust rather than a smaller one?
Not an area I’m particularly interested in, given that I do embedded and hard realtime development. Rust is the best language for that now, I just which allocations were fallible as well. And storage/allocator API was stabilised.
Not unless you consider Go a “bigger” language than Rust. The blog post means “smaller” in terms of what the user has to learn and think about, rather than smaller in implementation size or resulting binary.
I would indeed consider Go a bigger language, because I do indeed think in terms of the size of the runtime.
But your way of defining it also makes sense. Though in those terms I have no idea if Go is smaller or not (as I don’t know Go).
But Rust is still a small language by this definition, compared to for example C++ (which my day job still involves to a large extent). It is also much smaller than Python (much smaller standard library to learn). Definitely smaller than Haskell. Smaller than C I would argue (since there are leas footguns to keep in mind), though C has a smaller standard library to learn.
What other languages do I know… Erlang, hm again the standard library is pretty big, so rust is smaller or similar size I would argue. Shell script? Well arguably all the Unix commands are the standard library, so that would make shell script pretty big.
So yeah, rust is still a pretty small language compared to all other languages I know. Unsafe rust probably isn’t, but I have yet to need to write any (except one line to work around AsRawFd vs AsFd mismatch between two libraries).
Go is a “small” language in the sense that it has an exceptionally small number of concepts (i.e. features and syntactic constructs); someone else in this thread made a comment to the effect that it takes almost no time to learn because there’s nothing to learn if you’ve already learned a different language. This is of course an exaggeration, but only slightly: Go was very intentionally and explicitly designed to be as simple as possible to learn and to use. As an example, it famously had no support for generics until almost 10 years after its 1.0 release. I think that when talking about the size of a language, some people do include the standard library while others don’t; Go has quite a large standard library, but you don’t actually have to learn the whole library or even be aware of what’s available in order to be productive.
I personally don’t think it makes sense to include the standard library in the “size” of a language for the purpose of this thread, or Boats’ original blog posts. The fundamental point is about the learning curve of the language and the amount of work it takes to produce working code, and a large standard library tends to be more convenient, not less. Common functionality that isn’t in Rust’s standard library tends to come from libraries that become almost ubiquitous, such as serde, regex, crossbeam, and itertools. From the user’s perspective, this isn’t much more or less complicated than having the same functionality available via the standard library. (Of course, a large standard library certainly makes languages more difficult to implement and/or specify; if I recall correctly, about half the size of the C++ standard is dedicated to the standard library.)
I don’t really know how to fairly compare the “size” of Rust and C++, partly because Rust is so much younger, and several C++ “features” overlap with each other or are attempts to replace others (e.g. brace-initialization as a replacement for parentheses). But I don’t think I’ve ever heard someone claim that C++ is “small” or “minimal” in this sense, so it’s in no way a good point of comparison for determining whether Rust is “small”.
Edit to add: for what it’s worth, if I weren’t quoting Boats’ blog post (which is sort of the “canonical” post on this concept), I probably would have opted for “simpler (to learn & use)” rather than “smaller.”
If you want a high-level, convenient Rust, it’s already there: It’s Rust with liberal use of
Arc
and.clone()
andBox<dyn Trait>
and so on. If you want things to be convenient instead of efficient, Rust already has everything you need.If you think that’s convenient, then I fear you may never have experienced a convenient language.
What is a convenient language exactly?
Although I think the arguments are not exactly pro-Rust (they already show complexity with something like
Box<dyn Trait>
).Sure hacking something quickly together with python is quite a bit faster/easier/less mental overhead.
But long-term and IDE experience IMO Rust is the most convenient language (mind you I programmed in ~10-20 languages, and the whole DX is best with Rust IMO (cargo, rust-analyzer etc.)), as it forces you to write a clean architecture and the strong type system keeps it maintainable. While refactoring can feel cumbersome, I almost always had a clean and working/correct (often without tests) code afterwards (when all the compiler errors are fixed).
That said Rust is of course not perfect, all the strong-typing, zero-cost (async is certainly not something I would recommend beginners) systems-programming features make it complex at times (and the type-system can get in the way at times, e.g. trait-object-safety, or not “simple” higher-kinded types). So when that is annoying and control over the exact memory is not that important, something like OCAML or Haskell may be better.
“Faster/easier/less mental overhead” is indeed exactly what I mean by “convenient”.
Maintainability is very different from “convenience”, and I think we’re both in agreement that Rust makes the correct tradeoff by favoring maintainability over convenience. But that doesn’t mean that maintainability implies convenience!
I strongly prefer to write Rust versus “convenient” languages such as Python, Ruby, and (my least favorite, but the one I use most often professionally) Go. But that doesn’t stop me from appreciating the benefits of “convenience”; and I think that there is room in the language design space for a slightly different tradeoff: something that isn’t usable everywhere Rust is (e.g. it presumably wouldn’t ever be a candidate for inclusion in the Linux kernel) but still has many of the same maintainability advantages.
“Faster/easier/less mental overhead” is indeed exactly what I mean by “convenient”.
How different the conception of convenient is :P
I think it’s super convenient to just do
cargo new <project>
, start hacking, have superb tooling/expressiveness/performance etc. And it works remarkably well and fast if the problem space is not self-referential etc. or a lot of mutability is in play (I’m probably faster in Rust now than in python, but that probably has to do with the amount of time I’m spending with it…). But I get your point, and I think there’s certainly better languages for certain problems (and I even started one myself some time ago, because I wanted something like Nix but with strong typing (anonymous sum/union types/sets etc. similar as typescript))You agree with me so strongly that you started designing your own language?? Then why didn’t you lead with that, since the post was asking for neolang recommendations??
Well the project never left its roots, it’s a still a simple system-f implementation, and a lot of ideas. I’ve put it on ice, after seeing how much involved there is with questionable outcome (and I need to dedicate a good amount of time to get the theory right, it’s not that I have year long research experience background in type-theory etc.). There’s more concrete problems than designing yet another language… Maybe I’ll come back to that project at some time.
Anyway the idea was to have something like Nix but more general (including derivations, possibly controlled side-effects). Closest to that currently would be typescripts object type, Haskell (overall static typing), crystal-langs union type and nickel (which is less ambitious (probably for good reason)).
I guess we’re coming at this with different opinions on what is convenient. Python is extremely easy and quick to write - but then writing more code for tests than the actual program itself because the compiler catches next to nothing and still dealing with the occasional runtime error is highly inconvenient to me. I look at the “ease of use” - or painlessness if you will - of a programming language over the whole lifecycle of the program, not just initially writing it.
Initial development is like 2% of the total effort over the whole lifecycle of a program, at most. The vast majority of time is refactoring, troubleshooting, changing, testing, building, deploying, monitoring… and so on. With Rusts strong type system, I will probably spend 30% more time developing than with an easier language like Python/Go/Kotlin, but I will save 300% of time debugging, troubleshooting, deploying. Moreover, writing code is something I enjoy, while debugging is something I’d rather avoid. Any language that enables me to spend less time teoubleshooting runtime errors and debugging edge cases is a desirable language for me.
What would you consider a convenient language, and why?
I completely agree with almost everything you wrote (though from my limited experience of Kotlin it seems much less prone to the types of bugs you’re describing than Python and Go). That’s why, of the languages I know, I prefer Rust for pretty much every task (except shell scripting, for which I honestly don’t like any of the available options).
But I think Boats is right that there’s room in the language design space for a language that does make some sacrifices in favor of convenience, while still maintaining most of the correctness benefits of Rust.
Consider the case of
async
: Rust’s design was constrained by the need to not have an execution runtime built into the language, and a desire to be able to use async code in embedded environments without an allocator. The result is honestly an engineering marvel, and amazingly ergonomic given those constraints. But it’s full of rough edges and tricky corner-cases: on the language user side, execution engines have fractured the ecosystem, and on the implementation side, it’s a slow and arduous process to make all the other Rust language features play nicely with async (as evidenced by the fact that we only just recently got support for async in traits, and it’s still very limited).Those difficult requirements are not due to Rust’s goal of being highly reliable and maintainable; they’re due to the separate goal of Rust being usable anywhere C or C++ is usable. So what would a language look like that values maintainability strongly (maybe not quite as strongly as Rust, but nearly so), without being required to work on all embedded platforms or run without a garbage collector?