I was thinking that too. There are many cases where you do want to manage memory yourself, and in that case you should likely use Rust or maybe Zig if you can choose your own tool. But if you don't want to manage your own memory Nim works nicely, though IMO it requires adherence to a style guide more than most languages.
> While I can jump through hoops to compile JavaScript into a binary, such wouldn't feel "solid". And the very point of writing a native program in the first place is to make it feel solid
You can use Bun to compile to native binaries without jumping through hoops. It's not mature, but it works well enough that we use it at work.
I've written some Gleam as an exploration, and I liked it, but the "not widely used" thing is a concern. I need there to be some well maintained libraries for enterprise stuff.
I know this is not the fault of the language, and that is unfortunate.
Rust allows low level programming and static compilation, while still providing abstraction and safety. A good ecosystem and stable build tools help massively as well.
It is one of the few languages which managed to address a real life need in novel ways, rather than incrementing on existing solutions and introducing new trade offs.
>To paraphrase Norvig's Latency numbers a programmer should know, if we imagine a computer that executes 1 CPU instruction every second, it would take it days to read from RAM.
It's a detail, but this is a little bit off. RAM latency is roughly around ~100ns, CPUs average a couple instructions per cycle and a few cycles per ns.
Then in the analogy, a stall on RAM is about a 10 minute wait; not quite as bad as losing entire days.
This hits close to home. TypeScript is also my language of choice for 90% of the software I write. I agree with the author that TypeScript is very close to the perfect level of abstraction, and I haven't seen another language with a type system that's nearly as enjoyable to use. Of course, TS (any by extension JS) obviously has its issues/complications. Bun solves a lot of the runtime-related issues/annoyances though.
For the other 10% software that is performance-sensitive or where I need to ship some binary, I haven't found a language that I'm "happy" with. Just like the author talks about, I basically bounce between Go and Rust depending on what it is. Go is too simple almost to a fault (give me type unions please). Rust is too expressive; I find myself debugging my knowledge of Rust rather than the program (also I think metaprogramming/macros are a mistake).
I think there's space in the programming language world for a slightly higher level Go-like language with more expressiveness.
Odin has been really growing on me lately as a language that checks all of those boxes. String types, first class allocators, built in tests, a batteries included philosophy, and ease of use are some of the things that really drew me towards it.
I really wanted to like rust and I wrote a few different small toy projects in it. At some point knowledge of the language becomes a blocker rather than knowledge the problem space, but this is a skill issue that I'm sure would lessen the more I used it.
What really set me off was how every project turned into a grocery list of crates that you need to pull in in order to do anything. It started to feel embarrassing to say that I was doing systems programming when any topic I would google in rust would lead me to a stack overflow saying to install a crate and use that. There seemed to be an anti-DIY approach in the community that finally drew me away.
I really want to love rust, and I understand the niches it fills. My temporary allegiance with it comes down to performance, but I'm drawn by the crate ecosystem and support provided by cargo.
What's so damning to me is how debilitatingly unopinionated it is during situations like error handling. I've used it enough to at least approximate its advantages, but strongly hinting towards including a crate (though not required) to help with error processing seems to mirror the inconvenience of having to include an exception type in another language. I don't think it would be the end of the world if it came with some creature comforts here and there.
I’m not sure what his abstraction column really means, nuts and bolts-wise. But, Fortran is native, you get to allocate your own memory, and it has object oriented features (maybe that’s abstraction).
Very cool, I wish the author good luck! I've been writing a compiler in Rust for a few months now and I absolutely love it. The ways it solves most of the problems it addresses feel like the "right" way to do things.
There are some things that feel a little weird, like the fact that often when you want a more complex data structure you end up putting everything in a flat array/map and using indices as pointers. But I think I've gotten used to them, and I've come up with a few tricks to make it better (like creating a separate integer type for each "pointer" type I use, so that I can't accidentally index an object array with the wrong kind of index).
Rust is one of those languages that change how you think, like Haskell or Lisp or Forth. It won't be easy, but it's worth it.
It is a bit unclear to me why somebody who rejects C++ because "I once spent an entire year in the heaven of C++, walking around in a glorious daze of std::vector and RAII, before one day snapping out of it and realizing that I was just spawning complexity that is unrelated to the problem at hand." (which I can absolutely agree with!) is picking Rust from all options. If there is a language that can rival C++ in terms of complexity, it is Rust.
>There is an apocryphal story about Euler in elementary school solving all the math problems that the teacher gave to the class in a jiffy, so the teacher tells him to sum up the numbers to a thousand to get him to stop pestering for more. The expectation was that Euler would go through the numbers "imperatively", like C, summing them up. Instead, what Euler did was discover the summation formula and solved it "declaratively" like Haskell, in one go, as an equation.
I've heard this story be accounted to Gauss, not Euler.
The earliest reference is a biography of Gauss published a year after his death by a professor at Gauss' own university (Gottingen). The professor claims that the story was "often related in old age with amusement and relish" by Gauss. However, it describes the problem simply as "the summing of an arithmetic series", without mention of specific numbers (like 1-100). Also, it was posed to the entire classroom - presumably as a way to keep them busy for a couple of hours - rather than as an attempt to humiliate a precocious individual.
If anyone reads this and like me fears the difficulty and complexity of rust, but still wants a language that is competitive in performance, works for system level programming as well as something more general purpose definitely give Swift a go.
Over the last year I’ve started to write every new project using it. On windows, on linux and mac.
It is honestly a wonderful language to work with. Its mature, well designed, has a lot of similarities to rust. Has incredible interop with C, C++, Objective-C and even Java as of this year which feels fairly insane. It also is ergonomic as hell and well understood by LLM’s so is easy to get into from a 0 starting point.
Wow, a lot of stuff in here surprises me. C definitely can/does have spooky at a distance. Just share a pointer to a resource with something else and enjoy the spooky modifications. Changes are local as long as you program that way, but sometimes it can be a bit not-obvious that this is happening.
regarding redefining functions, what could the author mean? using global function pointers that get redefined? otherwise redefining a function wouldn't effect other modules that are compiled into separate object files. confusing.
C is simple in that it does not have a lot of features to learn, but because of e.g. undefined behavior, I find its very hard to call it a simple language. When a simple bug can cause your entire function to be UB'd out of existence, C doesn't feel very simple.
In haskell, side effects actually _happen_ when the pile of function applications evaluate to IO data type values, but, you can think about it very locally; that's what makes it so great. You could get those nice properties with a simpler model (i.e. don't make the langague lazy, but still have explicit effects), but, yeah.
The main thing that makes Haskell not simple IMO is that it just has such a vast set of things to learn. Normal language feature stuff (types, typeclasses/etc, functions, libraries), but then you also have a ton of other special haskell suff: more advanced type system tomfoolery, various language extensions, some of which are deprecated now, or perhaps just there are better things to use nowadays (like type families vs functional dependencies), hierarchies of unfamiliar math terms that are essentially required to actually do anything, etc, and then laziness/call-by-name/non-strict eval, which is its own set of problems (space leaks!). And yes, unfamiliar syntax is another stumbling block.
IME, Rust is actually more difficult than Haskell in a lot of ways. I imagine that once you learn all of the things you need to learn it is different. The way I've heard to make it "easier" is to just clone/copy data any time you have a need for it, but, what's the point of using Rust, then?
I wonder if the author considered OCaml or its kin, I haven't kept track of whats all available, but I've heard that better tooling is available and better/more familiar syntax. OCaml is a good language and a good gateway into many other areas.
There are some other langs that might fit, like I see nim as an example, or zig, or swift. I'd still like to do more with swift, the language is interesting.
When people say C is simple, besides everything that you point out, apparently they never learned anything beyond the classical K&R C book (ANSI/ISO C edition), and are stuck in a C89 mindset without any kind of compiler extensions.
> If someone posts a patch or submits a PR to a codebase written in C, it is easier to review than any other mainstream language. There is no spooky at a distance. [..] Changes are local.
Lol, wut? What about about race conditions, null pointers indirectly propagated into functions that don't expect null, aliased pointers indirectly propagated into `restrict` functions, and the other non-local UB causes? Sadly, C's explicit control flow isn't enough to actually enable local reasoning in the way that Rust (and some other functional languages) do.
I agree that Go is decent at this. But it's still not perfect, due to "downcast from interface{}", implicit nullability, and similar fragile runtime business.
I largely agree with the rest of the post! Although Rust enables better local reasoning, it definitely has more complexity and a steeper learning curve. I don't need its manual memory management most of the time, either.
And had not GNU/FSF made C the official main language for FOSS software on their manifesto, by the time when C++ was already the main userspace language across Windows, OS/2, Mac OS, BeOS, that "It is the reason for C's endurance" would be much less than it already is nowadays, where it is mostly UNIX/POSIX, embedded and some OS ABIs.
I program both C/C++ and Rust (the latter to a lesser degree currently). Rust is far superior to C in Error locality, if you write ideomatic Rust. Most of the types of errors I make in C would have been cought at compile time in Rust.
Aside from Rusts ownership model you can use the type system to enforce certain things. A typical example is that Rust uses different String types to force programmers to deal with the pitfalls. Turns out if you have a file name in an operating system it could not be a valid string, or you could have valid Unicode text that could not be a filename. Rust having different types for OS Strings and internal Unicode means when you want to go from one to the other you need to explicitly deal with the errors or choose a strategy how to handle them.
Now you could totally implement strings within Rust in a way that wouldn't force that conversion and programmers would then yolo their way through any conversion, provided they even knew about the issue. And the resulting error would not necessarily surface where it orginated. But that would be programming Rust like C.
In my experience many C libraries will just happily gulp up any input of any remotly valid shape as if it was valid data without many devs being even aware there were cases or conversions they would have had to deal with. You recognize exceptionally good C devs by the way they avoid those pitfalls.
And these skilled C devs are like seasoned mountaineers, they watch their every step carefully. But that doesn't mean the steep north face of the mountain is the safest, fastest or most ergonomic way to get to the summit. And if you believe that C is that, you should be nowhere near that language.
> And the very point of writing a native program in the first place is to make it feel solid.
What does that mean, and what is it about native programs (i.e. programs AOT-compiled to machine code) that makes them feel solid? BTW, such programs are often more, not less, sensitive to OS changes.
> realizing that I was just spawning complexity that is unrelated to the problem at hand
Wait till you use Rust for a while, then (you should try, though, if the language interests you).
For me, the benefit of languages with manual memory management is the significantly lower memory footprint (speed is no longer an issue; if you think Haskell and Go are good enough, try Java, which is faster). But this comes at a price. Manual memory management means, by necessity, a lower level of abstraction (i.e. the same abstraction can cover fewer implementations). The price is usually paid not when writing the first version, but when evolving the codebase over years. Sometimes this price is worth it, but it's there, and it's not small. That's why I only reach for low level languages when I absolutely must.
> What does that mean, and what is it about native programs (i.e. programs AOT-compiled to machine code) that makes them feel solid?
Im a little late here and as a Java user most of the time people tell me:
1. They just want to ship a binary. Most are not aware of Jpackage, but correct me if I’m wrong that just makes installers right? I’m hopeful that the “hermetic” work from Leyden will help here.
2. They frequently complain about Java’s memory usage, but don’t really understand how setting the heap size works and what the defaults are. I’m also hopeful that ZGC’s automatic heap sizing will solve this.
With those two features I think the view of Java will change, as long as there is good build tooling for them. It would be nice to make that the default, but that would break many builds.
I'm interested in the long piecewise elimination section. Presumably that's where they explain why not use Ocaml/Nim/yaddah yaddah.
If I were to write such a list, the answer would probably come down to "because I wanted to pick ONE and be able to stick with it, and Rust seems solid and not going anywhere." As much as Clojure and Ocaml are, from what I've heard, right up my alley, learning all these different languages has definitely taken time away from getting crap done, like I used to be able to do perfectly well with Java 2 or PHP 5, even though those are horrible languages.
Rust is an amazing language once you get over the initial mental hurdle. An important thing to go in with: 99% of programs should not require you to manage lifetimes (‘a notation) If you find yourself doing this and aren’t writing a inner loop high performance library, back up and find another way. Usually this entails using a Mutex or Arc (or other alternatives based on the scenario) to provide interior mutability or multiple references. This statement might not make sense now but write it down for when it will.
I use Rust now for everything from CLIs to APIs and feel more productive in it end to end than python even.
A nice thing about C is that you can be pretty confident that you know all major footguns (assuming you spent some time reading about it). With languages that are young or complex there is a much greater chance you’re making a terrible mistake because you’re not aware of it.
I'm yet to see someone be confident in this way on anything more than a trivial program, and be right. Just way too many footguns.
My personal memorable one was bit shifting 32bit values by varying amounts, and our test vectors all failing after a compiler update, because some of the shifts were by 32. Undefined behaviour.
It is nice that, unlike C++, the C language standard does list all the Undefined Behaviour (in Annex J.2), it's a pretty long list and IMO it's terrifying, not so much because of specifics like this:
"A searching or sorting utility function is called with an invalid pointer argument, even if the number of elements is zero"
But because of broad choices like:
"The execution of a program contains a data race"
"An object is referred to outside of its lifetime"
These are essentially categories of mistake we know programmers make, and in C the result is... Undefined Behaviour. No diagnostics, no exit, no errors, just throw your hands in the air and give up, anything might happen.
54 comments
[ 4.0 ms ] story [ 72.3 ms ] threadYou can use Bun to compile to native binaries without jumping through hoops. It's not mature, but it works well enough that we use it at work.
I know this is not the fault of the language, and that is unfortunate.
Rust allows low level programming and static compilation, while still providing abstraction and safety. A good ecosystem and stable build tools help massively as well.
It is one of the few languages which managed to address a real life need in novel ways, rather than incrementing on existing solutions and introducing new trade offs.
It's a detail, but this is a little bit off. RAM latency is roughly around ~100ns, CPUs average a couple instructions per cycle and a few cycles per ns.
Then in the analogy, a stall on RAM is about a 10 minute wait; not quite as bad as losing entire days.
For the other 10% software that is performance-sensitive or where I need to ship some binary, I haven't found a language that I'm "happy" with. Just like the author talks about, I basically bounce between Go and Rust depending on what it is. Go is too simple almost to a fault (give me type unions please). Rust is too expressive; I find myself debugging my knowledge of Rust rather than the program (also I think metaprogramming/macros are a mistake).
I think there's space in the programming language world for a slightly higher level Go-like language with more expressiveness.
https://ashishb.net/programming/maintaining-android-app/
I really wanted to like rust and I wrote a few different small toy projects in it. At some point knowledge of the language becomes a blocker rather than knowledge the problem space, but this is a skill issue that I'm sure would lessen the more I used it.
What really set me off was how every project turned into a grocery list of crates that you need to pull in in order to do anything. It started to feel embarrassing to say that I was doing systems programming when any topic I would google in rust would lead me to a stack overflow saying to install a crate and use that. There seemed to be an anti-DIY approach in the community that finally drew me away.
What's so damning to me is how debilitatingly unopinionated it is during situations like error handling. I've used it enough to at least approximate its advantages, but strongly hinting towards including a crate (though not required) to help with error processing seems to mirror the inconvenience of having to include an exception type in another language. I don't think it would be the end of the world if it came with some creature comforts here and there.
There are some things that feel a little weird, like the fact that often when you want a more complex data structure you end up putting everything in a flat array/map and using indices as pointers. But I think I've gotten used to them, and I've come up with a few tricks to make it better (like creating a separate integer type for each "pointer" type I use, so that I can't accidentally index an object array with the wrong kind of index).
Rust is one of those languages that change how you think, like Haskell or Lisp or Forth. It won't be easy, but it's worth it.
I've heard this story be accounted to Gauss, not Euler.
The earliest reference is a biography of Gauss published a year after his death by a professor at Gauss' own university (Gottingen). The professor claims that the story was "often related in old age with amusement and relish" by Gauss. However, it describes the problem simply as "the summing of an arithmetic series", without mention of specific numbers (like 1-100). Also, it was posed to the entire classroom - presumably as a way to keep them busy for a couple of hours - rather than as an attempt to humiliate a precocious individual.
[0] https://de.wikipedia.org/wiki/Gaußsche_Summenformel
Over the last year I’ve started to write every new project using it. On windows, on linux and mac.
It is honestly a wonderful language to work with. Its mature, well designed, has a lot of similarities to rust. Has incredible interop with C, C++, Objective-C and even Java as of this year which feels fairly insane. It also is ergonomic as hell and well understood by LLM’s so is easy to get into from a 0 starting point.
regarding redefining functions, what could the author mean? using global function pointers that get redefined? otherwise redefining a function wouldn't effect other modules that are compiled into separate object files. confusing.
C is simple in that it does not have a lot of features to learn, but because of e.g. undefined behavior, I find its very hard to call it a simple language. When a simple bug can cause your entire function to be UB'd out of existence, C doesn't feel very simple.
In haskell, side effects actually _happen_ when the pile of function applications evaluate to IO data type values, but, you can think about it very locally; that's what makes it so great. You could get those nice properties with a simpler model (i.e. don't make the langague lazy, but still have explicit effects), but, yeah.
The main thing that makes Haskell not simple IMO is that it just has such a vast set of things to learn. Normal language feature stuff (types, typeclasses/etc, functions, libraries), but then you also have a ton of other special haskell suff: more advanced type system tomfoolery, various language extensions, some of which are deprecated now, or perhaps just there are better things to use nowadays (like type families vs functional dependencies), hierarchies of unfamiliar math terms that are essentially required to actually do anything, etc, and then laziness/call-by-name/non-strict eval, which is its own set of problems (space leaks!). And yes, unfamiliar syntax is another stumbling block.
IME, Rust is actually more difficult than Haskell in a lot of ways. I imagine that once you learn all of the things you need to learn it is different. The way I've heard to make it "easier" is to just clone/copy data any time you have a need for it, but, what's the point of using Rust, then?
I wonder if the author considered OCaml or its kin, I haven't kept track of whats all available, but I've heard that better tooling is available and better/more familiar syntax. OCaml is a good language and a good gateway into many other areas.
There are some other langs that might fit, like I see nim as an example, or zig, or swift. I'd still like to do more with swift, the language is interesting.
Lol, wut? What about about race conditions, null pointers indirectly propagated into functions that don't expect null, aliased pointers indirectly propagated into `restrict` functions, and the other non-local UB causes? Sadly, C's explicit control flow isn't enough to actually enable local reasoning in the way that Rust (and some other functional languages) do.
I agree that Go is decent at this. But it's still not perfect, due to "downcast from interface{}", implicit nullability, and similar fragile runtime business.
I largely agree with the rest of the post! Although Rust enables better local reasoning, it definitely has more complexity and a steeper learning curve. I don't need its manual memory management most of the time, either.
Related post about a "higer-level Rust" with less memory management: https://without.boats/blog/notes-on-a-smaller-rust/
And had not GNU/FSF made C the official main language for FOSS software on their manifesto, by the time when C++ was already the main userspace language across Windows, OS/2, Mac OS, BeOS, that "It is the reason for C's endurance" would be much less than it already is nowadays, where it is mostly UNIX/POSIX, embedded and some OS ABIs.
Aside from Rusts ownership model you can use the type system to enforce certain things. A typical example is that Rust uses different String types to force programmers to deal with the pitfalls. Turns out if you have a file name in an operating system it could not be a valid string, or you could have valid Unicode text that could not be a filename. Rust having different types for OS Strings and internal Unicode means when you want to go from one to the other you need to explicitly deal with the errors or choose a strategy how to handle them.
Now you could totally implement strings within Rust in a way that wouldn't force that conversion and programmers would then yolo their way through any conversion, provided they even knew about the issue. And the resulting error would not necessarily surface where it orginated. But that would be programming Rust like C.
In my experience many C libraries will just happily gulp up any input of any remotly valid shape as if it was valid data without many devs being even aware there were cases or conversions they would have had to deal with. You recognize exceptionally good C devs by the way they avoid those pitfalls.
And these skilled C devs are like seasoned mountaineers, they watch their every step carefully. But that doesn't mean the steep north face of the mountain is the safest, fastest or most ergonomic way to get to the summit. And if you believe that C is that, you should be nowhere near that language.
What does that mean, and what is it about native programs (i.e. programs AOT-compiled to machine code) that makes them feel solid? BTW, such programs are often more, not less, sensitive to OS changes.
> realizing that I was just spawning complexity that is unrelated to the problem at hand
Wait till you use Rust for a while, then (you should try, though, if the language interests you).
For me, the benefit of languages with manual memory management is the significantly lower memory footprint (speed is no longer an issue; if you think Haskell and Go are good enough, try Java, which is faster). But this comes at a price. Manual memory management means, by necessity, a lower level of abstraction (i.e. the same abstraction can cover fewer implementations). The price is usually paid not when writing the first version, but when evolving the codebase over years. Sometimes this price is worth it, but it's there, and it's not small. That's why I only reach for low level languages when I absolutely must.
Im a little late here and as a Java user most of the time people tell me:
1. They just want to ship a binary. Most are not aware of Jpackage, but correct me if I’m wrong that just makes installers right? I’m hopeful that the “hermetic” work from Leyden will help here.
2. They frequently complain about Java’s memory usage, but don’t really understand how setting the heap size works and what the defaults are. I’m also hopeful that ZGC’s automatic heap sizing will solve this.
With those two features I think the view of Java will change, as long as there is good build tooling for them. It would be nice to make that the default, but that would break many builds.
(Of course they actually do want Haskell but they probably need to get there gradually)
If I were to write such a list, the answer would probably come down to "because I wanted to pick ONE and be able to stick with it, and Rust seems solid and not going anywhere." As much as Clojure and Ocaml are, from what I've heard, right up my alley, learning all these different languages has definitely taken time away from getting crap done, like I used to be able to do perfectly well with Java 2 or PHP 5, even though those are horrible languages.
short-circuited reading this
This was brilliant performance art. Bless your heart Dear Author, I adore you.
I use Rust now for everything from CLIs to APIs and feel more productive in it end to end than python even.
My personal memorable one was bit shifting 32bit values by varying amounts, and our test vectors all failing after a compiler update, because some of the shifts were by 32. Undefined behaviour.
"A searching or sorting utility function is called with an invalid pointer argument, even if the number of elements is zero"
But because of broad choices like:
"The execution of a program contains a data race"
"An object is referred to outside of its lifetime"
These are essentially categories of mistake we know programmers make, and in C the result is... Undefined Behaviour. No diagnostics, no exit, no errors, just throw your hands in the air and give up, anything might happen.