Roc rewrites the compiler in Zig
gist.github.com
gist.github.com
I recommend reading Roc's FAQ too - it's got some really great points. E.g. I'm internally screaming YESSS! to this: https://www.roc-lang.org/faq.html#curried-functions
But then it has other weird features too, like they seem to be really emphasising "friendliness" (great!) but then it has weird syntax like `\` for anonymous functions (I dunno where that dumb syntax came from by Nix also uses it and it's pretty awful). Omitting brackets and commas for function calls is also a bad decision if you care about friendliness. I have yet to find a language where that doesn't make the code harder to read and understand.
It’s a syntax that’s several decades old at this point.
It’s different, but not harder. If you learned ML first, you’d found Algol/C-like syntax equally strange.
That's not ML syntax. Haskell got it from Miranda, I guess?
In SML you use the `fn` keyword to create an anonymous function; in Ocaml, it's `fun` instead.
https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/unic...
[1]: https://homepages.inf.ed.ac.uk/wadler/papers/papers-we-love/... (can be spotted on page 353)
But yes, the slash is just an ASCII stand-in for a lambda.
ETA: I tracked down a copy of the Edinburgh LCF text and I have to eat crow. It doesn't use a lambda, but it does use a slash rather than a reserved word. The syntax, per page 22, is in fact, `\x. e`. Similar to Haskell's, but with a dot instead of an arrow.
Thanks for the link to the LCF text though :^)
It's like english. You don't need knowledge in obscure proto-germanic linguistics to use articles in your sentences. But if you want to understand why we seem to randomly attach "a" before nouns— proto-germanic linguistics has the answer (probably, I just speak english I don't know where all its syntax originates).
https://github.com/roc-lang/roc/releases/tag/0.0.0-alpha2-ro...
Ultimately, it's just syntax and not so important. Semantics are important.
1. I know what lambda calculus is, and I didn't even make the connection between \ and lambda. It's pretty tenuous.
2. Most programmers do not know what lambda calculus is. This is supposed to be a friendly language, not an obscure academic one.
3. It's not even the same lambda as in lambda calculus, e.g. it takes multiple arguments.
4. Lambda was a reasonable choice in lambda calculus since it's a very "mathsy" language, and it's pretty much the only symbol in the language. It's a pretty awful choice for a practical programming language though - there's a reason 99% of languages use something like `fn` or `fun` or `function` or `def` instead of lambda to define top-level functions.
I'm not a huge fan of `|foo|` either to be honest - I don't see why you can't simply use the same syntax for anonymous and named functions - but it is at least a little better.
If it were up to me I'd go with something like
fn double(a: list[int]) -> list[int] {
a.map(fn(i) { i * 2 })
}
Same syntax; just allow omitting the name and inferring the types. I can't recall any languages that do that though so maybe there's some tricky reason it can't work?Roc was appealing because it was slightly more distinct and more unique. Plus it was more focused on speed. I appreciate them wanting to become more approachable. The new syntax seems less approachable to me because it’s now a half step between approachable Haskell and what feels like a combination of Ruby and Python.
[1]: https://gleam.run/
I would frankly be shocked if a 4 year syllabus in CS these days just skipped any mention of Alonzo Church, especially considering that it's fundamental to functional programming and theoretical computer science...
Maybe it's not taught in "CS" programs that are actually just "practical software engineering" bootcamps with extra steps. The real nerds know about lambdas. They're even literally called that in Ruby (and possibly other languages).
Hell I used lambda functions in C++ for years without knowing what lambda calculus is. And C++ doesn't even use `lambda`.
Yes really.
In many places that won't get them through HR, nor calling oneself Software Engineer is legally allowed without a corresponding degree.
Honestly, I didn't take enough CS to get into functional langs, but I did learn what a lambda was. The importance of it all came later, after years of struggling with bugs in million-line codebases that wouldn't have even existed in an immutable, functional language without inheritance.
I don't really see how it would be misleading.
Is there some deeper technical reason for making such changes?
If anything I don't think Haskell goes far enough the automatic currying, points free stuff. If you're going to be declarative, don't half ass it.
The only relevant reason he lists is point-free, but he doesn't go far enough. Point-free very often turns into write-only balls of unmaintainable nastiness. Wanting to discourage this behavior is a perfectly reasonable position. Unfortunately, this one true argument is given the most tepid treatment of all the reasons.
Everything else doesn't hold water.
As he knows way better than most, Elm has auto-curry and has been the inspiration for several other languages getting better error messages.
Any language with higher-order functions can give a function as a result and if you haven't read the docs or checked the type, you won't expect it. He left higher-order function in, so even he doesn't really believe this complaint.
The argument about currying and pipe isn't really true. The pipe is static syntax known to the compiler at compile time. You could just decide that the left argument is applied/curried to the function before the right argument.
I particularly hate the learning curve argument. Lots of great and necessary things are hard to learn. The only question is a value judgement about if the learning is worth the payoff. I'd guess that most of the Roc users already learned about currying with a more popular FP language before every looking at Roc, so I don't think this argument really applies here (though I wouldn't really care if he still believed it wasn't worth the learning payoff for the fraction of remaining users).
To reiterate, I agree with his conclusion to exclude currying, but I wish he were more straightforward with his one good answer that would tick off a lot of FP users rather than resorting to a ton of strawman arguments.
I also tried Googling for that term (never heard before), and I found these:
https://stackoverflow.com/questions/944446/what-is-point-free-style-in-functional-programming
https://en.wikipedia.org/wiki/Tacit_programming
If you really meant "point-free", can you tell me where in his post he mentions it? I would like to learn more.Here's a made-up JS example. I've attempted to be fair to both approaches doing it how I personally would with each.
//typical implementation
const howManyAdults = (data) => {
const adults = data
.flatMap(obj => obj.type === 'parent' ? [obj, ...obj.children] : obj)
.filter(obj => typeof obj.age === 'number')
.filter(obj => obj.age >= 18 && obj.age < 150)
adults.forEach(adult =>
console.log(`${adult.name} is an adult age ${adult.age}`)
)
return adults.length
}
//nice "point-free" implementation using helper functions
const transformData = obj => obj.type === 'parent' ? [obj, ...obj.children] : [obj]
const isAdult = obj => obj.age >= 18 && obj.age < 130
const howManyAdults = pipe(
flatMap(transformData),
filter(hasTypeOf('age', 'number'),
filter(isAdult),
logEach`${pick('name')} is an adult age ${pick('age')}`,
len,
)
//completely point-free gets crazy
const howManyAdults = pipe(
flatMap(ifElse(
compose(eq('parent'), pick('type')),
juxt([identity, pick('children']),
identity,
), []),
filter(hasTypeOf('age', 'number'),
filter(both(gte(pick('age',18), lt(pick('age'), 130)))),
logEach`${pick('name')} is an adult age ${pick('age')}`,
len,
)
I went through a point-free phase early in my career, but even with lots of practice, I can't believe that most devs ever find the third example as readable as the first or second. I'd also note that this is a trivial example and doesn't require you to track any monad wrappers either.Personally, I rather like reading the moderate middle example because it removes the boilerplate and allows me to easily follow the overall flow without getting caught up in the details. But I'll take the first example every time if it means never dealing with the third example.
A one-character ASCII rendering of the Greek lowercase letter lambda: λ
λx → x + 5
\x -> x + 5
this didn't faze me in the least because it's just a more easily typed λ, the lambda character, which has for a long time now (many decades) been used to describe anonymous functions (i.e. "lambdas")
did you never take any formal CS education? if not, that might explain it
so before you jump to calling it "dumb", maybe next time lean on Chesterton's Fence for a bit.
https://sproutsschools.com/chesterton-fence-dont-destroy-wha...
That said, granted, the fact that it's also the escape character is problematic. Maybe /\ might have been better but that's even harder to type.
foo = \arg1, arg2 ->
body
...to this: foo = |arg1, arg2|
body
The reason for this change was that we have a new and extremely well-received language feature (landed but not yet formally announced) which results in `->` and `=>` having different meanings in the type system. This made it confusing to have `->` in the syntax for anonymous functions, because it seemed to suggest a connection with the type-level `->` that wasn't actually there.The most popular syntax that mainstream languages use today for anonymous functions is something like `(arg1, arg2) => body` but of course that has the same problem with having an arrow in it, so changing to that wouldn't have solved the problem.
Rust uses `|arg1, arg2| body` (and Ruby kinda uses it too for blocks), and we'd all had fine experiences using that syntax in Rust, so we chose it as the new lambda syntax. You can see the new syntax in the code example at the top of roc-lang.org.
Rust's slow compilation comes from lots of features and an excellent generated machine code quality. Both Zig and Roc will be equally slow or slower if they match what Rust offers.
If all they want is fast compilation, they can just try Pascal.
I used to root for the D programming language but it seems to have got stuck and never gained a good ecosystem. I've disliked Rust from the first time I saw it and have never warmed up to it's particular trade offs. C feels unergonomic these days and C++ is overfull with complexity. Zig feels like a nice pragmatic middle ground.
I actually think Rust is probably perfectly suited to a number of tasks, but I feel I would default to choosing Zig unless I was certain beyond doubt that I needed specific Rust safety features.
Recently, a coworker of mine made a great observation that made everything clear to me. I was looking for a replacement for C, but Rust is actually a replacement for C++. Totally different beast - powerful but complex. I need to see if Zig is any closer to C in spirit.
I've seen this sentiment a lot, and I have to say it's always puzzled me. The difference between Rust and basically any other popular language is that the former has memory safety without GC†. The difference between C++ and C is that the former is a large multi-paradigm language, while the latter is a minimalist language. These are completely different axes.
There is no corresponding popular replacement for C that's more minimalist than Rust and memory safe.
† Reference counting is a form of garbage collection.
I agree with Raymond Chen's take on the academic definition of GCs[1], and therefore Rust is certainly a GC'd language (because your code behaves as though memory is infinite... usually). It's probably one of the first examples of "static garbage collection" - though I'm sure someone will point out a prior example.
[1]: https://devblogs.microsoft.com/oldnewthing/20100809-00/?p=13...
The "simulates infinite RAM" is an interesting perspective but simply not the subject of most conversations.
A language based on such a paradigm can be provably memory safe, and regions can have their own allocators and optionally provide locking when the regions are shared.
This approach obviates the need for reference counting individual allocations (since regions are tracked as a whole), but it suffers from excess memory usage in the event of many short-lived allocations (i.e. they leak until the entire region's allocations go out of scope). But those types of memory accesses can be problematic in every systems language as they can eventually cause memory fragmentation.
That problem can be minimized using per-allocation reference counting, but that can incur a heavy performance hit. Although not having to use it everywhere could minimize the impact.
The plus side is you don't have to worry about borrow checking, so such a language can be more flexible than Rust, while still maintaining the memory safety aspect.
The question, as always, is: is the juice worth the squeeze?
Truthfully, I suspect no. The Rust train has left the station and has a decade head start. Even if it is a pain in the ass, lol.
And I admit, I loathe the borrow checker. Ironically, I never really have problems with it, because I do understand it, it's just that I find it too limiting. Not everything I want to do is unsafe, and I hate the way it has made people think that if you really do know better than the borrow checker, you must clearly be doing something wrong and you should re-architect your code. It's insulting.
But GC has nothing to do with whether heap allocations 'slow the program down', it's who owns the lifetime of the allocated object - for GC not the programmer, but the runtime.
If I create a an object, and then then soon after deref it [MyObj release], then I know it will dealloc immediately after. I'm basically in control of its lifetime, even though it's ref counted.
If I call [MyObj free] and it's still owned by another object (e.g. I added it to some collection), that's OK because the object is still useful and has a lifetime outside of my control, and its destruction will be deferred until it doesn't.
But, with a GC object, if I call new MyObj() and then soon after try my best to destroy it, I can't because I'm not in control of its lifetime, the runtime is.
That's what I see the distinction between GC and ref counted (not GC), and why I mostly don't agree with so many people here insisting that ref counting is garbage collection. How can it be when I can easily be explicitly in control of an objects lifetime? I create it, then I destroy it, and it happens exactly in the sequence that I dictate.
For sure, ARC makes it a bit more subtle, but even then, I can reliably predict when an object will be destroyed and factor that into the sequence of events in my program.
-An Atheist.
Theoretically you could have a GC'd language that cared about scope, lifetimes, stack vs heap and value vs reference distinctions, but no such thing was ever seen in the wild. (Perhaps because people use GC'd languages precisely because they don't want to care about these distinctions.)
1. Rust is an immensely complicated language, and it's not very composable (see the async debacle and whatnot). On the simple<->complex slider, it's smack dab on the right of the scale.
2. Ignoring any nitpicking [0], Zig is memory-safe enough in practice, placing it much closer to Rust than to C/C++ on the memory safety axis. My teammates have been using Zig for nearly a year, and the only memory safety bug was (a) caught before prod and (b) not something Rust's features would have prevented [1]. The `defer` and `errdefer` statements are excellent, and much like how you closely audit the use of `unsafe` in Rust there is only a small subset of Zig where you actually need to pull your magnifying glass out to figure out if the code has any major issues. In terms of memory issues I've cared about (not all conforming to Rust's narrow definition of memory safety), I've personally seen many more problems in Rust projects I contribute toward (only the one in Zig, plus a misunderstanding of async as I was learning the language a few years ago, many of varying severity in Rust, at this point probably more code written in Zig than Rust, 10yoe before starting with either).
With that in mind, you have C/C++ on the unsafe axis and Zig/Rust on the safe axis. The complexity axis is self-explanatory, fleshing out the analogy.
Is Zig memory-safe? No, absolutely not. Does that mean that Rust will win out for some domains? Absolutely. In practical terms though, your average senior developer will have many memory safety bugs in C/C++ and few in Zig/Rust. It's a reasonable way to compare and contrast languages.
Is it a perfect description? No, the map is not the territory. It's an analogy that helps a lot of people understand the world around them though.
[0] Even Python is simpler than Rust, and it's memory-safe. If we're limiting ourselves to systems languages, you still have a number of options like Ada and Coq. Rust is popular because it offers a certain tradeoff in the safety/performance/devex Pareto curve, and because it's had a lot of marketing. It's unique in that niche, by definition, but it's far from the only language to offer the features you explicitly stated.
[1] It was just an object pool, and the (aggregate) resetting logic wasn't solid. The objects would have passed through the borrow checker with flying colors though.
Edit: To your GC point, many parts of Rust look closer to GC than not under the hood. You don't have a GC pause, but you have object pools (sometimes falling back to kernel object pools) and a variety of allocation data structures. If RC is a GC tactic, the extra pointer increment/decrement is negligible compared to what Rust actually does to handle its objects (RC is everything Rust does, plus a counter). That's one of my primary performance complaints with the language, that interacting with a churn of small objects is both expensive and the easiest way to code. I can't trust code I see in the wild to behave reasonably by default.
The only time I've seen "almost memory safe" actually work is in Go and Swift, which have memory safety problems with concurrency, but they're actually rare enough not to matter too much (though in Go's case it would have been easy to design interfaces and slices not to have the problem, and I wish they had). I simply don't believe that Zig is meaningfully more memory safe than C++.
There's going to be some impact and low-level libraries that manipulate directly the words that constitute slices and interfaces, and there will some slight performance impact and increase in memory usage, but hopefully nothing drastic.
Two-word loads could be used for interface values (assuming that alignment is increased), except if support older x86-64 is needed. There are implementations that require using CMPXCHG16B, which is a really slow way to load two words. Both Intel and AMD updated their ISA manuals that using VMOVDQA etc. is fine (once the CPU supports AVX and the memory is cacheable).
Is your opinion based on anything other than pure speculation?
I think it makes more sense to form an opinion after actually having tried Rust, C++, and Zig in earnest.
There are lots of us out there who've done it. Join us!
While I use C++ a lot I am not a fan. It is just one of many languages I use. But from my personal experience it is true. I frankly forgot when was the last time I hit memory problem, years for sure. And my code is often a stateful multithreaded backends with high request rate.
If you're not looking, how would you know?
You could have a blatant SQL injection in your code and you can always pretend that it doesn't matter, since you haven't been attacked so far.
I my case SQL injection is not possible. I am not constructing any SQL statements from input.
>"you can always pretend"
I think it is you pretending to know problems that do not exist in my code.
The async featureset in Rust is far from complete, but async is also somewhat of a niche. You're not necessarily expected to use it, often you can just use threads.
> Zig is memory-safe enough in practice
Temporal safety is a huge deal, especially for the "programming in the large" case. Where unstated assumptions that are relied upon for the safety of some piece of code can be broken as some other part of the code evolves. The Rust borrow checker is great for surfacing these issues, and has very few practical alternatives.
> If we're limiting ourselves to systems languages, you still have a number of options like Ada and Coq.
It's easy to be "safe" if the equivalent to free() is marked as part of the unsafe subset, as with Ada. Coq is not very relevant on its own, though I suppose it could be part of a solution for proving the memory safety of C code. But this is known to be quite hard unless you do take your care to write the program in the "idiomatically safe" style that a language like Rust points you to.
I’m sorry but it’s not true. You lose access to most of the ecosystem.
Sure, but now you snuck in an Artifact of Death (in TvTropes sense) into your codebase. It won't kill your code base immediately and with proper handling it might work, but all it takes is one mistake, one oversight, for it to cause issues.
It is a procedural language and as such, composition of functions is not implemented easily, if at all possible. It is a shame for sure that such powerful techniques are not possible in Rust, but for some people it is worth the trade off.
The way i put it, is that Haskell is a language with very strict type system, while Rust has very strict type system, and strict scoping. Strict scopes mean that an Fn closure has to be different from a FnMut closure, which also means several other complications when it comes to async.
This trade off is fine with me, but for several other people it doesn't worth it.
That's what i meant it is impossible to do in Rust. From that point of view, Rust is a low level procedural language, but expressing any kind of business logic it is almost as high level as Haskell or Java.
But if we focus on error detection, then Rust is the highest level of any other PL. For example Java or Python are too low level when an error occurs. That high level of error detection is paid of course, and that is by longer compilation times.
If i had a wish though, i would wish Rust had as good closures as Haskell or Lisp.
I always thought C++ was a terrible idea, from way before C++ 98, I own Stroustrup's terrible book about his language, which I picked up at the same time as the revised K&R and it did nothing to change that belief, nor have subsequent standards.
However, I do have some sympathy for this sentiment about Rust being a better C++. Even though Rust and C++ have an entirely different approach to many important problems the syntax often looks similar and I think Zig manages to be less intimidating than Rust for that reason if you don't want that complexity.
Personally I had no interest in C++† and I have no serious interest in Zig.
† Ironically I ended up caring a lot more about C++ after I learned Rust, and most specifically when understanding how Rust's HashMap type works, but I didn't end up liking C++ I just ended up much better informed about it.
I think that's where the sentiment comes from. It's not that Rust is similar to C++ in terms of the actual languages and their features. It's that people who like C++ are morely likely to like Rust than people who like C are.
I would argue that C is not a minimalistic language either. There is a lot under the hood in C. But it feels small in a way that Rust and C++ don't.
I think Rust and C++ appeal to programmers who are OK with a large investment in wrapping their head around a big complex language with the expectation that they will be able to amortize that investment by being very productive in large projects over a large period of time. Maybe sometimes the language feels like trying to keep a piece of heavy duty machinery from killing you, but they're willing to wrestle with it for the power you get in return.
The people who are excited about Zig and C wants something that feels more like a hand tool that doesn't demand a lot of their attention and lets them focus on writing their code, even if the writing process is a little more manual labor in return.
It's funny because to me there's an analogy with heavy machinery but materially different: there are some industrial machines that have two buttons that need to be actuated to activate the mechanism, separated by arm length in order to ensure that the operator's arms are out of the way when the limb crunching bits are moving. I see Rust that way, engineering the safe way to do things as the path of least resistance, at the cost of some convenience when trying to do something "unsafe".
Most of programming is like that, but in the few cases where there literally are lives at stake, memory safety by itself will not do much for you and performance is going to be a secondary concern.
Zig does have safety features that C/C++ do not have, but also one should not underestimate the security implications of language complexity by itself.
You clearly don't like it, but it seems many people disagree.
Well, the really harsh way of putting this is that the patterns break for a reason; they rely on global claims about the program, so they aren't genuinely robust in the context of code that sits within a large, constantly evolving codebase that can't be practically surveyed in its entirety. Rust is very good at picking patterns that can be verified with a comparatively straightforward, "local" analysis that broadly follows the same structure as the actual program syntax. Safety claims that rely on "global" properties which cannot be kept within a self-contained, module-like portion of the code are essentially what the unsafe marker is intended for. And this is exactly what idiomatic C/C++ code often gives you.
This is actually why I think that proposals like Safe C++ should get a lot more attention that they do at present. Yes, Safe C++ changes what's idiomatic in the language but it does so in a way that's broadly sensible (given our increased attention to memory safety) especially in a context of "programming in the large".
You can certainly argue whether or not their perceptions are correct, but I generally believe that people are entitled to their priorities.
Either way, you initially stated confusion about why people stated certain opinions about Rust, C++, and C. I tried to explain why people might hold those opinins. You are then arguing that they shouldn't hold those opinions. Whether or not that's true, a prescriptive claim is orthogonal to understanding what's actually in their heads.
Actually that's wrong. In C you will need exhaustive formal verification, because UB doesn't cause minor miscompilation anymore. Formal verification is far more onerous than whatever Rust is demanding.
Among my friends I know several people who are very enthusiastic about board games and several who are very enthusiastic about craft beer, but there's not a particular noticeable overlap. Personally of course I am very into board games and I don't drink at all.
> I would argue that C is not a minimalistic language either. There is a lot under the hood in C.
Nah, C actually is small, that's why K&R is such a short book. It makes enormous compromises to pull that off, but presumably on a machine where 64kB of RAM is extraordinary these compromises made lots of sense. C23 is quite a bit bigger, for example "bool" is now an actual type (albeit implicitly convertible) but still small by modern standards.
There really isn't that much "under the hood", it's often just the least possible moving parts that could possibly have worked.
a[b] in C++ is a call to a member function a.operator[](b) -- arbitrary user code
a[b] in Rust is a call to core::ops::Index::index(a, b) or, in context IndexMut::index_mut -- again, arbitrary user code
a[b] in C is just a pointer addition of a and b - one of them will be converted to a pointer if necessary, and then the other one is added to the pointer using normal pointer arithmetic rules
I'd argue that C is much bigger than K&R, but that isn't immediately visible to a new programmer because it's all undefined behaviors.
Also even between C89 and C23, many folks wrongly count "whatever my compiler does" as C, and there are endless amounts of extensions to be aware of.
I jest, but only a tiny bit. The features of heavy OOP and feature-rich languages tend to show their value only in really large codebases being worked on by several different people—precisely because many of their features are just guardrails to make it hard for people to code incorrectly against another's understanding or assumptions, when shared understanding is more difficult to establish. Contrarily, any solo programmer or really small team is almost invariably better served by a language like go, C, scheme, or Zig.
If you say "you can't do x with y in C++" you will get an "yes you can, you just use asd::dsadasd::asdadqwreqsdwerig_hfdoigbhiohrf() with weaorgoiawr flag". From what I have seen from Rust, it is similar. I don't want to fill my brain with vim bindings.. cough.. Rust ways of doing something. I just want to code my hobby game engine v7.
That said, I am happy to use software written in it. Even though the evangelists can be really annoying.
Those are the axes relevant to the parent in the context of their comment - not specific language semantics or core features.
In the real world, memory safety is not all-or-nothing (unless you're willing to concede that Rust is not safe either, since unsafe Rust exists). I'm working on an embedded project in Rust and I'd MUCH rather be using Zig. The only safety thing that Rust would give me that Zig does not is protection from returning pointers to stack-allocated objects (there are no dynamic allocations and no concurrency outside of extremely simple ISRs that push events onto a statically allocated queue). But in exchange I have to deal with the presence of unsafe Rust, which feels like a gigantic minefield even compared to C.
I think idiomatic coding norms for unsafe Rust are still a bit half-baked, and this is where something like Zig can have an advantage of sorts. You can see this also, e.g. in the ongoing proposals for a Pin<> alternative.
There is Objective-C, it fits your definition of memory safety.
Also, there's FORTH!
> difference between C++ and C is that the former is a large multi-paradigm language, while the latter is a minimalist language. These are completely different axes. > There is no corresponding popular replacement for C that's more minimalist than Rust and memory safe.
Edit: oh, I never read the last bit "and memory safe" -- well ya, that's kind of rust's major advantage.
> The difference between Rust and basically any other popular language is that the former has memory safety without GC†. The difference between C++ and C is that the former is a large multi-paradigm language, while the latter is a minimalist language. These are completely different axes.
Indeed. Let one axis be the simple/multi paradigm. Let the other axis be no memory management / automatic memory management without GC / GC. This divides the plane into 6. In the simple + no memory management sits C and Zig, and in the multiparadigm + memory safe without GC segment sits C++ and Rust.
> There is no corresponding popular replacement for C that's more minimalist than Rust and memory safe.
Those are some weird requirements for "being a replacement", but it is obviously true, as you picked them such.
Rust is a great replacement for C++ as it fits into the same place in the stack of tools.
Go is not a C or Python replacement.
Zig is a good replacement for C.
C++ has tons of extra features over C because it's a kitchen sink language. Rust has some extra features over C because in order to support static analysis for memory safety in a practically usable manner, you need those features. In practice, there are lots of C++ features that Rust doesn't bother with, especially around templates. (Rust just has a more general macro feature instead. The C++ folks are now on track to adding a clunky "metaclass" feature which is a lot like Rust macros.)
C++ already has almost Rust macros, with a mix of compile time execution, concepts and traits, without requiring another mini-language, or an external crate (syn).
Sometimes they remove stuff, and then everything written before a specific date stop working.
Google stopped writing new Python, because they found that the same engineers would produce software with similar defect rates, at a similar price and in similar time, with Go, but it would have markedly better performance, so, no more Python.
Go manages to have a nice sharp start, which means there are going to be a bunch of cases where you didn't need all the perf from C, but you did need a prompt start, so, Java was not an option but Go is fine.
In my own practice, Rust entirely replaces C, which has previously been my preferred language for many use cases.
In what sense? Features or complexity? From a productivity/correctness perspective, Rust is a huge step up over C++.
Use more xcode, Clion, Visual Studio, C++ Builder, and less vi and Emacs for C and C++.
Not everyone has the RIIR luxury for what we do.
However C++ folks aren't standing still, and anyone pointing out to cargo, and not using conan/vcpkg, alongside a C++ aware IDE, is doing themselves a disservice, better be 85% there than none at all.
TinyGo and TamaGo folks enjoy writing their bare metal applications on embedded and firmware.
I have never used zig, although I have looked into it. I have used C and C++ for a long time, and would like a replacement for C.
To my mind zig is nowhere near a good replacement for C. It adds way too much extra stuff (e.g. metaprogramming). A good replacement for C would have a similar simplicity and scope to C (i.e. a portable assembler) but address known issues. For example it should fix the operator precedence rules to be sane. It should close-off on undefined behavior and specify things more formally than C, and still allow low-level memory manipulation. To be even better than C it should allow stuff that would be obviously correct in assembler, but is UB or ID in C.
nerdy langnoob or noobie langnerd here. not sure which is which, cuz my parsing skills are nearly zilch. ;)
Zig shatters that with comptime and the 'type' type.
I believe Zig even has some libraries for doing more exotic things easily, such as converting between an array of structs and a struct of arrays (and back).
I get it, thanks. in C you have to cast everything to (void *) to do things like a generic vector.
>I believe Zig even has some libraries for doing more exotic things easily, such as converting between an array of structs and a struct of arrays (and back).
yes, iirc, there was a zig thread about this on hn recently.
will reply in a day or two after reviewing my own comments, yours, and your link.
I would disagree. C++ provides way more features than Rust and to me Rust feels way more constrained comparatively.
Many of my favourite Rust features aren't in C++. For example they don't have real Sum types, they don't have the correct move semantic, they are statement oriented rather than expression oriented and their language lacks decent built-in tooling.
But also, some of the things C++ does have are just bad and won't get fixed/ removed because they're popular. And there's no sign of it slowing down.
I think that in practice you should use Rust in most places that C++ is used today, while some of the remainder should be WUFFS, or something entirely different.
I am doing fine. Thank you. Also I am not a language warrior. I do have my preferences but use many since I am an independent and work with many clients.
No one from them would accept a patch in Rust instead of C or C++.
Yes I know about Deno, but even them haven't rewriten V8, and wgpu or Rust CUDA aren't the same as what Khronos, NVidia, AMD, Microsoft put out in C++.
Last I checked, rust touted itself as a systems programming language, which C++ kinda is but mostly isn't (many C++ features just aren't appropriate or are poorly suited for systems programming).
I would never choose Rust over C++ for scientific programming, because Rust metaprogramming features are so poor.
However I'd probably choose Rust over C in many areas where C is best suited.
So to me the venn diagram of utility shows Rust to overlap with C far more than C++.
I suppose there is also Jai in a similar space as well, although I'm not a devotee to Jonathan Blow and I don't share much of the excitement his followers seem to have.
I do feel Zig has the current trend moving in its favor, with projects like Ghostty and Bun gaining prominence. I think Odin would need something like that to really capture attention.
But there are some: https://hn.algolia.com/?q=Odin
Odin has the best approach for "standard library" by blessing/vendoring immensely useful libraries
Odin also has the best approach for Vector Math with native Vector and Matrix types
Odin's "standard library" stuff is very silly, it still feels like we were just copy-pasted the lead developer's "useful stuff" directory. Bill doesn't feel there's a value to actual standard libraries, so instead here's... whatever. He insists that's not what's going on here, but that doesn't change how it feels.
Rust is primarily aimed at more specific use cases, evolving around memory safety and low(er) level programming. So where people might dislike the confusing syntax and difficulty of learning either Zig or Rust (among other inconveniences), its harder to make arguments against Rust's usefulness for safety, maturity (v1 plus), or present job market popularity. Zig does not have the luxury of those bonus points.
When it comes to general-purpose programming, including for hobbyists or students, there are many other alternative languages that are arguably much more attractive, easier to use, and/or easier to learn. Golang, Vlang, Odin, Jai, C3, etc...
[1]: https://www.youtube.com/watch?v=uVVhwALd0o4 ("Language Perf and Picking A Lang Stream" from 29:50)
Zig seems to take a neutral, technical-first stance, while Rust has a strong focus on inclusivity, strict moderation, and social policies like with the foundation drama (which is just nonsense for a programming language).
I really wish Nim would have won the language wars though.
And now they are doubling down on that by moving from "OCaml meets C++" to "C, the good parts"!
If FP isn't good for writing a compiler, what is it good for?
Summing the Fibonacci sequence I guess.
To be fair, most practical FP languages have that, but I never saw the appeal for a strictly functional general purpose language. The situations where I wished one could not use imperative constructs are very domain specific.
I assume that statement will need updating.
You get to decide what part of your code should be imperative, and which should be functional.
I don't understand why the goal is not to (eventually) implement Roc in Roc, maybe with a dash of something else for the "platform".
Roc is pitched as a general purpose functional programming language with great performance.
How does a compiler not fall under this category?
I first learned about Roc from this talk:
https://www.youtube.com/watch?v=vzfy4EKwG_Y
That's a niche that is not currently filled (well, maybe MLTon) and that has me very excited! I'm sure I'm not alone here.
Around 2014, I did some experiments with OCaml, and liked it very much
Then I went to do lexing and parsing in OCaml, and my experience was that Python/C++ are actually better for that.
Lexing and parsing are inherently stateful, it's natural to express those algorithms imperatively. I never found parser combinators compelling, and I don't think there are many big / "real" language implementations that uses them, if any. They are probably OK for small languages and DSLs
I use regular expressions as much as possible, so it's more declarative/functional. But you still need imperative logic around them IME [1], even in the lexer, and also in the parser.
---
So yeah I think that functional languages ARE good for writing or at least prototyping compilers -- there are a lots of examples I've seen, and sometimes I'm jealous of the expressiveness
But as far as writing lexers and parsers, they don't seem like an improvement, and are probably a little worse
[1] e.g. lexer modes - https://www.oilshell.org/blog/2017/12/17.html
But I'm saying there's nothing better about it than doing it in OCaml vs. C++ or Python -- it's the same or a little worse
IMW it's natural to express the interface to a lexer and parser as classes -- e.g. you peek(), eat(), lookahead(), etc.
Classes being things that control mutation
But objects in OCaml seem to be a little separate dialect: https://dev.realworldocaml.org/objects.html
When I debug a parser, I just printf the state too, and that is a little more awkward in OCaml as well. You can certainly argue it's not worse, but I have never seen anyone argue it's better.
---
Culturally, I see a lot of discussions like this, which don't really seem focused on helping people finish their parsers:
https://discuss.ocaml.org/t/why-a-handwritten-parser/7282/7
https://discuss.ocaml.org/t/good-example-of-handwritten-lexe...
I also use lexer/parser generators, and I like that there are more tools/choices available in C/Python than in OCaml.
There are a number of new imperative features that have been (or will be) added to the language that capture a lot of the convenience of imperative languages without losing functional guarantees. Richard gave a talk about it here: https://youtu.be/42TUAKhzlRI?feature=shared.
sorry, but why?
A compiler is a function from source code strings to binary bytes. Writing out instructions to do memory-unsafe things is not in itself a memory-unsafe activity.
i'm glad that zig helps offset what i find a rather worrying trend -- replacing protocols with libraries (and as a consequence designing new underlying protocols in view of their only being used through the vendor library).
protocols are traditionally designed to facilitate an independent implementation; in fact, many standards ratification processes require several independent implementations as a prerequisite.
libraries (or worse, frameworks) intermingle the actual api with their own design, and necessitate a single implementation.
just the other day i wanted to display a notification a linux desktop where it wasn't convenient to depend on a library (it was a game, so not a traditional application expected to have many dependencies). the protocol (there is one, wrapped by the library) is very unpleasant, but i got it working out of spite.
and of course, when there is a perfectly nice protocol available (llvm ir, in either the bitcode or text representation) why not choose it? at least on unix, where starting processes and interprocess communication is cheap. (and as an added bonus, you won't crash when llvm does.)
Rust on the other hand didn’t prioritize compile times and ended up making design decisions that make faster compilation difficult to achieve. To me it’s the biggest pain point with Rust for a large code base and that seems to be the sentiment here as well.
(Source: I wrote much of the Rust compiler.)
On that note, thank you for your part! I sure enjoy your work! :)
But now it's 2025, and the new version still hasn't been enabled on stable Rust, since the Polonius implementation is seen as far too slow for larger programs. (I'm not exactly sure how horrible it really is, I haven't looked at the numbers.) A big goal of the types team since last year has been to reimplement a faster version within rustc [2].
I'd count this as a language feature (albeit a relatively minor one) that's been greatly deferred in favor of shorter compile times.
[0] https://blog.rust-lang.org/inside-rust/2023/10/06/polonius-u...
[1] https://github.com/rust-lang/rust/pull/51133
[2] https://rust-lang.github.io/rust-project-goals/2024h2/Poloni...
Why aren't people designing modern languages to make it easier to keep a stable ABI, rather than giving up entirely?
I think a significant portion of our pain with rust compile times is self inflicted due to the natural growth of our crate organization and stages.
I still think the rewrite in zig is the right choice for us for various reasons, but I think at least a chunk of our compile times issues are self inflicted (though this happens to any software project that grows organically and is 300k LOC)
Many foundational crates, serde for example, contribute much more to compile times than they need to.
I spent a long time reinventing many foundational rust crates for my game engine, and I proved its possible to attain similar features in a fraction of the compile time, but it’s a losing battle to forgo most of the ecosystem.
Not to mention, Richard has a background mostly doing higher level programming. So jumping all the way to something like C or Zig would have been a very big step.
Sometimes you need a stepping stone to learn and figure out what you really want.
If you read the linked post carefully you will know.
> Compile times aside, the strengths and weaknesses of Rust and Zig today are much different than they were when I wrote the first line of code in Roc's Rust compiler in 2019. Back then, Rust was relatively mature and Zig was far from where it is today.
Almost all parser combinators are recursive descent with backtracking, they just add higher-order plumbing.
I have a feeling that whatever issue they've encountered with the combinator approach could have been worked around by handwriting only a few pieces.
Personally, I find parser combinators nice for small things but painful for large and robust things.
One specific thing I like is that the motivation for this rewrite is organic, i.e., not driven by an external pressure campaign. It's refreshing to see a drama-free rewrite.
Someone made a joke comment how in a few years there would be tons of HN threads about rewriting Rust to Zig.
There are probably some risks to it. And I think that you wouldn’t want to release a Roc v1 before Zig v1 as well.
But if things are working well now, you could always stay on an older version of Zig while you rewrite things for breaking changes in a newer version.
Still potentially a pain, but Rust is post v1 and they ended up deciding to rewrite anyway, so there are no guarantees in any approach.
On top of that, zig has gotten a lot more robust and stable of the last few releases. Upgrading is getting smoother.
But yeah, it is a risk.
I think I'd rather stick with C and run static analysis tools on my code when generating release candidates than have to deal with that.
It's somewhere in the middle of the list of the "Why now?" section.
Papercuts like this do make or break adoption.
Even C++ is improving on that front.
Although ClearMake object sharing was quite neat experience back in the day.
LLVM improvements will do little to improve Rust, because the major issue is the lack of parallelism and massive amount of LLVM IR that the fronted gives to the backend to handle.
Falsified many times during Rust's development.
Yes, Rust compile times have improved a lot, still I get better ones in C++, without having to reach out external tools.
Bevy's tutorial for compile times shouldn't be needed in first place.
Aren't parser combinators just one way to do recursive descent?
So yeah, I am not interested. Plus I feel in general not dogfooding your own language is a bad sign.
Dog-fooding is important, but it can be done outside of compiler development. If the only code the compiler team writes is the compiler, they end up designing a language that's great for implementing compilers at the expense of other use cases.
Their motivation seems to justify the decision, but I'm pretty sure this will scare away potential contributors. I've been following Roc personally, and probably will continue to do so, but this definitely has killed a lot of interest for me.
I'm part of the core team, and I'm honestly still a bit surprised we're switching to zig. I think it makes sense, it just kinda finally aligned and was decided now. I guess enough technical debt piled up and the core of the language was clearly discovered. So we really know what we want now.
I am a bit sad that Roc is following similar pitfalls as Elm in its quest to be simple by rejecting to have more advanced type system features. That just does not work. In dynamic languages you can opt for minimalism because everything goes by default but in fully statically typed languages things can painful quickly. Even golang had to add generics.
Still many amazing ideas in the language. The platform concept is pretty neat. Not doing implicit currying is a good move.
One thing that makes Go's restrictive type system more bearable is a fantastic stlib package for analyzing and generating Go code. I wonder if Roc will get anything similar.
I’ve been immersed in Swift for a couple years after working as a Go programmer, and I find myself pining for a language that’s more expressive than the latter and more concise than the former.
> Roc's compiler has always been written in Rust, and we do not plan to self-host. We have a FAQ entry explaining why, and none of our reasoning has changed on that subject.
So why not something like OCaml that has fast compile times and a lot of nice features for writing compilers? (pattern matching, etc.)
This includes the Roc compiler too. Zig is significantly faster than OCaml.
A Roc compiler written in Zig would compile Roc code significantly faster than a Roc compiler written in OCaml.
GCed languages can be fast, probably even as fast as any of the aforementioned languages with enough engineering effort, but in terms of high performance, it's actually easier to write high performance code in the aforementioned languages rather than in a GCed language.
> Zig has known bugs and even some miscompilations.
> Zig is immature. Even with Zig 0.13.0, working on a non-trivial project using Zig will likely require participating in the development process.
But then again, it's a great thing for the language to be used in larger public projects (like tigerbeetle, bun, ghostty). This will drive its development and adoption.
sweet@nadeko ~ $ mkcd test
sweet@nadeko ~/test $ zig init
info: created build.zig
info: created build.zig.zon
info: created src/main.zig
info: created src/root.zig
info: see `zig build --help` for a menu of options
sweet@nadeko ~/test $ time zig build
zig build 5.08s user 0.58s system 119% cpu 4.745 total
# cached
sweet@nadeko ~/test $ time zig build
zig build 0.01s user 0.03s system 132% cpu 0.026 total
# after rewriting the main function to call a function that takes a pointer to another function
sweet@nadeko ~/test $ time zig build
zig build 0.02s user 0.03s system 136% cpu 0.032 total
```
Behind the scenes, it uses Rust's high-performance hyper and tokio libraries to
execute your Roc function on incoming requests.
Will these be replaced with Zig networking libs?its build system is also terrible for targeting lots of varying platforms and feature sets.
it has a custom IR that isn't helpful interacting with other systems.
its runtime is really nice but it's a black box and not suitable for interop.