VISION
Code should just feel right.
When you look at this statement, you may be confused. How can you know your code feels right? What does it even mean for code to “feel right?”
To me, code feels right when I’m able to express myself without worrying about errors. Things are clear and explicit, with readable and clean syntax. I know this because I experienced the complete opposite when building a lexer in C++. I had many functions with implicit effects that made it hard for me to reason about my code.
But what really got to me was relying on comments to track what my functions did. Comments that could go stale, that I could (and usually did) forget to update and that weren’t actually true. It was hard to trust my code. Yes, I had written it, but I still had to double-check what it actually did.
This is what I mean by “coding should just feel right”. There should be no hidden state. What you see is what you get.
Everything is an expression!
Aera is an expression-oriented language. This means that (almost) every construct, including things that are traditionally "statements" in other languages (i.e., if, loops, blocks, and assignments), evaluates to a value. That value can then be used wherever a value is expected. There are many benefits of expression-oriented languages, but the one benefit I find important is the removal of temporary variables.
In statement-oriented languages, to compute something based on a branch, you often need to declare a variable above the branch and assign a value to it in each branch.
int result;if (cond) { result = a;}else { result = b;}The result is mutable and its final value depends on the control flow that you have to trace manually. You can't look at result's declaration and know what it is. In Aera, if is the value:
let result = if cond { a } else { b }Here, you can see what result will be without tracing through control flow. No uninitialized variable, no reassignment. What you see is what result is.
What, not how?
When coding, I find myself wanting to be able to separate what something does from how it does it. This sounds vague, and I promise you, it isn't.
Take this example. Say I want to add a logging feature to a program, but I don't want to have to create a logging class (with methods like debug, info, warning, and error) that's injected into every object needing to log something.
class Window { int width; int height; Logger _logger; ...} void resize_window(int new_width, int new_height) { if (new_width < 0 || new_height < 0) { _logger.error("Window width and/or height must be greater than 0."); return; } width = new_width; height = new_height;}Logging is effectively interwoven into the Window class, but that makes the code harder to follow. Why should Window need to know about logging at all? There should be a way to separate the two.
This is where algebraic effects come in. Effect handlers let you separate the "what" — what the effect is — from the "how" — the implementation of that effect.
With our logging example:
effect log(message: string) handle log(message: string) { # do logging stuff here}This separation of concerns increases readability, helping to achieve that goal of the language just feeling right.
Another benefit of algebraic effects is that they're resumable. Regular exceptions unwind the stack; effects continue execution from the point they were raised. Say you had a parser and wanted to add error recovery. Instead of throwing an exception on a missing token, you'd handle it and let the parser continue with a valid substitute. Things stay predictable, easy to follow, and clear to the programmer.
Side effects, begone!
Side effects can be painful. Trust me, I know. Imagine this: you're writing a function to advance a lexer's state. It returns the character at the current index, but also updates that index before returning. This is the same lexer I mentioned earlier with Aera's first iteration, written in C++:
char advance() { return source[index++]; # reads and mutates state}The function was reading and mutating state at the same time, with nothing in the signature to indicate it. Every time I called advance(), I had to remind myself that the character it returned belonged to the previous index.
That's why Aera makes side effects explicit. If a function performs I/O or mutates state, that's visible in the function signature. Some might argue this makes code more verbose, but Aera keeps things readable with clean, simple syntax.
When side effects are known to you upfront, your code becomes easier to follow and reason about.
What you see is what you get!
Aera is transparent by design. Effects, including side effects, control flow, and program behaviour, should be visible rather than implicit. Expression-orientation, algebraic effects, and explicit side effects. All of it exists in service of this one idea.
This is also why I keep coming back to "no hidden state." When behaviour is visible, you don't have to trust comments, look through call stacks, or hold half the program in your head just to know what a piece of code does. You can just look at it.
For coding to feel right, I believe Aera should be as transparent as possible to the programmer. What you see should be, as closely as possible, what you get.
A final note...
Saying that these things will make a language "feel right" is subjective. But through the research I've done on other language implementations and my own experience writing code, I believe these are the things that can make coding feel good, trustworthy, and clear.
That doesn't mean Aera won't have a learning curve. Most languages do. Feeling right isn't the same as being effortless. What it means is that the effort goes into solving your problem, not fighting the language to get there. Aera should still feel familiar to anyone who's spent time with modern programming languages.
This is the vision I have for Aera. Whether I've achieved it is for you to decide.
— Anjola