Idiomatic Karl for DevOps Tooling
Karl code is best when the dataflow is explicit and concise: process handles for external tooling, stream operators for transformation, and recover blocks only at failure boundaries.
1. Principles
- Prefer expression-first code over boilerplate wrapper functions.
- Use
runfor one-shot command wrappers andprocfor lifecycle-managed subprocesses. - Keep stream transforms in one pipeline before branching to tasks/channels.
- Recover only where fallback is meaningful (parsing/IO boundaries).
- Use object shorthand and local names to reduce visual noise.
2. Env + Config
let ns = trim(env("K8S_NS")) ? "default"
let pollMs = parseInt(trim(env("POLL_MS"))) ? 1000
let cfg = fromJson(readFile("config.json")) ? {
{ mode: "safe", retries: 3, }
}
3. Use run() for command wrappers
let st = run("kubectl", "get", "ns", { maxOutputBytes: 32768, }) ? {
fail("kubectl invocation failed: " + error.message)
}
if !st.ok { fail("kubectl exit " + str(st.code) + ": " + st.error) }
let names = st.output.split("\n").filter(_ != "")
log({ count: len(names), })
4. Use proc() for long-lived jobs
let p = proc(
"kubectl", "logs", "-f", "deploy/api",
{ stdout: PIPE, stderr: NULL, stdoutType: TEXT, }
)
for true with pair = p.stdout.read() {
let [line, eof] = pair
if eof { break () }
if line.contains("ERROR") { logt("error", line) }
pair = p.stdout.read()
} then ()
wait p ? {}
5. Stream-first transforms
let p = proc(
"kubectl", "get", "pods", "-A",
"-o", "jsonpath={range .items[*]}{.status.phase}{\"\\n\"}{end}",
{ stdout: PIPE, stderr: NULL, stdoutType: TEXT, }
)
let phases = p
| lines()
| filter(_ != "")
| group_count()
wait p
log(phases)
6. Live matrix loop (one-statement fan-in)
merge(
listPods()
.map(pod ->
proc(
"kubectl", "logs", "-f", "-n", ns, pod, "--all-containers=true", "--tail=100",
{ stdout: PIPE, stderr: NULL, stdoutType: TEXT, }
)
| lines()
| filter(_ != "")
| map(line -> { pod, level: levelOf(line), })
)
) | forEach(event -> {
bump(counts, event.pod, event.level)
if now() - last >= matrixMs {
render(counts)
last = now()
}
})
7. SQL boundaries
let db = sqlOpen(dsn) ? { fail("db open: " + error.message) }
let row = sqlQueryOne(db,
"SELECT id, email FROM users WHERE id = $1",
[userId]
) ? { fail("query failed: " + error.message) }
let result = match row {
case null -> { found: false, }
case r -> { found: true, id: r.id, email: r.email, }
}
sqlClose(db)
log(result)
8. Graceful shutdown pattern
let worker = spawn (() -> {
for true with running = true {
sleep(1000)
tick()
} then ()
})()
let sigs = signalWatch(["SIGINT", "SIGTERM"])
let [sig, closed] = sigs.recv()
if !closed { log("shutdown:", sig) }
worker.cancel()
wait worker ? {}
9. Conciseness rules
// good
let mkJSONHeaders = () -> map().set("Content-Type", "application/json")
let jsonResponse = (status, payload) -> { status: status, headers: mkJSONHeaders(), body: toJson(payload), }
// avoid redundant wrappers
// let msToRFC3339 = ms -> timeFormatRFC3339(ms)
10. Anti-patterns
- Recovering every expression instead of boundary failures.
- Using channels to bridge streams when direct stream consumption is enough.
- Adding generic helper layers before seeing real repetition.
- Wrapping simple values in extra blocks for no reason.
- Ignoring
wait pon long-lived process handles.