β€’

tech

Declarative, compile-time and runtime

How is that a declarative API is safer, more composable, and performant? It all has to do with types, API design, and the magic of the master behind the scenes, also called a runtime.


Sandro Maglione

Sandro Maglione

Software

A few concepts that I learned late:

  • Functional programming
  • Declarative API
  • Difference between at "compile-time" and at "runtime"

Lately I am diving deeper into API design, what makes an API declarative, and compile-time type safety πŸ€”

It's not obvious, and worth your attention πŸ‘‡


The first time I heard "runtime"

In all the exams I did at university, one was without doubts the most brutal: Formal Languages and Compilers 🀯

We learned all the data structures and formal methods behind compilers, as well as how to implement our own (more or less) πŸ™Œ

It was there that I first heard the terms "compile-time" and "runtime". And at the time I had no idea what they meant πŸ’πŸΌβ€β™‚οΈ

Here is what you need to know for your day-to-day:

  • Compile-time: everything that happens before executing the code
  • Runtime: everything that happens after/while the code is running

Why and where is this relevant? πŸ€”

Correctness and type safety

The main reason behind types is to check correctness before running the code.

In practice: convert as many bugs as possible from "runtime" to "compile-time" 🀝

The more assumptions can be "captured" by types, the more mathematical confidence you have that your app has as few bugs as possible.

Therefore, strive to make your code as type safe (compile-time) as possible.

But, the code still needs to run. A runtime is needed 🫑

Declarative, less bugs

Another core distinction is "imperative" vs "declarative":

  • Imperative: implement "how" to do something line by line
  • Declarative: define "what" to do, and let someone else figure out the "how"

How is this relevant to correctness and type safety? πŸ€”

Declarative is generally safer, more composable, and (possibly) even more performant πŸš€

Imagine having to implement the sum of all the elements in a list. You may come up with any sort of for loop:

function sum(numbers) {
  let sum = 0;
  for (let i = 0; i < numbers.length; i++) {
    sum += numbers[i];
  }
  return sum;
}

Here you are manually coding each step (imperative). It's like ordering the machine what to do, sort of "micromanaging".

Declarative instead is more like:

function sumList(numbers) {
  return numbers.reduce((sum, n) => sum + n, 0);
}

You can read this as "reduce numbers to their sum (+) starting from 0". You describe the intention, not how to do it.

Why a runtime

Declarative is good, you just declare what you want and you get it.

But who is then actually doing the work you request? A runtime ✨

In the reduce example above, someone behind the curtains implemented a (working) for loop for you.

Calling reduce captures your "intent", and you only provide enough information for the runtime to do its job πŸ—οΈ

Let's come back to the advantages:

  • Safer: a well typed declarative API uses types to block you from using the function improperly. You don't care how it's implemented internally, it just works
  • Composable: you can chain "intents" (do this, then this, then this) instead of hard-coding an imperative mess
  • Performant: library authors may come up with all sorts of optimizations behind the scenes, all hidden behind a simple API

How to make a declarative API

A declarative API requires you to store enough information for the runtime to understand the user intent.

Enough information, but nothing more than that πŸ™Œ

In practice, you create an interface that stores a bunch of "options":

export interface Command {
  readonly [CommandTypeId]: CommandTypeId;
  readonly _op:
    | "SPAWN"
    | "SPAWN_BATCH"
    | "INSERT_RESOURCE"
    | "REMOVE_RESOURCE"
    | "REGISTER_SYSTEM"
    | "UNREGISTER_SYSTEM";
}

A user can call all sorts of APIs, but the runtime only needs to know what operations was requested (_op), as well as some other metadata if necessary.

When you then execute a Command, the runtime takes charge, unpacks it all, and makes it happen πŸͺ„


Last week's effect + game dev experiment wasn't so successful, but, it opened the door to a new experiment for @typeonce/ecs (work in progress) πŸ”œ

Keep aiming for type safe game dev 🫑

See you next πŸ‘‹

Start here.

Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights

Not convinced? Well, let me tell you more about it