All chapters21
On this page

Guide · Chapter 2

Language Basics

Variables, values, functions, conditions, loops, and scope.

What dynamic typing means

HHY declarations omit types; each value carries its logical type at runtime. Dynamic does not mean coercive: conditions require Bool, String never automatically becomes Number, Bool, or Path, and invalid arity or operations raise structured errors. Use type(value) to inspect a type and is_type(value, name) to test it.

example.hhy
let nothing = null
let enabled = true
let count = 42
let ratio = 0.75
let title = "HHY"
let pattern = /ERROR|WARN/i
let names = ["Ada", "Linus"]
let user = { name: "Ada", active: true }
let indexes = 0..3
let size_limit = 10mib
let timeout_limit = 5s
let completion = 80%

print(type(user))
print(is_type(title, "String"))

Scalar and unit types

TypeExampleUse
NullnullAbsence
BooltrueConditions and predicates
Int42Integer arithmetic
Float3.14Floating-point arithmetic
String"hello"UTF-8 text
Regex/ERROR/iText matching
Bytes10mibFile or memory size
Duration5sTimeouts and intervals
Percent80%Ratios
DateTimenow()Zoned time
Pathpath("logs")Filesystem paths

Precise String, number, unit, and Path edge cases belong in Reference. For ordinary scripts, remember that HHY never implicitly converts among String, Number, Bool, and Path.

Open the type and syntax reference →Look up exact UTF-8, overflow, operator, and literal behavior.

List, Map, and Range

List indices start at zero and out-of-range access raises IndexError. Map keys are Strings and preserve insertion order; map.key equals map["key"]. A missing key normally returns null, while require distinguishes a missing key from a present key whose value is null. Range a..b includes a and excludes b without allocating a List.

example.hhy
let original = ["Flow", "System"]
let extended = append(original, "Pipe")
let shortened = remove_at(extended, 1)

let config = { retries: 3, label: null }
let updated = put(config, "timeout", 5s)
let selected = pick(updated, ["retries", "timeout"])

print(original)
print(shortened)
print(get(config, "missing"))
print(require(config, "label"))
print(selected)

Lists and Maps are not mutated in place. append, remove_at, put, remove_key, and pick return new collections, leaving original and config unchanged. Lists and Maps support deep equality; Functions, Streams, and system resources do not support value equality.

Result, Stream, and system objects

TypeUsed for
ResultExplicit success values or Errors from one operation
StreamLazy files, lines, processes, responses, and events
ErrorFailures with category, location, and Flow stage
FunctionUser functions and closures
System objectDedicated values such as File, Process, and HttpResponse

System objects are not Maps. Map or pick ordinary fields before JSON encoding. Flow explains Stream laziness and consumption in detail.

Bindings, scope, and immutability

example.hhy
let service = "api"
let mut retries = 0
retries = retries + 1

let creates a binding that cannot be reassigned; use let mut when reassignment is required. List and Map update functions return new collections rather than mutating originals. Names follow block lexical scope and must be declared before use.

Conditions, loops, and functions

example.hhy
fn classify(score) {
    if score >= 90 { return "excellent" }
    else if score >= 60 { return "pass" }
    else { return "retry" }
}

let mut total = 0
for score in [98, 72, 55] {
    if score < 60 { continue }
    total = total + score
}

let mut attempts = 0
while attempts < 3 {
    attempts = attempts + 1
}

print(classify(98))
print(total)

HHY supports if / else if / else, for item in iterable, while, break, and continue. for iterates Lists, Map entries, Ranges, or Streams; iterating a Stream consumes it. Functions use positional arguments checked at call time and return null without an explicit return.

example.hhy
fn summarize(items) {
    let mut total = 0

    for item in items {
        if item.enabled {
            total = total + item.score
        }
    }

    return total
}

let users = [
    { name: "Ada", enabled: true, score: 98 },
    { name: "Linus", enabled: false, score: 86 }
]

summarize(users) |> print

In the current 1.7.1 runtime, use the trailing Flow closure form { item -> expression }; a multi-statement closure must name its parameter and use return. A one-argument closure in an unambiguous Flow context may use { it * 2 }. V1.7.1 has no overloading, generics, or default arguments.