Skip to main content

S-Expressions: The JSON of Functional Programming (That Actually Makes Sense)

· 4 min read
Osamah Alghanmi
Co-Founder & Technical Lead

S-Expressions, JSON, and the Functional Renaissance

Why we chose Lisp-style S-expressions over JSON for logic definition, and why you might too.

Everyone loves JSON, but when you need logic, you end up with string templates or JavaScript. What if your data format was your logic format?

Entity(Matter)Page(Space)Trait(Energy)idleactivehas_traitrenderstransition
Orbital Unit = Entity + Traits + Pages

The JSON Limitation

JSON is great for data:

{
"name": "John",
"age": 30,
"hobbies": ["coding", "reading"]
}

But what about logic? You have a few options:

Option 1: String Templates

{
"condition": "user.age >= 18 && user.verified"
}
  • ❌ Error-prone (typos in strings)
  • ❌ No validation
  • ❌ Injection risk

Option 2: Custom DSL

{
"condition": {
"and": [
{ "gte": ["user.age", 18] },
{ "eq": ["user.verified", true] }
]
}
}
  • ✅ Structured
  • ❌ Verbose
  • ❌ Hard to read

Option 3: JavaScript Functions

const condition = (user) => user.age >= 18 && user.verified;
  • ✅ Readable
  • ❌ Not serializable
  • ❌ Security risk (eval)

Enter S-Expressions

S-expressions (symbolic expressions) have been around since 1958 with Lisp. They're simple:

(operator operand1 operand2 ...)

In JSON-friendly form:

["operator", "operand1", "operand2", ...]

S-Expressions in Almadar

Almadar uses S-expressions for guards and effects:

Guards: Conditional Logic

{
"from": "pending",
"to": "approved",
"event": "APPROVE",
"guard": ["and",
[">=", "@user.roleLevel", 5],
["not", "@entity.isFlagged"],
[">", "@entity.amount", 0]
]
}

This is equivalent to:

if (user.roleLevel >= 5 && !entity.isFlagged && entity.amount > 0) {
// Allow transition
}

But it's:

  • ✅ Serializable
  • ✅ Validatable
  • ✅ Secure (no eval)
  • ✅ Cross-platform

Effects: State Changes

{
"effects": [
["set", "@entity.status", "approved"],
["set", "@entity.approvedAt", "@now"],
["set", "@entity.approvedBy", "@user.id"],
["persist", "update", "Order", "@entity.id", "@entity"]
]
}

Each effect is an S-expression:

  • ["set", target, value] — Set a value
  • ["persist", operation, entity, id, data] — Save to database
  • ["emit", event, payload] — Emit an event

Why This Matters

1. Homoiconicity (Code as Data)

S-expressions are data that looks like code. This means:

["+", "@entity.count", 1]

Is both:

  • A data structure (array of strings)
  • Executable code (add 1 to count)

2. Composability

You can nest S-expressions arbitrarily:

["if",
["and",
[">", "@entity.score", 100],
["=", "@entity.status", "active"]
],
["emit", "ACHIEVEMENT_UNLOCKED", { "level": "gold" }],
["emit", "ACHIEVEMENT_PROGRESS", { "needed": ["-", 100, "@entity.score"] }]
]

3. Serialization

Because S-expressions are just arrays, they serialize perfectly:

// JavaScript
const guard = [">=", "@user.age", 18];
JSON.stringify(guard); // '[">=","@user.age",18]'
# Python
guard = [">=", "@user.age", 18]
json.dumps(guard) # '[">=","@user.age",18]'
// Rust
let guard = json!( [">=", "@user.age", 18] );
serde_json::to_string(&guard).unwrap();

The Binding Context

S-expressions in Almadar use special prefixes for context:

PrefixMeaningExample
@entity.fieldCurrent entity field"@entity.status"
@payload.fieldEvent payload"@payload.userId"
@stateCurrent state machine state name"@state" (e.g. "Browsing")
@user.fieldCurrent user"@user.id"
@nowCurrent timestamp"@now"

This creates a declarative binding system:

{
"guard": ["=", "@entity.ownerId", "@user.id"],
"effects": [
["set", "@entity.updatedAt", "@now"],
["set", "@entity.updatedBy", "@user.id"]
]
}

Real-World Analogy: Excel Formulas

If you've used Excel, you've used S-expressions:

=IF(AND(A1>100, B1="active"), "Gold", "Silver")

In Almadar:

["if",
["and", [">", "@entity.score", 100], ["=", "@entity.status", "active"]],
"Gold",
"Silver"
]

Excel formulas are S-expressions. They're:

  • Declarative (you say what, not how)
  • Composable (functions call functions)
  • Safe (no arbitrary code execution)

Standard Operators

Almadar's standard library includes:

Comparison

["=", "a", "b"]        // equality
["!=", "a", "b"] // not equal
[">", "a", "b"] // greater than
[">=", "a", "b"] // greater or equal

Logic

["and", "a", "b", "c"] // all must be true
["or", "a", "b", "c"] // at least one true
["not", "a"] // negation

Math

["+", "a", "b", "c"]   // sum
["-", "a", "b"] // difference
["*", "a", "b"] // product
["/", "a", "b"] // quotient

Array

["count", "@array"]    // array length
["contains", "@array", "item"] // membership
["filter", "@array", ["predicate"]]

String

["concat", "a", "b"]   // concatenate
["length", "str"] // string length
["matches", "str", "regex"]

Try It: Build a Guard

Let's create a guard for an approval workflow:

{
"from": "pending",
"to": "approved",
"event": "APPROVE",
"guard": ["and",
["or",
[">=", "@user.roleLevel", 5],
["=", "@user.id", "@entity.ownerId"]
],
["not", "@entity.isLocked"],
[">", "@entity.amount", 0],
["<", "@entity.amount", 10000]
]
}

This translates to:

if (
(user.roleLevel >= 5 || user.id === entity.ownerId) &&
!entity.isLocked &&
entity.amount > 0 &&
entity.amount < 10000
) {
// Allow approval
}

But with:

  • ✅ Declarative syntax
  • ✅ Automatic validation
  • ✅ No code injection risk
  • ✅ Serializable for audit logs

The Takeaway

S-expressions aren't just a Lisp curiosity — they're a practical solution to "how do we put logic in JSON?"

They give you:

  • The power of code (composability, expressiveness)
  • The safety of data (serialization, validation, no eval)
  • The clarity of Excel (declarative, readable)

Next time you're tempted to use eval() or string templates for dynamic logic, remember: there's a 60-year-old solution that actually works.

Want to explore more? Check out the standard library operators.

Recent Posts