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 π