
Design Principles for Enzo
In my last post, I introduced Enzo, a programming language I designed and implemented as both a learning tool and a piece of creative expression. It’s an art project, not a commercial product, a sketch of my personal perspective on what code could feel like. Its design wasn’t guided by formal UX research, but by a set of personal heuristics and intuition.
This article explains the principles behind Enzo’s syntax and contrasts them with mainstream languages. It’s written for a technical audience but aims to stay accessible. If you want reference material, skim the syntax spec README or the short interactive Colab. Neither is required, the focus here is the why: the design decisions, the heuristics behind them, and how they compare to mainstream languages.
Design principles/heuristics
- Personal Intuition as the Primary Driver: Enzo is an art project. I didn’t run user studies or benchmarks; I followed what felt clear, interesting, and readable to me. Its design reflects my personal taste and curiosity.
- Favor Non-Programmer Intuition: When a choice exists between a computer science convention and a pattern a layperson would find natural, prefer the latter. Would grandma understand it?
- Prioritize the Human Interface over Machine Performance: When forced to choose, I favor making the language predictable, safe, and simple for the human over maximizing computational efficiency. Enzo is biased toward clarity and ease of understanding (as understood by principles 1 and 2), even if it means doing extra work under the hood.
- Reinforce Generalizable Patterns: Unify disparate language concepts under a single, recurring pattern to reduce the surface area of the language to learn, and reinforce a cohesive mental model through repetition and parallel structure.
- No Overloading (of Symbols or Concepts): A symbol, keyword, or syntactic structure should have one conceptual job, rather than giving multiple meanings to the same symbol (which many languages do out of historical accident or for brevity).
- Explicit Over Implicit: The programmer’s intent should be clearly stated in the code; avoid “magic” or invisible behavior. This is supported by the famous “Knowledge in the World versus in the Head” heuristic articulated by Don Norman in “The Design of Everyday Things”.
- Favor Composition Over Inheritance: Build from small pieces that snap together (like LEGO bricks) instead of deep hierarchies. This harkens back to the Unix philosophy.
- Decouple Syntax from Formatting: People are going to have differences in taste in terms of presentation, so the parser shouldn’t depend on strict whitespace or formatting style; be strict where needed, loose everywhere else.
- Syntactic Clarity over Annotation: Let structure carry meaning so the language can infer, not litter code with boilerplate.
With these principles in mind, let’s build the language up from its most basic components.
Names and Values
Enzo is built around a simple, repeating rhythm. This is the most important pattern in the whole language: you have little chunks of data-values, which I call “atoms” (like a number or a piece of text) and you can bind them to keynames. The unit of an atom bound to a keyname is a variable.
// A Number atom is bound to a keyname
$the-answer: 42;
// A Text atom is bound to a keyname
$greeting: "hello, world";You’ll notice two things right away. First, all variable keynames start with a $ sigil. This is a personal, aesthetic choice for clarity, driven by the Explicit Over Implicit #6 heuristic. When I scan code, the $ is a visual flag that says, "this is a piece of data." I understand some people consider this noisy, but PHP/Wordpress cooked my brain.
Second, and more importantly, is the colon :. In many languages, you'd see an equals sign =, but that symbol is often overloaded. In Enzo, the colon has only one job: to declare a name and bind a value to it. This is a direct application of the No Overloading #5 principle.
This commitment to a uniform pattern becomes clearer when we look at more complex data. In JavaScript, the syntax for creating a variable is different from the syntax for creating a property inside an object:
// JavaScript: different syntax for similar ideas
var name = "Sam"; // uses '='
var person = { name: "Todd" }; // uses ':'This always seemed weird to me. In both cases, you are binding a name to a value. They’re both keyname-value pairs. You’re taking a value and assigning it to name. Shouldn’t they be the same? There are historical reasons for this distinction, and there are underlying differences in implementation, but in Enzo, the exact same syntax is used in both cases, because in Enzo they are the same conceptually, which Reinforces a Generalizable Pattern #4.
// Enzo: one consistent pattern
$name: "Sam";
$person: [
$name: "Todd",
$age: 50
];In this Enzo snippet, $person is a list (enclosed in [...]) containing two name-value pairs. We didn’t have to switch to a different syntax inside the brackets. This consistency reinforces a cohesive mental model: a binding is a binding, whether it’s at the top level or nested inside a structure.
This syntactic uniformity also makes the language more Composable #7. By erasing the distinction between “object property” and “variable” it creates a consistent nestable syntactic unit. Enzo is heavily inspired by LISP’s homoiconicity. It’s not technically homoiconic because it lacks a true one-to-one relationship with the AST, and there are additional control flow structures that glue these atoms together, but there is that same focus on consistent nestable composability.
Lists: One Structure to Rule Them All
Once we have named values, the next logical step is to structure them. In Enzo, there is only one way to do this: the List, previewed above.
// A List is a collection of values in square brackets
$my-list: [
10, //A simple Number value
"a piece of text", // a simple Text value
$age: 30 // a named Number value inside the list
];Many languages make you choose upfront: is this an array (a numbered list) or a map (a named collection)?
Inspired by the language Lua’s Table, Enzo's List is both simultaneously.
In most languages, the split exists for performance reasons. Arrays are fast for number-based access because they store elements as contiguous memory blocks. Maps (or objects) are optimized for name-based lookups via hashing. Even Lua, conceptually unified at the surface, separates these structures under the hood.
Enzo does not. Every element, named or unnamed, lives in the same structure. That means named values also have numerical indexes (unlike Lua). Following Prioritize the Human Interface over Machine Performance #3, I’d rather have one simple tool for the user than two optimized ones for the computer.
The pattern for accessing elements is also unified. In most languages, the syntax splits:
array[0]; // this accesses the 1st slot of an array by an index
numberobject.color; // this accesses the field named "color"but in Enzo there is a unified dot notation access:
$my-list.1; // returns 10
$my-list.age; // returns 30By using one structure and one access pattern, Enzo Reinforces a Generalizable Pattern #4.
And yes, in the spirit of Favoring Non-Programmer Intuition #2, we start counting at 1. Most programming languages use 0-based indexing because it maps directly to how computer memory is addressed (the first item is at an offset of 0). But humans don’t count from zero. “The first item” is item number one.
For those wondering: How do you access a dynamically computed indices or properties? Enzo uses Function atoms, the unified value type for any evaluable expression.
Functions are also values
Inspired by LISP’s philosophy of “code as data”, one of Enzo’s core ideas is that a block of code is just another kind of value.
What if a block of code was just another type of value, like a Number or a List? In Enzo, it is. The parentheses () delimit Function atoms, little self-contained sets of operations on data, as data:
// This isn't just a calculation, it's a value that REPRESENTS the action of adding 2 and 2.
(2 + 2)Because code blocks are values, we can name them using the same keyname: atom pattern from Part 1. Technically all functions in Enzo are anonymous. Some just get bound to keynames.
// This follows the exact same pattern as `$greeting: "hello"`
add-two: (
param $number: 0;
return($number + 2);
);
// Note: Function keynames can omit the `$` sigil for readability purposes. But you can also invoke using the `$` even if you didn't declare it that way. So `$add-two()`, and `add-two()` are both valid. Even `$add-two` is valid, allowing the Function to recede into the background as just another value.In Enzo, the entire definition, including the parameter declarations inside, is the value. Everything is wrapped up in one little parenthetical “sushi bite.” This deep consistency has several powerful consequences:
- It Replaces {}: It’s used everywhere a code block is needed, including in control flow structures, ensuring consistent scoping rules, an example of Reinforcing a Generalizable Pattern #4.
- It Replaces Grouping Parentheses: In
(2 + 2) * 10, the(2 + 2)part is not a special “grouping” syntax as in most languages; it’s a code block that gets evaluated, and its result is then used in the multiplication, another example of Reinforcing a Generalizable Pattern #4. - It Enables Inferred Static Typing: Because every core data type has an unambiguous delimiter (
“”,[],()), Enzo can infer static types, leveraging Syntactic Clarity over Annotation #9. - It Allows for Default Arguments to Parameters: Because parameters are moved inside the function body, you can declare function parameters with default values. This lets functions behave more like regular data; if no parameters are needed, no parentheses are required.
For the computed access of lists mentioned above? Enzo handles computed access by requiring the expression in parentheses. For example, $my-list.($i + 1) would evaluate the expression $i + 1 and use that as the index. This again leverages the idea that parentheses in Enzo always represent an evaluable code block, here used to compute an index.
This is a stark contrast to JavaScript, which has at least six different ways to declare a function, and where the name, parameters, and body are often separate syntactic parts:
// JavaScript: traditional function syntax
function addTwo(number) { return number + 2;}In JavaScript, the function’s name (addTwo), its parameters (number), and its body { return number + 2; } are three different parts of the syntax. They’re separated, and the function is not an expression, unless you use one of the alternative syntaxes like function expression or arrow function. In Enzo, there is just one powerful flexible way, which mirrors the structure of other keyname-atom pairs.
Mutation and References
Enzo is very deliberate about how data is changed. In many languages, assigning a value to a variable and later changing that variable use the same operator (typically =).
let score = 10; // initial declaration
score = 15; // reassignment (same operator '=')count = 1
count = count + 1 # (uses '=' for update as well)Enzo splits these into two distinct operations with different syntax, following No Overloading (#5) and Explicit Over Implicit (#6):
$score: 100; // Creation with ':'
$score <: 101; // Mutation is explicit and visually distinct
102 :> $score; // You can even bind in the other direction (inspired by R)Another choice: all rebindings are passed by value, not by reference.
$original: [1, 2, 3];
$copy: $original; // this makes a copy of the list
$copy.1 <: 99; // change the first element of the copy
$original.1; // still 1 -- $original is unchangedScripting languages like JavaScript or Python apply this default behavior inconsistently. For instance, JavaScript copies primitive values like numbers or strings, but does not copy objects/arrays by value:
let x = 5;
let y = x;
y = 6;
console.log(x); // 5 (primitives are copied by value)
let arr1 = [10];
let arr2 = arr1;arr2[0] = 99;
console.log(arr1[0]); // 99 (objects/arrays assigned by reference)Other languages do this for efficiency: copying large objects is memory-intensive. Enzo prefers predictability. Everything copies by value, Prioritizing the Human Interface over Machine Performance #3.
What if you do want to pass by reference?
Enter the @ sigil, Enzo’s single, explicit token for reference semantics, following Explicit Over Implicit #6. It mirrors the $ pattern, Reinforcing a Generalizable Pattern #4.
Not only the @ generalizing the sigil pattern, the @ is generalized across several domains:
Referencing Variables: $copy: $original; makes a copy, but $ref: @original; creates an alias. If you mutate through one, the other sees it.
$original: [10];
$copy: $original;
$ref: @original;
$copy.1 <: 99;
$original.1; // still 10, because $copy was a separate copy
$ref.1 <: 99;
$original.1; // now 99, because $ref was referencing $originalPassing Variable Arguments by Reference: By default, function arguments are passed by value (copies), which prevents accidental side-effects on the caller’s data. But if you want to modify something in-place, you can pass a reference using @. For example:
increment: (
param $num: 0;
$num <: $num + 1;
);
$val: 5;
increment(@val);
// After the call, $val is now 6, because the function modified it by reference.In this example, increment would normally just update its local $num, and the change wouldn’t affect anything outside. But by calling increment(@val), we explicitly said "no, use $val itself," so inside the function $num refers to the same storage as $val. Thus, $val gets updated.
Higher-Order Functions (Passing Functions as Values): A higher-order function is any function that either takes another function as an argument or returns one as its result. This pattern is useful when you want to abstract behavior: instead of writing similar loops or transformations over and over, you can write one function that accepts another to decide what to do with each piece of data.
function double(x) {
return x * 2;
}
function applyTwice(func, value) {
return func(func(value));
}
applyTwice(double, 5); // → 20Enzo works the same way conceptually, but it makes the intent more explicit. To pass a function itself (rather than invoking it and passing that value as a copy), you prefix it with @, the same sigil used for all reference semantics:
double: (param $x: 0; $x * 2);
apply-twice: (
param $func: (); // expects a function
param $value: 0;
return(func(func($value)));
);
apply-twice(@double, 5); // -> 20Without the @, Enzo would try to call double before passing it, which isn’t what we want. Using the explicit general @ also means functions can omit parentheses when no arguments are needed, letting them behave like any other value, a small example of Decoupling Syntax from Formatting #9.
Loop Variables by Reference: By default, Enzo loops give you copies, protecting the original data. To mutate in place, declare the loop variable with @:
$nums: [1, 2, 3];
Loop for @item in $nums, (
$item <: $item * 10;
);
// $nums is now [10, 20, 30]If we had written Loop for $item in $nums, ... without the @, then $item would just be a copy and updating it wouldn’t affect the original list.
Destructuring by Reference: Normally, destructuring copies elements:
$nums: [1, 2, 3];
$a, $b, $c: $nums[];
$a <: 99;
// $nums is still [1, 2, 3]Mark them with @ to alias the originals:
$nums: [1, 2, 3];
$a, $b, $c: @nums[];
$a <: 99;
// $nums is now [99, 2, 3]This makes element-level mutation explicit and safe. You can see the direction of data flow immediately, and keeps rebinding semantics uniform across variable assignment, loops, and unpacking.
Now that we understand how Enzo handles mutations and references explicitly, let’s look at how it directs the flow of execution.
Directing the Flow
In Enzo, control flow structures like If/Else, Loop, and the then/$this pipeline operator are the glue that sticks the Function atoms together and directs their flow.
Conditional flow
Controlling which blocks run is done with plain English1 rather than abstract symbols, Favoring Non-Programmer Intuition #2. The Enzo If construct reads almost like an English sentence:
If $score is greater than 90 and $user.isActive, (
"Excellent!";
);
Else if $score is greater than 75, (
"Pretty good.";
);
Else (
"Keep trying.";
);Notice that the conditional statement isn’t enclosed in parentheses, another case of No Overloading #5. Parentheses mean one thing, and one thing only.
Enzo’s multi-branch check (like a switch statement) reuses the same structure, avoiding a separate case syntax:
If $fruit is "apple", (
"Crunchy snack";
),
or is "orange", (
"Juicy citrus";
),
or is "banana", (
"Yellow goodness";
),
Otherwise, (
"Unknown fruit";
);Pipeline flow
Many modern languages let you “chain” methods together to process data in sequence:
userList.filter(isActive).sortBy("lastName").select(["id", "email"]);It’s concise, but the dot (.) pulls double duty: sometimes “access a property,” sometimes “chain a call,” sometimes both. The actual data flow is implicit, each step must return an object exposing the next method.
This implicit behavior hides the actual data flow: it looks like everything belongs to the same object, even when it doesn’t. It also requires the returned implicit objects have the necessary methods, which ties up things in a more complex and fragile hierarchy. That violates Enzo’s No Overloading #5, Explicit Over Implicit #6, and Composition Over Inheritance #7 principles.
Other languages approach this problem differently. Functional languages and newer JavaScript proposals favor pipeline operators, which make the flow more composable:
users |> filter(%, "active") |> sortBy(%, "lastName") |> select(%, ["id", "email"]);users|> List.filter isActive|> List.sortBy lastName|> List.map selectColumnsEnzo has its own approach which is even more explicit and Favors Non-Programmer Intuition #2 with a plain English then and $this. Instead of implicitly passing a value as the first argument to the next function, you must use the special $this keyword to place it, which also gives greater flexibility with what can be done with that incoming piece of data :
// It works with simple anon Function atoms
100 then ($this + 1); // returns 101
10 then ($this + $this); // returns 20, this operation would be impossible with the implicit first argument pattern.
1 then ($this + 1) then ($this * 3) // returns 6
// It also works with named Function atoms.
$users
then filter($this, "active")
then sort-by($this, "last-name")
then select($this, ["id", "email"]);
// Exactly equivalent to, but more readable than a set of nested functions:
select(sort-by(filter($users, "active"),"lastName"), ["id", "email"]);Looping flow
Loops in Enzo are another place where a unified approach replaces several traditional constructs. Many languages have a variety of loop keywords: for, while, do...while, foreach, etc., each with its own syntax. Enzo has just one: Loop. The behavior of the loop is controlled by the phrase that follows the word “Loop”. This keeps the number of loop constructs low and the syntax uniform, Reinforcing Generalizable Patterns #4.
Here are the forms it can take:
Simple Loop: Loop, (...) repeats until told to stop:
$count: 0;
Loop, (
$count <: $count + 1;
If $count is greater than 5, (
end-loop;
);
"Looping... iteration <$count>";
);This will print “Looping…” messages and increment $count until $count becomes 6, then the If triggers an end-loop; which breaks out. Inside any loop, end-loop; acts like a “break” (exit the loop immediately) and restart-loop; acts like “continue” (jump to the next iteration early). I chose more descriptive keywords over the terse C-style break/continue , which makes their intention clear at the cost of a few extra characters, and aligns with Non-Programmer Intuition #2.
While Loop: Loop while <condition>, (...) This will check the condition before each iteration, and run the block only if the condition is true. For example:
$n: 1;
Loop while $n is less than 3, (
"n is <$n>";
$n <: $n + 1;
);Until Loop: Loop until <condition>, (...) is the opposite check, it loops until the condition becomes true (and then stops).
$x: 1;
Loop until $x is 5, (
"x is <$x>";
$x <: $x + 1;
);For-Each Loop: Loop for $item in $collection, (...) will iterate over each element in a List. Example:
$nums: [10, 20, 30];
Loop for $x in $nums, (
"Saw number <$x>";
// $x is a copy here, changes won't persist
);
Loop for @x in $nums, (
$x <: $x * 2;
);
// now $nums is [20, 40, 60] because we doubled each in placeAll these forms reuse the single Loop keyword. This keeps the number of loop constructs low and the syntax uniform. Contrast this to how other programming languages handle different kinds of loops: C and Java have while, do { } while, and the for(;;) loop, all with different syntax. Python has while and a for ... in ... but no direct equivalent of a C-style numeric for or an infinite loop (aside from while True). Rust has loop { } for infinite loops, plus while and a for ... in .... Each of these introduces another keyword or form to learn.
Additionally, the English-like phrasing of Loop while ... or Loop for ... is pretty approachable. Someone could probably guess what Loop until $done, (...) means without knowing much programming.
Interpolating Data
You’ve seen <…> inside strings. That’s interpolation, one mechanism that replaces a pile of separate features in other languages.
In other languages, you might have to learn and use different syntax for different kinds of combination or insertion:
- String concatenation: e.g., in JavaScript you might do
"Hello, " + name + "!". - Merging two arrays: e.g., JavaScript
[...arr1, ...arr2]spread syntax orarr1.concat(arr2). Python uses the+operator (list1 + list2) or extend. - Appending or prepending an element to a list: Most languages have special methods or syntax (like
arr.push(item)orarr.unshift(item)in JS,list.append(item)in Python, etc.). - Inserting a value into a string or an array at a specific point: often requires either splitting or manual index operations.
Enzo tries to cover a lot of this ground with one general mechanism, Reinforcing Generalizable Patterns #4.
Whenever you have text in Enzo, you can interpolate expressions inside < and >. For example:
$first-name: "Ava";
$last-name: "McCabe";
"Hello, <$first-name> <$last-name>!"; // -> "Hello, Ava McCabe!"
"Sum: <2 + 3>"; // -> "Sum: 5"You can put any expression inside the brackets, not just a variable. This is akin to template strings in other languages, but with its own Enzo flavor.
You can also interpolate Lists which allows you to merge, append, and prepend:
$list-1: [1, 2];
$list-2: [3, 4];
$list-3: [<$list-1>, <$list-2>]; // merged List of [1, 2, 3, 4]
$list-3 <: [0, <$list-3>]; // prepended List of [0, 1, 2, 3, 4]
$list-3 <: [<$list-3>, 5]; // appended List of [0, 1, 2, 3, 4, 5]One simple pattern serves multiple needs, which reinforces the concepts while keeping the syntax simple.
Custom Data Structures with Blueprints
The last major piece of Enzo’s syntax design to discuss is how you define your own custom data structures. Lists can represent almost anything in Enzo, but sometimes you want to define boundaries, “this kind of thing always looks like this,” and “these are the only valid forms it can take.”
That’s the point of structured types, the foundation of what most programming languages call object orientation.
Structured types are what let you describe repeatable shapes of data, patterns you can instantiate many times without rewriting the same fields and defaults. They give your program a semantic grammar for talking about the world it models. A Goblin or User or Invoice becomes more than just a loose bundle of keys and values; it becomes a kind of thing the language understands.
At their core, structured types solve three practical problems:
- Consistency and reuse: You can create many instances of the same conceptual shape without duplicating field definitions or forgetting defaults.
- Semantic clarity: A type name like
InvoiceorUsercommunicates intent far better than a raw data literal. The structure itself becomes self-documenting. - Constrained variation: Real systems have categories and subtypes. You want to model, for example, that an
Enemymight be aGoblinor anOrc, but not a mixture of both. Structured types let you define those distinctions formally.
Product and Sum types
Most languages describe data using two basic patterns:
- Product types combine information, an and relationship. For example, a
Goblinmight have bothhpandstatus-effect. These are what object-oriented languages express as classes or structs. - Sum types describe alternatives, an or relationship. An
Status-Effectmight beNormal, orPoisoned, orBleeding. These are often called enums, variants, or union types, depending on the language.
Object-oriented languages usually separate these patterns. A class handles the product side (fields combined together), and an enum handles the sum side (choices between options). You often have to glue the two together by hand.
Languages like Java institutionalized this pattern through classes and enums. You’d write one construct for the structure and another for the variants, and then connect them manually:
public enum StatusEffect { NORMAL, POISONED, BLEEDING; }
public class Goblin {
int hp = 100;
boolean hasDagger = true;
StatusEffect status = StatusEffect.NORMAL;
}
public class Orc {
int hp = 200;
int rage = 50;
StatusEffect status = StatusEffect.NORMAL;
}That works pretty well but what if you want a “sum-of-products”, that is an enum where each item also carries its own structured data (product type)?
In that case you have to declare both and then glue them together with switches and casts:
// an enum to group the Goblin and Orc together
public enum EnemyType { GOBLIN, ORC; }
// But the GOBLIN enum isn't connected to the Goblin class, so you have to do this:
public void printEnemyDetails(EnemyType type, Object data) {
switch (type) {
case GOBLIN:
Goblin goblin = (Goblin) data; // unchecked cast
System.out.println("A Goblin with " + goblin.hp + " HP.");
break;
case ORC:
Orc orc = (Orc) data;
System.out.println("An Orc with " + orc.hp + " HP and " + orc.rage + " rage.");
break;
}
}The pain points here are clear:
- Boilerplate: You have multiple declarations for what is conceptually one idea (“an enemy”).
- Manual Connection: The language doesn’t connect the choice and the data for you.
- Lack of Safety: The (
Goblin) data cast is not checked by the compiler. It’s a promise from you, the programmer, that can easily be broken, leading to runtime crashes.
Rust unifies those ideas under algebraic data types. A struct defines a product type, and an enum defines a sum type, both using the same lightweight syntax:
enum StatusEffect { Normal, Poisoned, Bleeding }
struct Goblin { hp: i32, has_dagger: bool, status: StatusEffect }
struct Orc { hp: i32, rage: i32, status: StatusEffect }
enum Enemy { Goblin(Goblin), Orc(Orc) }It’s cleaner and safer than Java, but you still have two constructs with different keywords.
Enzo’s Blueprint system is heavily inspired by Rust, and unifies them under a Generalizable Patterns #4 echoing its established keyname: atom rhythm, expressing their relationships through Syntactic Clarity over Annotation #9:
// Sum type for state
Status-Effect variants: Normal or Poisoned or Bleeding;
// Product types with a sum-typed field
Goblin: <[
hp: 100,
has-dagger: "true",
status: Status-Effect
]>;
Orc: <[
hp: 200,
rage: 50,
status: Status-Effect
]>;
// Sum type for which enemy
Enemy variants: Goblin or Orc;Here, the “which kind” and the “what it contains” live in the same pattern. Each variant carries its own structured data, and each structure can contain variants of its own, without extra glue code.
Composing Types
Traditional object-oriented design adds another layer: inheritance. It says, “this new type is like that old one, plus or minus a few differences.” That can be powerful, but deep hierarchies often create brittle dependencies:
class Creature {
int hp;
Creature(int hp) { this.hp = hp; }
}
class Humanoid extends Creature {
boolean usesTools = true;
Humanoid(int hp) { super(hp); }
}
class NightCreature extends Creature {
boolean darkVision = true;
NightCreature(int hp) { super(hp); }
}
// Goblin should be BOTH a Humanoid (uses tools) AND a NightCreature (dark vision)...
// but Java only allows one parent class.
class Goblin extends Humanoid { // can't also extend NightCreature
boolean small = true;
Goblin() { super(100); }
}
// Workaround: invent a combo base to simulate multiple inheritance
class NightHumanoid extends Creature {
boolean usesTools = true;
boolean darkVision = true;
NightHumanoid(int hp) { super(hp); }
}
class Orc extends NightHumanoid {
int rage = 50;
Orc() { super(200); }
}
// Every new combination (DayHumanoid, FlyingNightBeast, UndeadNightHumanoid...) needs its own class.
// This is the "class explosion" problem.Each new subclass copies behavior or overrides fields, and the tree of relationships quickly becomes hard to follow.
Enzo Favors Composition Over Inheritance #7, focusing on simple, reusable pieces you can snap together, :
Humanoid: <[ tool-use: "true" ]>;
Creature: <[ hp: 100 ]>;
Night-Creature: <[ dark-vision: "true" ]>;
Flying-Creature: <[ wings: "true" ]>;
// A Goblin is composed of simple, reusable pieces
Goblin: Humanoid and Creature and Night-Creature;
// and these parts can be reused to build other Blueprints
Bat: Creature and Night-Creature and Flying-Creature;Composition keeps structures shallow and flexible. Blueprints give you the expressive power of classes, inheritance, and enums, but without the fragmentation. They unify data definition and composition under one consistent, human-readable pattern. No matter what you’re building, you’re always describing structured relationships.
Enzo does include mechanisms for inheritance where it’s genuinely needed, but this article is already long enough.
Next Time
If you’ve found these ideas interesting, stay tuned for the next article. I’ll shift from language design to implementation, how I actually built Enzo from scratch.
This piece covered only a subset of Enzo’s features. For the full syntax and examples, see the syntax spec README, or explore the interactive Colab notebook to try it yourself.
End Notes
- I did not know about the Plain Language Programming language when I designed Enzo but a friend sent it to me and it ends up looking a lot like Enzo! ↩︎