Many of the engineers who were most successful with AI-based development had years of experience:
- Writing, testing, deploying, and debugging code themselves
- Architecting software and delivering it in manageable milestones
- Managing engineers who make mistakes and have flaws
With this background and some practice, it wasn’t that hard to get good results out of AI even in these early days of coding agents, but how do new engineers gain this experience? Should they be spending huge amounts of time learning how to program in incredibly complex languages that they will never practically code in themselves?
Ether offers an alternative approach to learning programming fundamentals without all of the complexity baked into modern programming languages.
What is Ether?
Put simply, Ether is a meta programming language built on a non-numeric machine abstraction.
Most popular, modern programming languages are more the same than they are different, and a lot of that similarity derives from how computers and CPUs in particular have operated for decades. Ether deliberately departs from that paradigm by providing a very simple but low-level machine model which provides a solid and easy to use foundation to build concise, expressive programs.
Like all programming languages, Ether shares attributes with many predecessor languages. To name a few, it uses a post-fix execution model similar to Forth. It has a message-passing concurrency model similar to Erlang. It leans heavily on the functional language families in how it leverages immutable data structures and first-class functions. Perhaps most of all, it aims for simplicity over the machine abstraction in the way that C did for real world machines.
However, it’s what Ether doesn’t do that is probably most defining. It doesn’t inherit the complex syntax and execution that is ubiquitous in modern languages whether it's a derivative of the C-family, a divergent branch like Python, or something completely out there like Ruby. Seasoned programmers might shake their heads at such a statement initially, but consider just a simple Python function like this:
def counts(l: list[str]) -> dict[str,int]:
'''Count items in a list.'''
result: dict[str,int] = {}
for v in l:
result[v] = result.get(v, 0) + 1
return result
It’s hard to think of a simpler function that actually has a loop yet explaining how this code is parsed and executed would probably take about fifty pages of introductory computer science text to be thorough about it. It’s easy for fluent software engineers to look past that complexity and to lose sight of how many years of intuition they have accumulated on everything from lexical scope to tokenization which allows them to pick up a new language in a matter of hours or days. People who have never coded before don’t have that foundation and arguably, they won’t ever need it.
A Language Specification
Ether is a language specification more than a particular implementation because it is so simple to implement.
Since 2008, I’ve implemented variants of Ether many times in many different languages. A minimalist Ether interpreter usually takes one line for the tokenizer and about 10 to 20 for the core code interpreter. The rest is just whatever functions you want it to have built-in. Only a handful are really needed to make it possible to bootstrap a general purpose Ether environment.
Back in 2012 there was a reference implementation of fully featured Ether written in C that executed at similar speed to Python but with unbounded, lock-free concurrency. This version met the full details of the specification including the object, concurrency, and memory model with the correct performance characteristics, and because of that, the implementation was quite complex. However, this is rarely necessary to get most of the benefit of learning Ether or even of using it as an embedded interpreter language. In some ways, it’s more useful to actually implement it yourself and build up to the finer points if and when you need them.
Objects
To understand Ether execution, you need to understand the basic object types that make up Ether. In full Ether, there are 11 types though you can get away with substantially fewer in dialects. In most simple interpreters, you use the types that the underlying interpreter language already supports. To understand the basics of Ether, you really only need to think about 4 types:
- Lists - A sequence of objects
- Dictionaries - A mapping from keys to values like a dictionary maps words to definitions
- Functions - A reference to code that can be executed
- Strings - Sequences of bytes often representing text
If you’re already familiar with a programming language, you are probably familiar with how to use these types in any language that you use regularly. For people new to programming, these are fundamental aspects of any modern language.
Processes
Ether processes are very simple. They are dictionaries with 3 required key-pairs:
- “values”: A value list
- “instructions”: An instructions list
- “macros”: A dictionary with arbitrary keys and list values
Ether process evaluation essentially consumes objects one at a time from the instructions stack. If they are defined as macro lists in the macros dictionary, the contents of the macro are all pushed onto the instructions stack. If the current instruction is not a macro and is a function, it is called with the process as an argument. If the current instruction is neither a macro nor a function, it is pushed onto the value stack. This continues until there are no instructions left.
from typing import Any
def evaluate(process: dict[str, Any]):
while process['instructions']:
cur = process['instructions'].pop()
if cur in process['macros']:
process['instructions'].extend(process['macros'][cur])
elif callable(cur):
cur(process)
else:
process['values'].append(cur)
INSTRUCTIONS = "hello print".split()
INSTRUCTIONS.reverse()
MACROS = {'print': [lambda p: print(p['values'].pop())]}
evaluate({'values':[], 'instructions': INSTRUCTIONS, 'macros': MACROS})
A useful Ether interpreter would need more functions in macros to bootstrap a full language, but not many. The important part is that the functions are simple and self contained so they don’t bring complexity to the language model itself. Even the addition of concurrency and robust exception handling isn’t much more code than this.
Tokenization
Even for experienced programmers, writing out the rules for parsing source code into an abstract syntax tree, which itself is a complex concept, would be difficult. Why don’t block comments nest but other block elements do? When do you actually need a semicolon in Javascript? Can you mix tabs and spaces in Python? There’s a huge amount of baseline complexity in most languages just to tokenize the source code.
The Ether language is essentially tokenizer agnostic but the default is normally to do whitespace delimited tokenization. Virtually every modern language trivially supports doing that, and it is extremely easy to precisely define, even to non-programmers. This dramatically reduces the complexity for fully understanding the language. Ether also does not have a notion of an abstract syntax tree. There are simply values on the instructions stack.
Practical Ether
Much has changed since I first implemented the earliest variant of Ether in 2008 and later released full Ether in 2012, but one thing hasn’t changed: a new programming language is almost never the solution to a real world implementation problem. That’s still true today so even as Ether’s creator, I would caution people that there are really only two main use cases for Ether:
- Recreational and educational use by both novices and experienced engineers
- Real world use as an embedded interpreter language
After learning more than a dozen languages, I have found that it’s less about the language and more about ecosystem, community, and adaptability. Language purists love to hate Python and Javascript, but both have been tremendously successful. I doubt anyone would say Python is ideal, and the evidence of baggage it has picked up over the last 35 years is abundant. Still, it’s hard to argue with the thriving community it has today, especially around AI. A language designed to be a “better” Python would almost certainly fail because it wouldn’t convert the Python community or migrate the massive ecosystem of tools that makes Python so useful.
Full Ether
11 Object Types
Ether is not object oriented at its core. It is more akin to a functional programming language and everything is built on only 11 object types. They typically come in pairs and two general categories: common objects and unique objects. These form the entirety of the Ether object model.
- Common - Data structures and primitive types
- Lists - Node-based sequences
- Tuple - Immutable
- Stack - Mutable
- Tables - Key/value containers
- Trees - Immutable
- Arrays - Mutable
- Blocks - Raw byte storage
- ROM - Read-only bytes
- RAM - Read-write bytes
- Lists - Node-based sequences
- Unique - Communication and Processing
- Sink - A queue sink for sending messages
- Source - A queue source for receiving messages
- Functions: The syscalls of the Ether language where IO and object manipulation occurs
Probably the most important thing to note is that lists in Ether are conceptually stacks. They are singly linked lists not array-backed data structures that support fast index based retrieval and assignment. This is important because all Ether objects are cloneable (shallow copy) in constant time except RAM.
The other important thing to note is that the immutable common types may only contain other immutable common types or unique types. This is important for concurrency and constant-time garbage collection on “clean” processes.
Coming Soon…
In the coming months, I’ll be resurrecting the C reference implementation and the tutorial. These are useful if you want to explore some of the finer and more difficult to implement parts of Ether like constant time garbage collection on clean processes and lock free concurrency.