The itch I initially wanted to scratch was frustration at various anemic type systems.
Especially in databases, SQL has a few atomic types and all other structure is expressed
through relations, thanks to a misunderstanding of normal forms. At the time, NoSQL was
entering the scene and gaining popularity by promising to rid developers of pesky schemas. It
looks very clean and simple, but when code written to handle an “Employee” can’t depend on it
having a “department” field of a specific type, it either crashes or, worse, silently ignores
the missing field. We never got rid of the schema, rather, we recreated it, poorly, in the
application.
Contrast with a language like Haskell which has a tremendously rich and powerful type system.
It’s a real joy and it’s fun bending your mind to really understand the functional paradigm and
all the different ideas that have been added to Haskell. One such case was the lens library
which may make you feel a bit overwhelmed at all the operators it offers. The library
exists, though, because Haskell solved the problem of mutation by pushing it back on the user.
In fairness, it has always been the spirit of Haskell to build language features in the
language itself.
The problem of how to share values is fairly ubiquitous, but it points towards a deeper issue:
how we establish a boundary between independent code units, and how that evolves over time.
The solution that seems likely is we need to be able to translate Tenet to other languages,
so that the other side of the boundary has consistent semantics.
I’d like Tenet to have a core feature set that’s fun and satisfying to code in. For instance, the
syntax tries to be very flexible, offering plenty of sugar to handle .
By the same token, many of the semantics are very clearly defined, so a string is just a string,
an integer is just an integer. The idea is that avoiding footguns and gotchas help the
basics of Tenet feel solid and reliable. It’s why I’ve avoided making it an expression based
language: if when is a keyword,
Error handling
Thinking about failure is hard: the possible failure scenarios tend to explode multiplicatively,
and trying to handle them tends to obscure the desired algorithm.
It’s also difficult to agree on what things are errors. If we’re writing a lookup table,
a key being missing seems to naturally be an error, unless we’re using it to guarantee elements
are unique, in which case being present indicates an error.
Even when it’s clear that a particular result ought to be some sort of error, error-handling
schemes typically require that we further categorize the error as part of a pre-existing hierarchy.
Error handling code is rarely executed, hard to test, and winds up being the obscure code
that users are wary of modifying.
Approach
Tenet’s approach is to view developers’ design of error handling as having two broad phases:
initial and refinement. Initial shouldn’t be taken to mean thoughtless or instinctive,
nor are all refinements necessarily beneficial. Rather, we’d like some clear doctrine that can
be understood during both phases, and is naturally supported by the language.
That doctrine asks two questions:
Should control ever reach this point?
Reworded: Should the system ever be in this state?
If so, what few words clearly describe this result?
Then, if the answer to the first question is “no,” then this is a failure and
the never statement will essentially crash the program.
The second question then treats the result as an ordinary value. Rather than trying to read
tea leaves to decide exactly what kind of error a value happens to be, a library developer
states what the value plainly is. Then the caller applies domain knowledge to decide exactly
what it represents and how to treat it.
The caller selects the expected result using the ! operator, and if the unexpected result is
transmitted, it escapes the expected control path. This is similar to how an exception is
thrown; if it’s not locally caught and the function declares that it can return the escaping value,
it can escape the function entirely.
So the separation of responsibilities is:
the library developer is responsible for clearly naming all possible results
the library caller describes the expected and exceptional paths of control
Zero-based indexing is more natural for programming languages
Users have been trained on zero-based indexing because it’s an industry standard, and
that’d be a safe decision.
But the objection seems fair: when you want ten things it’s weird to express
that as , let alone the half-open .
Let’s clarify “natural,” though: in language design, we’d like to use
conventions that line up with a user’s intuition. That is, if the user has some
assumptions or unconscious preference, then if the language conventions align with that,
the user will find the language “just works.”
It’d be great if we could test a large number of people and see whether, unprompted,
they tend towards zero-based or one-based.
As luck would have it, on Dec 31, 1999, people all over the world were celebrating
the new millennium. And it wasn’t the “odometer rolling over” from 1999 to 2000, it
was explicitly the millennium.
Our calendar uses the natural convention because the first year is 1 AD. The first
millennium is the years 1 through 1000, the second is 1001 through 2000, and the
third started on 2001.
Despite this, people widely believed that the next millennium would start on 2000,
or at least it never seemed off or odd to them, which is an intuition consistent
with 0-based indexing.
Let’s do another test. Constantinople fell in 1453, quick, what century was that?
Many people stumble with this. Let’s think it through: we know the first century
is 1 to 100, the second century is 101 to 200.
100 has “1” in it and 200 has the “2”, so the trick is to round up and truncate:
We’re getting some different results. It’s true that when counting small numbers,
people start at 1 and it feels odd to start at zero.
It’s also true that manipulating 1-based numberings by grouping them into something
like centuries is something people often stumble over. The “round up and truncate” is
really ceiling division, so it’s very obscure.
When the numbers are large, it seems like people use estimation strategies that
assume a zero base.
That reconciles the conflicting results: for small numbers, we have a preference
for 1-based counting, and for large or unknown numbers, our preference is
for 0-based estimating.
A programming language needs to pick one, and since we’re routinely dealing with large or
unknown numbers, we should go with 0-based indexing.