Selected snippets that demonstrate Lambda Prelude in practice. Each block is a self-contained, executable script. Save as a .lp file and run with the interpreter, or build to a native binary.
lambda-prelude run hello.lp
lambda-prelude build http-server.lp --out http-server
Smoke test
"Smoke test: simplest possible script."
5 factorial printNl.
Protocols replace inheritance
Protocol subclass: #Comparable
requires: (#<).
Comparable >> > other = other < self.
Comparable >> <= other = (self > other) not.
Comparable >> >= other = (self < other) not.
Comparable >> min: other = (self < other) ifTrue: [self] ifFalse: [other].
Comparable >> max: other = (self > other) ifTrue: [self] ifFalse: [other].
Object subclass: #Counter
fields: (value)
protocols: (Comparable).
Counter class >> of: v = Counter fields: { value: v }.
Counter >> value = value.
Counter >> < other = value < other value.
a := Counter of: 3.
b := Counter of: 5.
(a < b) printNl. "true"
(b > a) printNl. "true — derived from Comparable"
(a max: b) value printNl. "5"
The class implements only <. Everything else is derived from the protocol.
Ping-pong actors on OCaml 5 effects
"The actor surface starts with three communication primitives — spawn, send, receive — plus Actor self for the current fiber:
Actor self — current fiber's actor ref
Actor spawn: [:me | ..] — spawn a new actor
actor ! msg — fire-and-forget message send
actor receive — block until next message arrives"
parent := Actor self.
child := Actor spawn: [:me |
msg := me receive.
msg printNl.
parent ! #pong
].
child ! #ping.
parent receive printNl.
Stateful actor with typed dispatch
Object subclass: #Inc.
Object subclass: #GetCount fields: (reply).
Object subclass: #Counter.
GetCount >> reply = reply.
Counter class >> run: me count: n =
me receive: {
Inc -> [:msg | Counter run: me count: n + 1].
GetCount -> [:msg | msg reply ! n. Counter run: me count: n]
}.
parent := Actor self.
counter := Actor spawn: [:me | Counter run: me count: 0].
counter ! (Inc fields: {}).
counter ! (Inc fields: {}).
counter ! (Inc fields: {}).
counter ! (GetCount fields: { reply: parent }).
parent receive printNl. "3"
Counter holds no mutable state — n lives in the recursive Counter run: me count: n loop argument. The typed receive: table carries each message class through type inference so handler bodies see concrete types.
JSON
obj := JSON parse: '{"name": "Alice", "age": 30, "active": true}'.
(obj at: 'name') printNl.
(obj at: 'age') printNl.
nested := JSON parse: '{"user": {"id": 42, "tags": ["admin", "dev"]}}'.
((nested at: 'user') at: 'id') printNl.
((nested at: 'user') at: 'tags') size printNl.
d := Dict new.
d := d at: 'name' put: 'Bob'.
d := d at: 'age' put: 25.
(JSON serialize: d) printNl.
The standard Dict is a persistent HAMT. at:put: returns a new Dict; the original is unchanged.
HTTP server
module Main imports: (Http).
HttpServer
start: 8080
handler: [:req |
HttpResponse fields: {
status: 200,
headers: (Dict new at: 'Content-Type' put: 'text/plain'),
body: 'Hello from Lambda Prelude!\n'
}
].lambda-prelude build http-hello.lp --out hello-server
./hello-server
curl -i http://localhost:8080/
The HTTP server takes a block as the request handler. Each request runs on a fresh fiber under the actor scheduler; a slow upstream cannot pin other handlers.
SQLite
db := DB open: ':memory:'.
db exec: 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)'.
db exec: 'INSERT INTO users (name, age) VALUES (?, ?)'
params: (Array with: 'Alice' with: '30').
db exec: 'INSERT INTO users (name, age) VALUES (?, ?)'
params: (Array with: 'Bob' with: '25').
rows := db query: 'SELECT name, age FROM users ORDER BY age'.
((rows at: 1) at: 'name') printNl. "Bob"
((rows at: 1) at: 'age') printNl. "25"
db close.
SQLite is built in. PostgreSQL and MariaDB / MySQL are loaded as Dynlink plugins, under both the interpreter and the AOT binary.
JSON-RPC dispatch
module Main imports: (Http, JsonRpc).
Object subclass: #Demo.
Demo >> add: params = (params at: 1) + (params at: 2).
Demo >> subtract: params = (params at: 1) - (params at: 2).
Demo >> fail: params = Error raise: 'something broke'.
server := JsonRpcServer onService: (Demo fields: {}).
req := Dict from: {
jsonrpc: '2.0',
method: 'add',
params: (Array with: 7 with: 5),
id: 1
}.
(JSON serialize: (server dispatchOne: req)) printNl.
server dispatchOne: accepts single requests, batches, notifications, unknown methods, and handler exceptions; unknown methods and raised exceptions become -32603 envelopes. The HTTP wire round-trip is shipped as a separate example.
TERIOS-style envelope dispatch
module Main imports: (Http, Terios::Rpc).
Object subclass: #XA001Save.
XA001Save >> execute: requestDto =
(requestDto includesKey: 'name')
ifTrue: [Res ok: (Dict from: { id: 42, status: 'created' })]
ifFalse: [Res error: 'name is required'].
registry := Dict new.
registry := registry at: 'XA001Save' put: (XA001Save fields: {}).
dispatcher := ServiceDispatcher withRegistry: registry.
Business RPC carries serviceName, requestDto, responseDto, and errorInfoList envelopes. Services are small Command-pattern classes; the dispatcher normalises request / response shapes.
More
The full set of bundled examples covers:
- Language basics — arithmetic, blocks, recursion, logic, cascades
- Classes and protocols —
fields:, class methods, multi-protocol composition - Actors and supervision — selective receive, receive timeouts, futures, link, monitor, trap-exits, supervisor strategies
- I/O and networking — file I/O, JSON, stdin handling, TCP, TLS
- Services — HTTP server / client, SQLite, PostgreSQL, MariaDB / MySQL, Redis / Valkey, JSON-RPC, TERIOS-style envelope dispatch
- Distribution — multi-domain spawn, remote refs (
Remote at:for:id:), virtual actors (grains: single-node / cross-node / in-memory store) - Operational — reflection, subprocess, cache, connection pool