Rue
rue-lang.devKey Features
Tech Stack
Key Features
Tech Stack
> No garbage collector, no manual memory management. A work in progress, though.
I couldn't find an explanation in the docs or elsewhere how Rue approaches this.
If not GC, is it via:
a) ARC
b) Ownership (ala Rust)
c) some other way?
Do you see this as a prototype language, or as something that might evolve into something production grade? What space do you see it fitting into, if so?
You've been such a huge presence in the Rust space. What lessons do you think Rue will take, and where will it depart?
I see compile times as a feature - that's certainly nice to see.
It's a fun project for me right now. I want to just explore compiler writing. I'm not 100% sure where it will lead, and if anyone will care or not where it ends up. But it's primarily for me.
I've described it as "higher than Rust, lower than Go" because I don't want this to be a GC'd language, but I want to focus on ergonomics and compile times. A lot of Rust's design is about being competitive with C and C++, I think by giving up that ultra-performance oriented space, I can make a language that's significantly simpler, but still plenty fast and nice to use.
We'll see.
Have fun! :)
To me it just seems like Rust has Linear types, and the compiler just inserts some code to destroy your values for you if you don't do it yourself.
See https://faultlore.com/blah/linear-rust/ for a (now pretty old but still pretty relevant, I think) exploration into what linear types would mean for Rust.
(And sorry to hear about your brother's passing.)
Anyhow, I just thought it might be a good jumping off point for what you're exploring.
What's the practical implication of this - how does a Rue program differ from a Rust program? Does your method accept more valid programs than the borrow checker does?
Mutable value semantics means no references at all, from a certain perspective.
You can sort of think of linear types as RAII where you must explicitly drop. Sorta.
“More programs” isn’t really the right way to think about it. Different semantics, so different programs :)
It transpiles to Zig, so you have native access to the entire C library.
It uses affine types (simple ownership -> transfers via GIVE/TAKES), MVCC & transactions to safely and scalably handle mutations (like databases, but it scales linearly after 32 cores, Arc and RwLock fall apart due to Cache Line Bouncing).
In limits concurrent complexity only to the spot in your code WHERE you want to do something concurrently, not your entire codebase.
It's memory and liveness safe (Rust is only memory safe).
It's simpler than Go, too, IMO.
But it's nearly impossible to beat Go at its own game, and it's not zero overhead like Rust - so I'm pessimistic it's in a "sweet spot" that no one will be interested in.
Time will tell.
May be an interesting approach. That language seems very academic and slow moving at the moment though.
Does it? From its docs [0]:
> There are 4 ways to manage memory in V.
> The default is a minimal and a well performing tracing GC.
> The second way is autofree, it can be enabled with -autofree. It takes care of most objects (~90-100%): the compiler inserts necessary free calls automatically during compilation. Remaining small percentage of objects is freed via GC. The developer doesn't need to change anything in their code. "It just works", like in Python, Go, or Java, except there's no heavy GC tracing everything or expensive RC for each object.
> For developers willing to have more low-level control, memory can be managed manually with -gc none.
> Arena allocation is available via a -prealloc flag. Note: currently this mode is only suitable to speed up short lived, single-threaded, batch-like programs (like compilers).
So you have 1) a GC, 2) a GC with escape analysis (WIP), 3) manual memory management, or 4) ...Not sure? Wasn't able to easily find examples of how to use it. There's what appears to be its implementation [1], but since I'm not particularly familiar with V I don't feel particularly comfortable drawing conclusions from a brief glance through it.
In any case, none of those stand out as "memory safety without GC" to me.
[0]: https://docs.vlang.io/memory-management.html
[1]: https://github.com/vlang/v/blob/master/vlib/builtin/prealloc...
Regarding the details, here is a pretty informative github discussion thread on same topic: https://github.com/vlang/v/discussions/17419
It is also accompanied with a demo video (pretty convincing in case you would like to watch).
V-lang is not shiny as other languages are, but, it does have a lot to learn from.
As sibling said, autofree is still stated to use a GC, which obviously disqualifies it from "memory safety without GC".
> Regarding the details, here is a pretty informative github discussion thread on same topic: https://github.com/vlang/v/discussions/17419
I did see that! Unfortunately it doesn't really move the needle on anything I said earlier. It describes manual memory management as an alternative to the GC when using autofree (which obviously isn't conducive to reliable memory safety barring additional guardrails not described in the post) and arenas are only mentioned, not discussed in any real detail.
> It is also accompanied with a demo video (pretty convincing in case you would like to watch).
Keep in mind the context of this conversation: whether V offers memory safety without GC or manual memory management. Strictly speaking, a demonstration that autofree works in one case is not sufficient to show V is memory safe without GC/manual memory management, as said capability is a property over all programs that can be written in a language. As a result, thoroughly describing how V supposedly achieves memory safety without a GC/manual memory management would be far more convincing than showing/claiming it works in specific cases.
As an example of what I'm trying to say, consider a similar video but with a leak/crash-free editor written in C. I doubt anyone would consider that video convincing proof that C is a memory-safe language; at most, it shows that memory-safe programs can be written in C, which is a very different claim.
I'm just working off of what I could easily find, particularly in public docs. If the V devs have hinted at other possibilities, then you are certainly better positioned than me to know about them.
Are the hints you mention publicly visible somewhere? Would be interesting to see more details if they're available.
Based on what I've read and hints dropped, can see them adding some type of additional DFA and borrow checker, somewhat like D, to be combined with autofree. Awareness of this possibility, might be why V's competitors have been so overly focused on them. As we are talking about combining it to an easier to use language, with cleaner syntax.
Compile-time, reference-counting GC, not runtime tracing GC. So no background collector, no heap tracing, and no stop-the-world pauses. Very different from the JVM, .Net, or Go.
Additionally there isn't a single ARC implementation that is 100% compile time, that when looking at the generated machine code has removed all occurrences from RC machinery.
Saying that the language has GC just because it has opt-in reference counting is needlessly pedantic
Also implementation has nothing to do with CS definition, there are tracing GC libraries for C as well.
I can't see how the language allocates its structs or arrays or how it passes structs as arguments. I see no pointers.
Is everything on the stack thus far and allocated/deallocated following RAII? That might explain it.
As some of the larger design decisions come into place, I'll find a better way of describing it. Mostly, I am not really trying to compete with C/C++/Rust on speed, but I'm not going to add a GC either. So I'm somewhere in there.
As for Go... I dunno. Go has a strong vision around concurrency, and I just don't have one yet. We'll see.
If this ever becomes a production thing, then I can worry about FFI, and I'll probably just follow what managed languages do here.
I couldn't see how long-running memory is handled, is it handled similar to Rust?
I'm totally unsure about async.
Right now there's no heap memory at all. I'll get there :) Sorta similar to Rust/Swift/Hylo... we'll see!
Out of curiosity, how would you compare the goals of Rue with something like D[0] or one of the ML-based languages such as OCaml[1]?
Fascinating.
I look forward to seeing where you go with Rue over time.
You should use Rust for embedded, I doubt Rue will ever be good for it.
I'd love to see how Rue solves/avoids the problems that Rust's borrow checker tries to solves. You should put it on the 1st page, I think.
I'll put more about that there once it's implemented :)
There are quantitative ways of describing it, at least on a relative level. "High abstraction" means that interfaces have more possible valid implementations (whether or not the constraints are formally described in the language, or informally in the documentation) than "low abstraction": https://news.ycombinator.com/item?id=46354267
C was designed as a "high level language" relative to the assembly languages available at the time and effectively became a portable version of same in short order. This is quite different to other "high level languages" at the time, such as FORTRAN, COBOL, LISP, etc.
It didn't not even had compiler intrisics, a concept introduced by ESPOL in 1961, allowing to program Burroughs systems without using an external Assembler.
K&R C was high level enough that many of the CPU features people think about nowadays when using compiler extensions, as they are not present in the ISO C standard, had to be written as external Assembly code, the support for inline Assembly came later.
C is a relatively "low level" language. This
characterization is not pejorative; it simply means that C
deals with the same sort of objects that most computers do,
namely characters, numbers, and addresses.[0]
0 - https://dn710204.ca.archive.org/0/items/the-c-programming-la...As an independent axis from close to the underlying machine/far away from the underlying machine (whether virtual like wasm or real like a systemv x86_64 abi), which describes how closely the language lets you interact with the environment it runs in/how much it abstracts that environment away in order to provide abstractions.
Rust lives in high type system complexity and close to the underlying machine environment. Go is low type system complexity and (relative to rust) far from the underlying machine.
I don't think that's right. The level of abstraction is the number of implementations that are accepted for a particular interface (which includes not only the contract of the interface expressed in the type system, but also informally in the documentation). E.g. "round" is a higher abstraction than "red and round" because the set of round things is larger than the set of red and round things. It is often untyped languages that offer the highest level of abstraction, while a sophisticated type system narrows abstraction (it reduces the number of accepted implementations of an interface).
The way the level of abstraction is "felt" is by considering how many changes to client code (the user of an interface) is required when making a change to the implementation. Languages that are "closer to the underlying machine" - especially as far as memory management goes - generally have lower abstraction than languages that are less explicit about memory management. A local change to how a subroutine manages memory typically requires more changes to the client - i.e. the language offers a lower abstraction - in a language that's "closer to the metal", whether the language has a rich type system like Rust or a simpler type system like C, than a language that is farther away.
As a more concrete example, the way I interpreted GP's comment is that a language that is unable to natively express/encode a tagged union/sum type/etc. in its type system would fall on the "less complex/less power to define abstractions" side of the proposed spectrum, whereas a language that is capable of such a thing would fall on the other side.
> which includes not only the contract of the interface expressed in the type system, but also informally in the documentation
I also feel like including informal documentation here kind of defeats the purpose of the axis GP proposes? If the desire is to compare languages based on what they can express, then allowing informal documentation to be included in the comparison renders all languages equally expressive since anything that can't be expressed in the language proper can simply be outsourced to prose.
I think you're right with respect to discussion about abstractions in the context of high-/low-level languages, but again, I feel like what GP was trying to get away from the high-/low-level framing in the first place and might have meant something different when they used the word "abstraction".
Perhaps this is me misinterpreting things, but I took GP's use of "abstraction" as something more along the lines of what it might mean in "this library's abstractions are designed poorly/well because they are easy/hard to misuse and/or understand". In that context I think "abstraction" is more about the precise interface contract and its quality - e.g., a poorly-chosen abstraction might not reflect the domain it ostensibly represents well because it permits actions/behaviors that don't make sense for that domain, and that in part might be due to a language being unable to express a more appropriate contract. I feel that better matches GP's high-/low-type-system-complexity axis.
I did C and typescript first. At the time, my C implementation ran about 20x faster than typescript. But the typescript code was only 2/3rds as many lines and much easier to code up. (JS & TS have gotten much faster since then thanks to improvements in V8).
Rust was the best of all worlds - the code was small, simple and easy to code up like typescript. And it ran just as fast as C. Go was the worst - it was annoying to program (due to a lack of enums). It was horribly verbose. And it still ran slower than rust and C at runtime.
I understand why Go exists. But I can't think of any reason I'd ever use it.
I still regularly use typescript. One problem I run into from time to time is "spooky action at a distance". For example, its quite common to create some object and store references to it in multiple places. After all, the object won't be changed and its often more efficient this way. But later, a design change results in me casually mutating that object, forgetting that its being shared between multiple components. Oops! Now the other part of my code has become invalid in some way. Bugs like this are very annoying to track down.
Its more or less impossible to make this mistake in rust because of how mutability is enforced. The mutability rules are sometimes annoying in the small, but in the large they tend to make your code much easier to reason about.
C has multiple problems like this. I've worked in plenty of codebases which had obscure race conditions due to how we were using threading. Safe rust makes most of these bugs impossible to write in the first place. But the other thing I - and others - run into all the time in C is code that isn't clear about ownership and lifetimes. If your API gives me a reference to some object, how long is that pointer valid for? Even if I now own the object and I'm responsible for freeing it, its common in C for the object to contain pointers to some other data. So my pointer might be invalid if I hold onto it too long. How long is too long? Its almost never properly specified in the documentation. In C, hell is other people's code.
Rust usually avoids all of these problems. If I call a function which returns an object of type T, I can safely assume the object lasts forever. It cannot be mutated by any other code (since its mine). And I'm not going to break anything else if I mutate the object myself. These are really nice properties to have when programming at scale.
> If I call a function which returns an object of type T, I can safely assume the object lasts forever. It cannot be mutated by any other code (since its mine). And I'm not going to break anything else if I mutate the object myself. These are really nice properties to have when programming at scale.
I rarely see this mentioned in the way that you did, and I'll try to paraphrase it in my own way: Rust restricts what you can do as a programmer. One can say it is "less powerful" than C. In exchange for giving up some power, it gives you more information: who owns an object, what other callers can do with that object, the lifetime of that object in relation to other objects. And critically, in safe Rust, these are _guarantees_, which is the essence of real abstraction.
In large and/or complicated codebases, this kind of information is critical in languages without garbage garbage collection, but even when I program in languages with garbage collection, I find myself wanting this information. Who is seeing this object? What do they know about this object, and when? What can they do with it? How is this ownership flowing through the system?
Most languages have little/no language-level notion of these concepts. Most languages only enforce that types line up nominally (or implement some name-identified interface), or the visibility of identifiers (public/private, i.e. "information hiding" in OO parlance). I feel like Rust is one of the first languages on this path of providing real program dataflow information. I'm confident there will be future languages that will further explore providing the programmer with this kind of information, or at least making it possible to answer these kinds of questions easier.
Your paraphrasing reminds me a bit of structured vs. unstructured programming (i.e., unrestricted goto). Like to what you said, structured programming is "less powerful" than unrestricted goto, but in return, it's much easier to follow and reason about a program's control flow.
At the risk of simplifying things too much, I think some other things you said make for an interesting way to sum this up - Rust does for "ownership flow"/"dataflow" what structured programming did for control flow.
The restrictions seem a bit silly to list out because we take them for granted so much. But its things like:
- When a function is called, execution starts at the top of the function's body.
- Outside of unions, variables can't change their type halfway through a program.
- Whenever a function is called, the parameters are always passed using the system calling convention.
- Functions return to the line right after their call site.
Rust takes this a little bit further, adding more restrictions. Things like "if you have a mutable reference to to a variable, there are no immutable references to that variable."
Async is an irritation but not the end of the world ... You can write non asynchronous code I have done it ... Honestly I am coming around on async after years of not liking it... I wish we didn't have function colouring but yeah ... Here we are....
If anything, borrow checker makes writing functions harder but combining them easier.
I almost never even think about the borrow checker. If you have a long-lived shared reference you just Arc it. If it's a circular ownership structure like a graph you use a SlotMap. It by no means is any harder for this codebase than for small ones.
I will say, that for most of the Rust code that I've read, the vast majority of it has been easy enough to read and understand... more than most other languages/platforms. I've seen some truly horrendous C# and Java projects that don't come close to the simplicity of similar tasks in Rust.
Typescript also lacks enums. Why wasn't it annoying?
I mean, technically it does have an enum keyword, but that keyword behaves exactly the same as Go.
All three languages do have enums, though. Go is only the odd one out by using a different keyword. As these programs were told to be written to be carbon copies of each other, not written to the idioms of the language, it is likely the author didn't take time to understand what features are available. No enum keyword was assumed to mean it doesn't exist at all.
I’ve been programming for 30+ years. Long enough to know direct translations between languages are rarely beautiful. But I’m not an expert in Go. Maybe there’s some tricks I’m missing?
The code in question defines a text editing operation as a list of editing components: Insert, Delete and Skip. When applying an editing operation, we start at the start of the document. Skip moves the cursor forward by some specified length. Insert inserts at the current position and delete deletes some number of characters at the position.
Eg:
enum OpComponent {
Skip(int),
Insert(String),
Delete(int),
}
type Op = List<OpComponent>
Then there’s a whole bunch of functions with use operations - eg to apply them to a document, to compose them together and to do operational transform.How would you model this in Go?
C has unions, but they're not tagged. You can roll your own tagged unions, of course, but that's moving beyond it being a feature of the language.
> How would you model this in Go?
I'm committing the same earlier sin by trying to model it from the solution instead of the problem, so the actual best approach might be totally different, but at least in staying somewhat true to your code:
type OpComponent interface { op() }
type Op = []OpComponent
type Skip struct { Value int }
func (s Skip) op() {}
type Insert struct { Value string }
func (i Insert) op() {}
type Delete struct { Value int }
func (d Delete) op() {}
op := Op{
Skip{Value: 5},
Insert{Value: "hello"},
Delete{Value: 3},
}This feels like a distinction without a real difference. Hand-rolled tagged unions are how lots of problems are approached in real, professional C. And I think they're the right tool here.
> the actual best approach might be totally different, but at least in staying somewhat true to your code: (...)
Thanks for having a stab at it. This is more or less what I ended up with in Go. As I said, I ended up needing about 50% more lines to accomplish the same thing in Go using this approach compared to the equivalent Typescript, rust and swift.
If anyone is curious, here's my C implementation: https://github.com/ottypes/libot
Swift: https://github.com/josephg/libot-swift
Rust: https://github.com/josephg/textot.rs
Typescript: https://github.com/ottypes/text-unicode
I wish I'd kept my Go implementation. I never uploaded it to github because I was unhappy with it, and I accidentally lost it somewhere along the way.
> the actual best approach might be totally different
Maybe. But honestly I doubt it. I think I accidentally chose a problem which happens to be an ideal use case for sum types. You'd probably need a different problem to show Go or C# in their best light.
But ... sum types are really amazing. Once you start using them, everything feels like a sum type. Programming without them feels like programming with one of your hands tied behind your back.
Which is fine. Lines of code is a metric that matters not one bit. Perl can write some amazing one liners, but I'm not about to choose Perl because of that, nor world anyone else.
But there is a folly in trying to model from a solution instead of the problem. For example, maybe all you needed was:
type OpType int
const (
OpTypeSkip OpType = iota
OpTypeInsert
OpTypeDelete
)
type OpComponent struct {
Type OpType
Int int
Str string
}
Or something else entirely. Without fully understanding the exact problem, it is hard to say what the right direction is, even where the direction you chose in other languages was the right one for that language. What is certain is that you don't want to write code in language X as if it were language Y. That doesn't work in programming languages, no does it work in natural languages. Every language has their own rules that don't transfer to another.> You'd probably need a different problem to show Go or C# in their best light.
That said, my profession sees me involved in working on a set of libraries in various languages, including Go and Typescript, that sound an awful lot like your example. And I can say from that experience that the Go version is much more pleasant to work on. It just works every time without the fuss or muss the Typescript version always seems to end up with.
I'll agree with you all day every day that the Typescript version's types are much more pleasant to read. It absolutely does a better job at modelling the domain. No question about it. But you only need to read it once to understand the model. When you have to fight everything else after that, it is of little consolation how beautiful the type definitions are.
What are you “fighting all day” in typescript? That’s not my experience with TS at all.
What are the virtues of go, that you’re so enamoured by? If we give up beauty and type safety, what do you get in trade?
type Operation = {type: “insert”, …} | {type: “delete”, …} | …;
It’s trivial to switch based on the type field. And when you do, typescript gives you full type checking for that specific variant. It’s not as efficient at runtime as C, but it’s very clean code.Go doesn’t have any equivalent to this. Nor does go support tagged unions - which is what I used in C. The most idiomatic approach I could think of in Go was to use interface {} and polymorphism. But that was more verbose (~50% more lines of code) and more error prone. And it’s much harder to read - instead of simply branching based on the operation type, I implemented a virtual method for all my different variants and called it. But that spread my logic all over the place.
If I did it again I’d consider just making a struct in go with the superset of all the fields across all my variants. Still ugly, but maybe it would be better than dynamic dispatch? I dunno.
I wish I still had the go code I wrote. The C, rust, swift and typescript variants are kicking around on my github somewhere. If you want a poke at the code, I can find them when I’m at my desk.
When you want your project to be able to cross-compile down to a static binary that the end user can simply download and run without any "installation" on any mainstream OS + CPU arch combination
From my M1 Mac I can compile my project for Linux, MacOS, and Windows, for x86 and ARM for each. Then I can make a new Release on GitHub and attach the compiled binaries. Then I can curl the binaries down to my bare Linux x86 server and run them. And I can do all of this natively from the default Go SDK without installing any extra components or system configurations. You don't even need to have Go installed on the recipient server or client system. Don't even need a container system either to run your program anywhere.
You cannot do this with any other language that you listed. Interpreted languages all require a runtime on the recipient system + library installation and management, and C and Rust lack the ability to do native out-of-the-box cross compilation for other OS + CPU arch combinations.
Go has some method to implement enums. I never use enums in my projects so idk how the experience compares to other systems. But I'm not sure I would use that as the sole criteria to judge the language. And you can usually get performance on par with any other garage collected language out of it.
You can always emulate functionality on different architectures, though, so where is the line even drawn?
The line is blurred, and doesn't help that some folks help spread the urban myth C is special somehow, only because they never bother with either the history of programming language, and specially the history of systems programming outside Bell Labs.
While most modern CPUs are designed for C, if your CPU is of a different design, you have to emulate the behaviour. Which works perfectly fine — but the question remains outstanding: Where does the practical line get drawn? Is 6502 assembler actually a high-level language too? After all, you too can treat it as an abstract machine and emulate its function on any other CPU just the same as you do with C pointers.
One, objective definition is simply that everything that is not an assembly is a high-level language - but that is quite a useless def. The other is about how "deeply" you can control the execution, e.g. you have direct control of when and what gets allocated, or some control over vectorization, etc.
Here Rust is obviously as low-level as C, if not more so (both have total control over allocations, but still leaves calling conventions and such up to the compiler), while go is significantly higher (the same level as C#, slightly lower than Java - managed language with a GC and value types).
The other often mistaken spectrum is expressivity, which is not directly related to low/high levelness. E.g. both Rust and Scala are very expressive languages, but one is low, the other is high level. C and Go both have low expressivity, and one is low the other is high level.
This answer is imo a very must have read about the topic of expressivity: https://langdev.stackexchange.com/a/2016
It is similar to PyTorch (which I also like), where you can add two tensors by hand, or have your whole network as a single nn.Module.
Perhaps calling it an “optimization” is misleading. Certainly it makes code faster, but more importantly it’s syntax sugar to translate recursion into loops.
(To be fair, if you are programming functionally, it is essential. But to flat-out state that a language that doesn't support isn't "serious" is a bit rude, at best.)
If I put out a language that crashed after 1000 iterations of a loop, I'd welcome the rudeness.
If every iteration of a while-loop cost you a whole stack frame, then I'd be very rude about that language.
This works, btw:
#include <stdio.h>
long calc_sum(int n, long acc) {
return n == 0
? acc
: calc_sum(n-1, acc+n);
}
int main(void) {
int iters = 2000000;
printf("Sum 1...%d = %ld\n", iters, calc_sum(iters, 0));
return 0;
}Well, sure, but real programmers know how to do while loops without invoking a function call.
I also find that D is good between language. You can do high level or low level whenever you need it.
You can also do some inbetween systems programming in C# if you don’t care about a VM or msft.
C# Native AOT gets rid of the JIT and gives you a pretty good perf+memory profile compared to the past.
It's mostly the stigma of .NET Framework legacy systems that put people off, but modern C# projects are a breeze.
I’m looking forward to seeing how it shapes out over the next few years. Especially once they release union types.
Zig really aims to be great at things I don't imagine Rue being useful for, though. But there's lots of good stuff there.
And lots of respect to Swift as well, it and Hylo are also major inspiration for me here.
One huge difference is that Hylo is using LLVM, whereas I'm implementing my own backends. Another is that Hylo seems to know what they want to do with concurrency, whereas I really do not at all right now.
I think Hylo takes a lot of inspiration from Swift, whereas I take more inspiration from Rust. Swift and Rust are already very similar. So maybe Hylo and Rue will end up like this: sister languages. Or maybe they'll end up differently. I'm not sure! I'm just playing around right now.
And congrats on starting a language project, even if Just for Fun (Linux). ;)
https://frappe.io/blog/book-reviews/just-for-fun-a-book-on-l...
Both are playing around in similar spaces, both shared team members for a while, both have a take an automatic memory management that isn’t a garbage collector, both were sponsored by a primary company for a while (Swift still is, I think). There’s a lot of differences too.
I may eventually diverge from this, but I like Rust's syntax overall, and I don't want to bikeshed syntax right now, I want to work on semantics + compiler internals. The core syntax of Rust is good enough right now.
I'm using @ for intrinsics because that's how Zig does it and I like it for similar reasons to how Rust uses ! for macros.
I'd like fast compile times, and giving up some of Rust's lowest level and highest performance goals in exchange for it. As well as maybe ease of use.
I've thought a Rust like language but at Go's performance level would be interesting. Garbage collected, but compiled to a binary (no VM), but with Rust's mix of procedural and functional programming. Maybe some more capable type inference.
If you don't mind me asking, how did you get started with programming language design? I've been reading Crafting Interpreters, but there is clearly a lot of theory that is being left out there.
Crafting interpreters is fantastic!
Mostly just… using a lot of them. Trying as many as I could. Learning what perspectives they bring. Learning the names for their features, and how they fit together or come into tension.
The theory is great too, but starting off with just getting a wide overview of the practice is a great way to get situated and decide which rabbit holes you want to go down first.
Well I got that part covered at least. Seems like I'm constantly getting bored and playing around with a different language, probably more than I should lol
Just to link them all together. This is the one that the algorithm picked up :)
I maintain a medium sized, old-ish C++ code base. It uses classes and inheritance and virtual methods and even some multiple inheritance. I despise this stuff. Single Inheritance is great until you discover that you have a thing that doesn’t slot nicely into the hierarchy or when you realize that you want to decompose an interface (cough, base class) into a couple of non-hierarchically related things. Multiple inheritance is an absolute mess unless you strictly use base classes with pure virtual methods and no member variables. And forcing everything into an “is a” relationship instead of a “has a” relationship can be messy sometimes.
I often wish C++ had traits / or Haskell style type classes.
93 more comments available on Hacker News
Not affiliated with Hacker News or Y Combinator. We simply enrich the public API with analytics.