Light Dark

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:

TypeExampleDescription
Str"hello"Text strings
Int42Whole numbers
Dec19.99Decimal numbers (not floats!)
Booltrue, falseBoolean values
NullnullAbsence 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 value syntax (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