-
Notifications
You must be signed in to change notification settings - Fork 3
Notes on the Design of Epoch
I'm putting this page together to collect a bunch of historical thoughts about how and why I designed the Epoch language. This is in response to some questions I've received.
There is an important piece of context to keep in mind when reading this page. Epoch is not a single incarnation of a completed language. There are many flavors to think about: the original vision I had when I set out to create the language; the vision I have now, after years of refinement and learning; the parts I actually managed to implement in 32-bit; and the parts I have (most recently) been working to port into 64-bit.
Many of these questions need to be answered more than once, from more than one of these perspectives.
When I started the project, I really wanted a language that didn't frustrate me to the severe extent that C++ did. Keep in mind this was a long time ago - pre C++11 by a few years. (After C++11 landed, I actually went through a considerable period of questioning if I still wanted to replace it at all.) Back then, I was really interested in functional programming - but not purity, which I still think is a silly, idealistic, naive thing to spend time trying to achieve. I wanted a way to blend imperative and functional code and get really good binaries from the mix.
I was most excited about a language that was fast but not at the expense of sanity. I called it "pragmatism." My goal was to balance the rich features of languages besides C++ with the raw performance potential of lower-level languages. I also really, really wanted to be lazy.
More than a decade later, I still feel mostly the same. Idealism and zealotry in language design do not interest me. I still believe it is possible to get many different kinds of code to play nice together. Something I still really want to do is build a system where garbage-collection enabled code and code using other memory management strategies can interoperate in a single binary.
I have also gained a really potent appreciation for tooling. Early on I had the hubris to plan around writing an IDE, debugger, and so on in Epoch itself. I still insist on the compiler being 100% Epoch code (modulo the LLVM libraries I link in) but I have switched gears to targeting Visual Studio for IDE/debugger support. I am really proud of the progress I've made on integrating Epoch into VS, even though there is still a huge amount left to do.
Pretty much all of it. I still find every language I've ever used to annoy me in some way. I have let go of the prideful assumption that Epoch won't annoy me when I finally ship it, but I still hope it will annoy me less.
I really want to have control over things when I need it and to be able to just fire and forget when I don't.
I want the "easy default" to be really close to the optimal strategy most of the time. In other words, if I just sit down and write some code that makes sense, it should be a good solution almost always. I really hate it when I sit down in a language, write some stuff, and it turns out "everyone knows" that it's a bad approach. C# does this to me all the time which makes me very sad. Python does it more (although I detest Python for larger reasons). JavaScript is like the canonical example of getting this wrong.
It's hard to pick one thing, because Epoch isn't really a single killer feature. It's a synthesis of a lot of good features into a package that just hasn't been mixed together that way before.
Actually, none of it. Almost all of the language-like things I put into Epoch (even conceptually) are stolen or simply remixed from other languages. What excites me about it is the systemic design of it all: beyond stuff like syntax or libraries and on to the greater harmony of all the features coming together to make programming just feel good.
From a linguistic point of view I think Epoch is actually kind of primitive. The type system is super basic, there is no inheritance notion, there's barely any templating, the syntax is reductionist but not minimal... in the best case I think Epoch holds its own with other really solid languages. I don't think it really pushes the envelope if you look at it from the lens of single language elements.
Epoch's 32-bit implementation (which is stable and released) uses tracing garbage collection. The GC itself is simple - mark/sweep, non-generational, stop-the-world, etc. There is no way to allocate or reference memory that is not GC controlled.
I want the future to look very different. I want to allow combining the GC facility with manual (or other automatic) resource control mechanisms. In particular I want to get a solid RAII model into the language to enable that. And I also want to allow blending - some code modules use GC, others use malloc, etc.
Another critical piece of this is that I don't believe in the language dictating memory control. Allocators (in the C++ sense) are very important to me. From my background in games, I find memory (and resource) allocation management to be extremely useful. Most languages are rubbish at it, and C++ is only slightly better. I really want a language where this is effortless and clean.
Do you lean towards giving the programmer control, or having a smart compiler that interprets intent?
(I paraphrased this one a bit for brevity.)
This is a tricky one. I believe that programmers should be effective. To be effective we must be efficient, and to be efficient we must relinquish control on some occasions. This is precisely why we don't write MMOs in assembly language. I think having control as an option for special cases is important, but I don't want to care 80% of the time. So I want programmers to have control but not to want it.
There is an old programming language development idea of a "sufficiently intelligent compiler." Having done a fair bit of work in both PL implementation and AI, I think this idea is hilariously ignorant. Interpreting intent is impossible. We can't even get phones to understand when we poke them. How can we possibly expect a computer to know what we want in something as complex and detail-sensitive as a piece of code?
We are moving towards an increasingly software-pervasive world. If I can indulge a philosophical digression, I think that it is vital for us as a species to be responsible with how we handle creating that software. Self-driving cars, Mars landers, whatever other things Future Humans invent - we need to know we can trust them. Intelligent compilers will kill people.
Intelligence and comprehension are nice things for compilers to offer but they must never be the "golden path" to getting software built. They are optimizations, not things to fundamentally rely upon. We need to treat them as suggestion-generators, not things that we believe in.
Frankly, I started out trying to find a blurring of the lines between C and s-expressions. I wanted Lisp more than I wanted C.
The longer I spend working with syntax, the less I care about large structural details. I'm happy to learn from history and convention here. So Epoch has curly braces for blocks, parentheses for function calls, and so on. I have slimmed down the syntax noticeably over time. I find that being familiar-looking is sometimes useful but also sometimes harmful. I like to make things that work like conventional syntax to also look like conventional syntax (blocks, function calls). Things that are different get something different.
My biggest obsession in syntax is consistency. I want everything to feel like it makes sense. If you see a new syntax element in Epoch, it should immediately feel "right" - as if you knew it all along. This requires building a lot of patterns and similarities into the syntax itself. It also forces me to think very hard before adding syntax, which I find valuable from a design perspective.
I have stopped listing things I want to add to Epoch, because my list of things that I already want is huge enough as it is. Someday as demand shifts and I get things done I might introduce new stuff.
There were a few specific features called out in the original question:
- Pattern matching - this already exists except it a wonky way. You do it by overloading functions. I do plan to add
match{}
blocks someday. - Multimethods - already exists as well. Function overloading in Epoch is quite potent.
- Concepts - I find this inherently tied to template-oriented programming. I want to enrich Epoch's template system but honestly not by very much. I have other thoughts which I think are much more powerful, but I still don't have good terminology for describing them.