Version: 1.0.0 Last Updated: 2025-11-13
HQL embraces maximum flexibility while maintaining a preferred style for consistency. Following Clojure's proven approach (15+ years in production), HQL accepts both Lisp-style and JavaScript-style syntax without forcing one over the other.
Core Principle: "Be opinionated where it matters (correctness), flexible where it doesn't (style)."
Use this for new HQL code:
// Objects/Maps - unquoted keys, no commas
{name: "Alice" age: 25 city: "NYC"}
// Arrays - no commas
[1 2 3 4 5]
// Function parameters - unquoted keys
(fn connect {host: "localhost" port: 8080 ssl: false}
(+ (if ssl "https" "http") "://" host ":" port))
// Function calls - unquoted keys
(connect {host: "api.example.com" ssl: true})
// Destructuring - unquoted
(let {x y z} point)
(let {x: newX y: newY} point)
Why Lisp style?
Use this when copy-pasting from JSON or when team prefers it:
// Objects/Maps - quoted keys, commas
{"name": "Alice", "age": 25, "city": "NYC"}
// Arrays - commas
[1, 2, 3, 4, 5]
// Function parameters - quoted keys, commas
(fn connect {"host": "localhost", "port": 8080, "ssl": false}
(+ (if ssl "https" "http") "://" host ":" port))
// Function calls - quoted keys, commas
(connect {"host": "api.example.com", "ssl": true})
Why JSON style is supported:
| Feature | Lisp Style | JSON Style | Both Work? |
|---|---|---|---|
| Map keys | {x: 1} | {"x": 1} | ✅ Yes |
| Commas in maps | {x: 1 y: 2} | {"x": 1, "y": 2} | ✅ Yes |
| Array elements | [1 2 3] | [1, 2, 3] | ✅ Yes |
| Function params | {x: 0 y: 0} | {"x": 0, "y": 0} | ✅ Yes |
| Destructuring | {x y} | {x, y} | ✅ Yes |
| Mixed styles | {x: 1, "y": 2} | - | ✅ Yes |
Use: Pure Lisp style throughout
// Preferred
(fn api-call {url: "" method: "GET" timeout: 30}
(fetch url {method: method timeout: timeout}))
(api-call {url: "https://api.example.com" method: "POST"})
Use: JSON style initially, migrate gradually
// Start with JSON style (familiar)
(fn api-call {"url": "", "method": "GET", "timeout": 30}
(fetch url {"method": method, "timeout": timeout}))
// Migrate to Lisp style over time
(fn api-call {url: "" method: "GET" timeout: 30}
(fetch url {method: method timeout: timeout}))
Use: Whichever matches your data source
// Copy-paste JSON response directly
(let response {
"status": 200,
"data": {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
}
})
// Or convert to Lisp style if you prefer
(let response {
status: 200
data: {
users: [
{id: 1 name: "Alice"}
{id: 2 name: "Bob"}
]
}
})
Use: Pick one style and be consistent
// Bad - mixed styles confuse readers
(fn process {url: ""} ...)
(fn handle {"data": []} ...)
// Good - consistent throughout
(fn process {url: ""} ...)
(fn handle {data: []} ...)
// Use kebab-case (Lisp tradition)
(let user-name "Alice")
(fn get-user-data [] ...)
(fn parse-json-response [] ...)
// Not camelCase
(let userName "Alice") // Works but not idiomatic
// Use SCREAMING-KEBAB-CASE
(let MAX-RETRIES 3)
(let API-TIMEOUT 30)
(let DEFAULT-HOST "localhost")
// End with ? for predicates
(fn empty? [coll] ...)
(fn valid-email? [email] ...)
(fn authenticated? [] ...)
// 1. Imports first
(import [helper1 helper2] from "./utils")
// 2. Constants
(let API-BASE "https://api.example.com")
(let MAX-RETRIES 3)
// 3. Helper functions
(fn validate-input [data] ...)
(fn format-response [res] ...)
// 4. Main functions
(fn fetch-data {url: "" retries: MAX-RETRIES}
...)
// 5. Exports last
(export [fetch-data] "./api")
// Small, focused functions
(fn validate-email [email]
(.includes email "@"))
(fn validate-user [user]
(and
(validate-email user.email)
(> (.-length user.name) 0)))
// Not one giant function
(fn process-user [user]
// 100 lines of mixed concerns
...)
[] - Simple Functions// ≤3 parameters, no defaults needed
(fn add [x y]
(+ x y))
(fn format-name [first last]
(+ first " " last))
(fn calculate-area [width height]
(* width height))
Use when:
{} - Config Functions// Many parameters, all have defaults
(fn connect {
host: "localhost"
port: 8080
ssl: false
timeout: 30
retries: 3
}
...)
// Call with only what you need
(connect {host: "api.com" ssl: true})
Use when:
// 2 spaces per level
(fn process-data [data]
(let cleaned (clean data))
(let validated (validate cleaned))
validated)
// Align closing parens with opening line
(fn complex-logic []
(if condition
(do
(action-1)
(action-2))
(fallback)))
// Prefer ≤80 characters
// OK - fits in one line
(fn greet [name] (+ "Hello, " name "!"))
// Better - split long lines
(fn greet [name]
(+ "Hello, " name "!"))
// Long function calls - break at logical points
(api-call
{url: "https://api.example.com/v1/users"
method: "POST"
headers: {authorization: token}
body: {name: "Alice" role: "admin"}})
// Use // for single-line comments
// This is a comment
// Prefer comments above code, not inline
(let result (+ x y)) // Add numbers <- avoid
// Better:
// Calculate sum of x and y
(let result (+ x y))
// Document public functions
// Fetches user data from the API
//
// Parameters:
// user-id: The unique identifier for the user
// options: Optional fetch configuration
//
// Returns: User object or null if not found
(fn fetch-user {user-id: 0 options: {}}
...)
(fn divide [a b]
(if (=== b 0)
(throw (Error "Division by zero"))
(/ a b)))
// Bad
(throw (Error "Invalid"))
// Good
(throw (Error "Invalid email format: expected user@domain.com"))
// Named-arg call-site sugar is NOT supported
(fn add [x y] (+ x y))
(add x: 10 y: 20) // ❌ HQL1001 error
// Must use map instead:
(add {x: 10 y: 20}) // ✅ Works (Lisp style)
(add {"x": 10, "y": 20}) // ✅ Works (JSON style)
// Loop keywords to:, from:, by: ARE allowed (special form)
(for [i to: 10] ...)
(for [i from: 0 to: 10 by: 2] ...)
// Lisp style
{name: "Alice" age: 25}
[1 2 3]
(fn connect {host: "" port: 8080} ...)
(connect {host: "api.com"})
// JSON style
{"name": "Alice", "age": 25}
[1, 2, 3]
(fn connect {"host": "", "port": 8080} ...)
(connect {"host": "api.com"})
// Named-arg call-site syntax
(add x: 10 y: 20) // ❌ HQL1001 error
// Mixing function param styles
(fn bad [x {y: 0}] ...) // ❌ Error - pick one style
Following Clojure's battle-tested approach:
Result: Maximum compatibility without sacrificing identity as a Lisp.
docs/features/06-function/README.md - Function feature documentationdocs/features/06-function/spec.md - Detailed function specificationtest/organized/syntax/function/ - Comprehensive test examples