Design Notes

This section will have various articles on the design of Tenet.

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 systme. 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 the plethora of operators it offers. The library exists, though, because tHaskell solved the problem of mutation by pushing it back on the user. In fairness, this has always been

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: