Design Notes

General motivation

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:

  1. Should control ever reach this point?
    • Reworded: Should the system ever be in this state?
  2. 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:

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 [0,9], let alone the half-open [0,10).

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:

   1453150015th century

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.