Will ChenWill Chen
← Writingsystem design

Semantic blocks

English is loosely structured because it comes from speech. Text is a medium where you take in more at once, so I sketched the language in between.

Will ChenWill Chen5 min

Motivation

English is shaped by the fact that we invented it for talking, and the two channels have almost nothing in common:

speechthe page
deliveryone word at a time, in real timea whole paragraph at once
directionforward onlyglance back, skim forward at will
where structure is heldthe listener's working memoryin front of your eyes
decodingone pass, at the speed of productionas many passes as you want
so the grammar isloose, fuzzy and redundant, to survive being generated liveunconstrained by any of it

The left column is the grammar we have. The right column is where we use it. My notes call the page a 1.5-dimensional medium: more than the single line speech gives you, less than a full plane.

What would a notation fitted to the page carry that this one cannot?

The pidgin

First candidate: stop using English. A formal notation has the properties speech could not carry.

  • unambiguous
  • nests
  • a machine can read it without guessing

It also gives up the one thing English is uniquely good at, carrying a meaning nobody has finished defining yet, which was most of what I wanted to write down. So the move is to augment English rather than replace it: a vocabulary of semantic blocks for ideas that are less obvious in plain natural language, producing what my note calls an augmented English, a pidgin between English and a programming language.

Pidgin is the accurate word, and it names three properties:

  • it borrows from both languages
  • it is simpler than either
  • it exists for a job rather than for a culture

That rules out the two nearby things I did not want, a formal language with English-looking syntax and English with annotations bolted on.

No block is named, and the constraint that stopped it is real: a construct for the page has to be justified against a specific under-structured representation, and English is too large a target to aim at. The two cases below are where the same complaint has a surface small enough to design against, and they are where the concrete constructs are.

Load-bearing representations

If the complaint is about English specifically, it should not appear in representations with no English in them. It appears in two.

Money. Accounting software works in codified low-level objects: ledgers, transaction entries, amounts, dates. People think in a business expense, which is not a row in a table but, as I put it, a loose collection of related tasks, objects, entities and rules that hangs together in semantic space. The sketch is an API for Human Concepts, whose operations are themselves loosely defined, because that is how two people hand work to each other.

Types. TypeScript, taking the cue from Mojo, Modular's superset of Python. TypeScript's types are erased before runtime, which makes the transaction lopsided:

  • you pay a strict and verbose type checker, and fix everything it complains about
  • you get editor support
  • the compiled JavaScript carries none of the guarantee

The erasure is deliberate and defensible, because it is what lets TypeScript compile to JavaScript any runtime will accept and what made it adoptable at all. It is still the cost worth buying back.

Koala is the superset that does it, and it changes three things:

  • Types are enforced at runtime and introspectable before compilation, so logic can depend on them.
  • Decorators work as they do in Python, rewriting at the AST level rather than annotating.
  • Pattern matching happens at compile time against type expressions, with consequences in the logic rather than in the checker.

What that looks like is TypeScript with the sized types and sum types the erasure rules out:

function addTwoNumbers(a: Uint8, b: Uint8): Uint8 {
  return a.checkedAdd(b);
}

enum Result<T, E> {
  Ok  { res: T; },
  Err { err: E; }
}

function divideBy(a: Uint8, b: Uint8): Result<Uint8, String> {
  if (b === 0) {
    return Result.Err { err: "cannot divide by zero" };
  } else {
    return Result.Ok { ok: a / b };
  }
}

Every line there depends on types being real:

  • Uint8 has a width, so checkedAdd has something to check against
  • Result is a choice between two shapes rather than a union the compiler forgets, so a caller cannot read err off a value that carries res
  • erase the types and each of these becomes a comment

English, a ledger schema and a TypeScript type all carry less structure than the job in front of them, and in the third case the structure exists and has been deliberately disconnected from what it describes.

The common shape: a representation should be load-bearing. A structure that does not constrain, produce, or otherwise affect what actually happens is decoration, paid for in attention and returning nothing. Same premise as a compiler that makes the language carry the constraint rather than the documentation.

Interrogating a design in writing

The same question applies to a dependency instead of a notation, and it is cheap enough to run before writing code.

The candidate: editor extensions built with a custom React renderer. A renderer decides what the description of an interface actually turns into, so a custom one would let you describe an editor panel in React and have it come out as something the editor understood. Four steps, in writing, before the proof of concept:

  1. Name what the tool gives you. React gives JSX, reactivity and state.
  2. Name the domains where those pay off: markup like HTML or PDF, interfaces like React Native, and tree-shaped data such as a syntax tree.
  3. Check your own case against the list. Mine's strongest justification was rendering web views.
  4. See what removing the tool would cost. Web views can already use React's ordinary DOM renderer, so a custom one would not be necessary.

Step four kills it, and the test generalises: if removing the tool costs nothing, the tool was never load-bearing. The renderer was an interesting angle rather than a requirement.

What got built

Three artifacts:

  • the medium argument
  • a language proposal, with real syntax and three named changes to the type system
  • a subtraction test, four steps

All three are documents, and the systems language has a repository scaffold under the name chickenscript.

The gap between a design document and a running implementation has its own mechanism, and it is not effort:

noticing now that I've encountered the threshold of discomfort that exists between reading code — and feeling good about understanding it, delusional and not grounded in reality, and actually sitting down to write the code.

Reading code produces a feeling of understanding that writing code does not require you to have earned. A design document sits on the same side of that threshold as reading, which is why it can be complete and still constrain nothing.

Which is the load-bearing test pointed back at the documents themselves. The medium argument survives it, because it does not depend on any of the code: it constrains what any notation for the page has to carry before a line is written.