Vars and Values
In Hot, everything is either a Var (a named binding) or a Value (data). This simple model is the foundation of the language.
Values
Values are the data in your program. Hot supports these value types:
| Type | Example | Description |
|---|---|---|
Str | "hello" | Text strings |
Int | 42 | Whole numbers |
Dec | 19.99 | Decimal numbers (not floats!) |
Bool | true, false | Boolean values |
Null | null | Absence of value |
Vec | [1, 2, 3] | Ordered collections (vectors) |
Map | {a: 1} | Key-value collections (like objects) |
Fn | (x) { x } | Functions |
All values are immutable. You don't modify data; you create new data.
Vars
A Var is a name bound to a value. Unlike most languages, Hot uses no = sign for assignment:
// Var assignment: name followed by value
name "Alice"
age 30
items [1, 2, 3]
Think of it as "name is value" rather than "name equals value".
With Type Annotations
You can optionally add a type between the name and value:
name-typed: Str "Alice"
count: Int 42
prices: Vec<Dec> [9.99, 19.99, 29.99]
Deep Path Assignment
Assign to nested paths to build up Maps:
user.name "Bob"
user.email "bob@example.com"
user.settings.theme "dark"
user.settings.notifications true
Deep paths also work with Vec indices:
config.servers[0] "api.example.com"
config.servers[1] "db.example.com"
config.retries 3
Appending to Vectors
Use empty brackets to append to a vector:
// Use empty brackets to append to a vector
list[] "first"
list[] "second"
list[] "third"
// list = ["first", "second", "third"]
Like all Hot operations, this follows immutable semantics. Each append creates a new vector and rebinds the variable—it doesn't mutate the original. The syntax is shorthand for:
list [] // list = []
list concat(list, ["first"]) // list = ["first"] (new vector, rebind)
list concat(list, ["second"]) // list = ["first", "second"] (new vector, rebind)
This also works with nested paths:
// Append to nested vectors
shopping.items[] "apple"
shopping.items[] "banana"
shopping.items[] "cherry"
// shopping = {items: ["apple", "banana", "cherry"]}
Each nested append creates a new outer structure with the updated inner vector.
Deep Path Access
Read from nested paths into a new var:
user-nested {name: "Alice", settings: {theme: "dark"}}
// Deep-get access: assign nested value to a new var
theme user-nested.settings.theme
user-nested.settings.theme → "dark"
This works with Vec indices too:
servers ["api.example.com", "db.example.com"]
primary first(servers)
Namespaces
Every Var lives in a namespace. Each Hot file declares its namespace with ns keyword:
::myapp::users ns
// These vars are in the ::myapp::users namespace
default-role "member"
max-users 1000
Referencing Vars
Use the full path ::namespace/var-name to reference vars from other namespaces:
// Reference a var from another namespace
role ::myapp::users/default-role
Namespace Aliases
Create shorter names for frequently-used namespaces:
// Create aliases
::http ::hot::http
::env ::hot::env
// Now use the short form
api-url ::env/get("API_URL")
response ::http/get(api-url)
Importing Specific Items
Import individual vars/functions into your namespace:
::myapp::handlers ns
// Import specific items
HttpResponse ::hot::http/HttpResponse
send-email ::notifications::email/send
// Use without namespace prefix
response HttpResponse({status: 200})
Core Vars
Vars marked with core: true in their metadata are available everywhere without namespace qualification.
Hot's standard library uses this extensively:
::myapp::example ns
// These are core - no prefix needed
doubled map([1, 2, 3], (x) { mul(x, 2) })
total add(1, 2)
name Str(42)
// Equivalent to:
// doubled ::hot::coll/map(...)
// total ::hot::math/add(...)
// name ::hot::type/Str(...)
Core functions from hot-std include: map, filter, reduce, add, sub, mul, div, eq, lt, gt, if, and, or, not, Str, Int, Dec, ok, err, and many more.
Making Your Own Core Vars
You can mark your own vars as core to make them available throughout your application without namespace prefixes. This is powerful for domain-specific languages or frequently-used utilities:
::myapp::domain ns
// Mark a function as core
send-notification
meta {
core: true,
doc: "Send a notification to a user."
}
fn (user-id: Str, message: Str): Result {
// implementation
}
// Mark a type constructor as core
UserId
meta {core: true}
type fn (id: Str): UserId { id }
Now any file in your application can use these without imports:
::myapp::handlers ns
// No import needed - these are core
result send-notification("user-123", "Welcome!")
id UserId("user-456")
This lets you bend the language to your domain, making common operations feel built-in.
Immutability
All bindings are immutable. You cannot reassign a var:
count 1
count 2 // This creates a NEW binding, shadowing the first
This isn't "changing" count—it's creating a new var that shadows the previous one. In most contexts, you'll work with transformations that produce new values:
numbers [1, 2, 3]
doubled-nums map(numbers, (x) { mul(x, 2) }) // [2, 4, 6]
// 'numbers' is still [1, 2, 3]
numbers → [1, 2, 3] // Original unchanged
doubled-nums → [2, 4, 6] // New value returned
Summary
- Values are immutable data: strings, numbers, vectors, maps, functions
- Vars bind names to values using
name valuesyntax (no=) - Namespaces organize vars with
::path::to::namespace - Core functions are available everywhere without qualification
- Everything is immutable — create new values, don't modify existing ones