For AI Assistants: Use this as a skill file for Claude, Cursor, Copilot, or other LLMs.

Download raw markdown file

Nostos Language Reference

Complete reference for the Nostos programming language. This document combines all skill documentation into a single file.

Table of Contents

  1. Language Overview
  2. Gotchas & Common Mistakes
  3. Standard Library Reference
  4. End-to-End Examples
  5. Quick Reference Cheat Sheet
  6. Basics
  7. Collections
  8. Concurrency
  9. Control Flow
  10. Database
  11. Error Handling
  12. File I/O
  13. Functions
  14. HTTP
  15. JSON
  16. Lists
  17. Modules
  18. Reactive Programming
  19. Strings
  20. Templates & Metaprogramming
  21. Testing
  22. Traits
  23. Types
  24. WebSockets

Nostos Language Overview

Nostos is a modern functional programming language with a focus on expressiveness, safety, and metaprogramming.

Key Characteristics

Expression-based: Everything is an expression that returns a value. No statements.

# if/else returns a value
result = if x > 0 { "positive" } else { "non-positive" }

# Blocks return their last expression
value = {
    a = 10
    b = 20
    a + b  # This is returned
}

Algebraic Data Types: First-class sum types (variants) and product types (records).

type Result[T, E] = Ok(T) | Err(E)
type Person = { name: String, age: Int }

Pattern Matching: Destructure data elegantly.

match result {
    Ok(value) -> "Got: " ++ show(value),
    Err(msg) -> "Error: " ++ msg
}

UFCS (Uniform Function Call Syntax): Call any function as a method.

# These are equivalent:
double(5)
5.double()

# Enables fluent chaining:
[1, 2, 3].map(x => x * 2).filter(x => x > 2).sum()

Compile-time Metaprogramming: Templates generate code at compile time.

template logged(fn) = quote {
    println("Calling " ++ ~fn.name)
    ~fn.body
}

@logged
compute() = 42  # Prints "Calling compute" when called

Hindley-Milner Type Inference: Types are inferred but can be annotated.

# Compiler infers: add(Int, Int) -> Int
add(a, b) = a + b

# Explicit when needed
identity[T](x: T) -> T = x

Compared to Other Languages

Feature Nostos Similar to
Algebraic types type Option[T] = Some(T) \| None Rust, Haskell, OCaml
Pattern matching match x { ... } Rust, Scala, Elixir
Lambdas x => x * 2 JavaScript, Kotlin
String concat "a" ++ "b" Haskell, Elixir
List literals [1, 2, 3] Python, JavaScript
Map literals %{"a": 1} Elixir (similar)
Immutable by default x = 5 Rust, Scala
UFCS 5.double() D, Nim
Templates/macros @decorator Rust (proc macros), Elixir
Traits trait Show { show() } Rust, Scala, Haskell (typeclasses)

What Nostos is Good For

Program Structure

# Imports (optional)
import json
import http

# Type definitions
type User = { id: Int, name: String }

# Constants
const API_URL = "https://api.example.com"

# Functions
fetchUser(id: Int) -> Result[User, String] = {
    # ...
}

# Entry point
main() = {
    match fetchUser(1) {
        Ok(user) -> println("Hello, " ++ user.name),
        Err(e) -> println("Error: " ++ e)
    }
}

Module System

# math.nos
pub add(a: Int, b: Int) = a + b      # Public
helper(x: Int) = x * 2               # Private (no pub)

# main.nos
import math
main() = math.add(1, 2)              # Use qualified name

Concurrency Model

Nostos uses lightweight tasks (green threads) with message passing:

import concurrent

main() = {
    # Spawn concurrent task
    handle = spawn(() => {
        sleep(100)
        42
    })

    # Wait for result
    result = await(handle)  # 42
}

Error Handling

Two mechanisms: Result types for expected errors, exceptions for unexpected ones.

# Result for expected failures
parseNumber(s: String) -> Result[Int, String] = {
    # ...
}

# Exceptions for unexpected failures
riskyOperation() = {
    if somethingWrong {
        throw("Unexpected error")
    }
    result
}

# Catch exceptions
safe() = try {
    riskyOperation()
} catch {
    e -> "Caught: " ++ e
}

Nostos Gotchas & Common Mistakes

Things that trip people up when learning Nostos.

Syntax Differences

Comments use #, not //

# Correct: hash for comments
// Wrong: this is NOT a comment, it's a syntax error

String concatenation is ++, not +

# Correct
"Hello" ++ " " ++ "World"

# Wrong - type error (+ is for numbers)
"Hello" + " " + "World"

No semicolons

# Correct - expressions separated by newlines or in blocks
x = 1
y = 2

# Wrong - semicolons are syntax errors
x = 1;
y = 2;

Commas in match arms

# Correct - commas between match arms
match x {
    1 -> "one",
    2 -> "two",
    _ -> "other"
}

# Wrong - no commas causes parse error
match x {
    1 -> "one"
    2 -> "two"
}

then required in single-line if

# Multi-line if (no then needed)
if x > 0 {
    "positive"
} else {
    "non-positive"
}

# Single-line if REQUIRES then
if x > 0 then "positive" else "non-positive"

# Wrong - missing then
if x > 0 "positive" else "non-positive"

Variable Mutability

var vs mvar - different scopes

# var = local mutable variable (inside functions)
process() = {
    var counter = 0
    counter = counter + 1
    counter
}

# mvar = module-level mutable variable (top-level)
mvar globalCounter: Int = 0

increment() = {
    globalCounter = globalCounter + 1
}

# Wrong - var at module level
var badGlobal = 0  # Error!

# Wrong - mvar inside function
process() = {
    mvar x = 0  # Error!
}

Immutable by default

# This creates an immutable binding
x = 42
x = 43  # Error! Cannot reassign

# Use var for mutability
var x = 42
x = 43  # OK

Try/Catch Syntax

Catch uses pattern matching, not variable binding

# Correct - pattern matching syntax
try {
    riskyOperation()
} catch {
    "specific error" -> handleSpecific(),
    e -> handleGeneric(e)  # Catch-all pattern
}

# Wrong - this is NOT Nostos syntax
try {
    riskyOperation()
} catch (e) {
    handleError(e)
}

Catch arms need commas

# Correct
try { x } catch {
    "error1" -> handle1(),
    "error2" -> handle2(),
    _ -> handleOther()
}

# Wrong - missing commas
try { x } catch {
    "error1" -> handle1()
    "error2" -> handle2()
}

Type System

Generic syntax uses [], not <>

# Correct
List[Int]
Map[String, Int]
Option[T]

# Wrong - angle brackets are comparison operators
List<Int>  # Parsed as: List < Int > (comparison!)

Type field access uses .ty, not .type

# In templates, accessing field types:
template example(typeDef) = quote {
    ~typeDef.fields.map(f =>
        # Correct - use .ty
        eval(f.name ++ ": " ++ f.ty)
    )
}

# Wrong - type is a keyword
f.type  # Error!

Records need type definitions

# Correct - define type first
type Person = { name: String, age: Int }
p = Person("Alice", 30)

# Wrong - anonymous records don't exist
p = { name: "Alice", age: 30 }  # Error!

Functions

Single-expression functions don't need braces

# Both are correct:
add(a, b) = a + b
add(a, b) = { a + b }

# But multi-statement needs braces
process(x) = {
    y = x * 2
    y + 1
}

Return is implicit (last expression)

# Correct - last expression is returned
calculate(x) = {
    y = x * 2
    z = y + 1
    z  # This is returned
}

# Explicit return exists but rarely needed
earlyExit(x) = {
    if x < 0 { return 0 } else { () }
    x * 2
}

Pattern matching in function definitions

# Multiple clauses with patterns
factorial(0) = 1
factorial(n) = n * factorial(n - 1)

# List patterns
sum([]) = 0
sum([h | t]) = h + sum(t)

# The clauses are tried in order

Lists and Collections

List cons pattern is [h | t], not h::t

# Correct
match list {
    [] -> "empty",
    [h | t] -> "head: " ++ show(h)
}

# Wrong - not Nostos syntax
match list {
    h::t -> "head: " ++ show(h)
}

Map literals use %{}

# Correct
myMap = %{"a": 1, "b": 2}

# Wrong - this is a block, not a map
myMap = {"a": 1, "b": 2}  # Error!

Indexing with [] vs .get()

# Direct indexing (may panic)
list[0]       # First element, panics if empty
map["key"]    # Value for key, panics if missing

# Safe access with Option
list.get(0)   # Some(first) or None
map.get("key") # Some(value) or None

Common Runtime Errors

Forgetting to handle None/Err

# This will panic if list is empty
first = list[0]

# Safe alternative
first = match list.get(0) {
    Some(x) -> x,
    None -> defaultValue
}

Integer division truncates

5 / 2   # Returns 2, not 2.5

# For float division, use floats
5.0 / 2.0  # Returns 2.5

String comparison is case-sensitive

"Hello" == "hello"  # false

# For case-insensitive comparison
"Hello".toLower() == "hello".toLower()  # true

Templates

~ splices AST, not values

template example(fn) = quote {
    # ~fn.body inserts the AST of the function body
    result = ~fn.body
    result * 2
}

# The splice happens at compile time, not runtime

eval() parses strings as code

template makeFn(name) = quote {
    # eval turns a string into code
    ~eval(~name ++ "() = 42")
}

# This generates: myFunc() = 42
@makeFn("myFunc")
type Dummy = Dummy {}

gensym for unique names

template safe(fn) = quote {
    # Without gensym, variable names might collide
    ~gensym("tmp") = ~fn.body
    ~gensym("tmp")  # Different name each time!
}

Nostos Standard Library Reference

Quick reference for methods on built-in types.

String

s = "Hello, World!"

# Length and access
s.length()              # 13
s.charAt(0)             # 'H'
s.substring(0, 5)       # "Hello"

# Case conversion
s.toLower()             # "hello, world!"
s.toUpper()             # "HELLO, WORLD!"

# Search
s.contains("World")     # true
s.startsWith("Hello")   # true
s.endsWith("!")         # true
s.indexOf("o")          # Some(4)
s.lastIndexOf("o")      # Some(8)

# Manipulation
s.trim()                # Remove whitespace from ends
s.trimStart()           # Remove leading whitespace
s.trimEnd()             # Remove trailing whitespace
s.replace("World", "Nostos")  # "Hello, Nostos!"
s.replaceAll("l", "L")  # "HeLLo, WorLd!"

# Split and join
"a,b,c".split(",")      # ["a", "b", "c"]
["a", "b", "c"].join("-")  # "a-b-c"

# Conversion
"42".parseInt()         # Some(42) or None
"3.14".parseFloat()     # Some(3.14) or None
show(42)                # "42" (any value to string)

# Concatenation
"Hello" ++ " " ++ "World"  # "Hello World"

# Characters
s.chars()               # List of Char
s.bytes()               # List of Int (UTF-8 bytes)

List[T]

list = [1, 2, 3, 4, 5]

# Length and access
list.length()           # 5
list[0]                 # 1 (panics if out of bounds)
list.get(0)             # Some(1) or None
list.first()            # Some(1) or None
list.last()             # Some(5) or None

# Add/remove
list.push(6)            # [1, 2, 3, 4, 5, 6]
list.append([6, 7])     # [1, 2, 3, 4, 5, 6, 7]
list.prepend(0)         # [0, 1, 2, 3, 4, 5]
[h | t] = list          # h = 1, t = [2, 3, 4, 5]

# Transform
list.map(x => x * 2)    # [2, 4, 6, 8, 10]
list.filter(x => x > 2) # [3, 4, 5]
list.flatMap(x => [x, x])  # [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]

# Reduce
list.fold(0, (acc, x) => acc + x)  # 15
list.reduce((a, b) => a + b)       # 15 (no initial value)
list.sum()              # 15 (for numeric lists)
list.product()          # 120 (for numeric lists)

# Search
list.find(x => x > 3)   # Some(4)
list.any(x => x > 3)    # true
list.all(x => x > 0)    # true
list.contains(3)        # true
list.indexOf(3)         # Some(2)

# Sort and reverse
list.sort()             # [1, 2, 3, 4, 5]
list.sortBy(x => -x)    # [5, 4, 3, 2, 1]
list.reverse()          # [5, 4, 3, 2, 1]

# Slice
list.take(3)            # [1, 2, 3]
list.drop(2)            # [3, 4, 5]
list.slice(1, 4)        # [2, 3, 4]
list.takeWhile(x => x < 4)  # [1, 2, 3]
list.dropWhile(x => x < 3)  # [3, 4, 5]

# Combine
list.zip([10, 20, 30])  # [(1, 10), (2, 20), (3, 30)]
[[1, 2], [3, 4]].flatten()  # [1, 2, 3, 4]

# Check
list.isEmpty()          # false
list.nonEmpty()         # true

# Convert
list.toSet()            # Set with unique elements

Map[K, V]

map = %{"a": 1, "b": 2, "c": 3}

# Access
map["a"]                # 1 (panics if missing)
map.get("a")            # Some(1) or None
map.getOrElse("x", 0)   # 0 (default if missing)

# Modify (returns new map)
map.insert("d", 4)      # %{"a": 1, "b": 2, "c": 3, "d": 4}
map.remove("a")         # %{"b": 2, "c": 3}
map.update("a", v => v + 10)  # %{"a": 11, "b": 2, "c": 3}

# Query
map.contains("a")       # true
map.size()              # 3
map.isEmpty()           # false

# Iterate
map.keys()              # ["a", "b", "c"]
map.values()            # [1, 2, 3]
map.entries()           # [("a", 1), ("b", 2), ("c", 3)]

# Transform
map.map((k, v) => (k, v * 2))     # %{"a": 2, "b": 4, "c": 6}
map.filter((k, v) => v > 1)       # %{"b": 2, "c": 3}
map.mapValues(v => v * 2)         # %{"a": 2, "b": 4, "c": 6}

# Merge
map.merge(%{"c": 30, "d": 4})     # %{"a": 1, "b": 2, "c": 30, "d": 4}

Set[T]

set = Set.from([1, 2, 3, 2, 1])  # {1, 2, 3}

# Modify (returns new set)
set.insert(4)           # {1, 2, 3, 4}
set.remove(2)           # {1, 3}

# Query
set.contains(2)         # true
set.size()              # 3
set.isEmpty()           # false

# Set operations
a = Set.from([1, 2, 3])
b = Set.from([2, 3, 4])
a.union(b)              # {1, 2, 3, 4}
a.intersection(b)       # {2, 3}
a.difference(b)         # {1}
a.isSubsetOf(b)         # false

# Convert
set.toList()            # [1, 2, 3]

Option[T]

some = Some(42)
none = None

# Check
some.isSome()           # true
none.isNone()           # true

# Extract
some.unwrap()           # 42 (panics if None)
none.unwrapOr(0)        # 0
some.getOrElse(0)       # 42

# Transform
some.map(x => x * 2)    # Some(84)
none.map(x => x * 2)    # None
some.flatMap(x => Some(x + 1))  # Some(43)
some.filter(x => x > 50)        # None

# Convert
some.toList()           # [42]
none.toList()           # []
some.okOr("error")      # Ok(42)
none.okOr("error")      # Err("error")

# Pattern match
match opt {
    Some(x) -> "got " ++ show(x),
    None -> "nothing"
}

Result[T, E]

ok = Ok(42)
err = Err("failed")

# Check
ok.isOk()               # true
err.isErr()             # true

# Extract
ok.unwrap()             # 42 (panics if Err)
err.unwrapErr()         # "failed"
ok.unwrapOr(0)          # 42
err.unwrapOr(0)         # 0

# Transform
ok.map(x => x * 2)      # Ok(84)
err.map(x => x * 2)     # Err("failed")
ok.mapErr(e => "Error: " ++ e)   # Ok(42)
err.mapErr(e => "Error: " ++ e)  # Err("Error: failed")
ok.flatMap(x => Ok(x + 1))       # Ok(43)

# Convert
ok.ok()                 # Some(42)
err.ok()                # None
ok.err()                # None
err.err()               # Some("failed")

# Pattern match
match result {
    Ok(value) -> "success: " ++ show(value),
    Err(e) -> "error: " ++ e
}

Int / Float

# Int operations
42.abs()                # 42
(-42).abs()             # 42
10.max(20)              # 20
10.min(20)              # 10
10.clamp(5, 15)         # 10
2.pow(10)               # 1024

# Float operations
3.14.floor()            # 3.0
3.14.ceil()             # 4.0
3.14.round()            # 3.0
3.14.abs()              # 3.14
(-1.5).abs()            # 1.5
4.0.sqrt()              # 2.0
2.0.pow(3.0)            # 8.0

# Conversion
42.toFloat()            # 42.0
3.14.toInt()            # 3
42.toString()           # "42"

# Ranges
(1..5)                  # [1, 2, 3, 4] (exclusive end)
(1..=5)                 # [1, 2, 3, 4, 5] (inclusive end)

Char

c = 'A'

c.isAlpha()             # true
c.isDigit()             # false
c.isAlphanumeric()      # true
c.isWhitespace()        # false
c.isUpper()             # true
c.isLower()             # false
c.toLower()             # 'a'
c.toUpper()             # 'A'
c.toInt()               # 65 (ASCII/Unicode value)
Char.fromInt(65)        # 'A'

Tuple

t = (1, "hello", true)

# Access by destructuring
(a, b, c) = t           # a = 1, b = "hello", c = true

# Two-element tuples have .0 and .1
pair = (10, 20)
pair.0                  # 10
pair.1                  # 20

# Swap
(10, 20).swap()         # (20, 10)

Common Utility Functions

# Printing
print("no newline")
println("with newline")
println("Value: " ++ show(42))

# Assertions (panic on failure)
assert_eq(expected, actual)
assert_eq(4, 2 + 2)

# Type conversion
show(anyValue)          # Convert to String
"42".parseInt()         # String to Int
"3.14".parseFloat()     # String to Float

# Comparison
min(a, b)               # Smaller of two
max(a, b)               # Larger of two

# Ranges
range(0, 5)             # [0, 1, 2, 3, 4]
range(0, 10, 2)         # [0, 2, 4, 6, 8] (with step)

File I/O

import file

# Read
content = file.read("path.txt")           # Result[String, String]
lines = file.readLines("path.txt")        # Result[List[String], String]
bytes = file.readBytes("path.bin")        # Result[List[Int], String]

# Write
file.write("path.txt", "content")         # Result[(), String]
file.writeLines("path.txt", ["a", "b"])   # Result[(), String]
file.append("path.txt", "more")           # Result[(), String]

# Check
file.exists("path.txt")                   # Bool
file.isFile("path.txt")                   # Bool
file.isDir("path")                        # Bool

# Directory
file.listDir(".")                         # Result[List[String], String]
file.createDir("newdir")                  # Result[(), String]

JSON

import json

# Parse
json.parse('{"name": "Alice", "age": 30}')  # Result[Json, String]

# Access Json values
j = json.parse('{"items": [1, 2, 3]}').unwrap()
j["items"]              # Json array
j["items"][0]           # Json number

# Convert to typed value
type Person = { name: String, age: Int }
json.decode[Person]('{"name": "Alice", "age": 30}')  # Result[Person, String]

# Encode
json.encode(Person("Alice", 30))  # '{"name":"Alice","age":30}'

HTTP

import http

# GET request
response = http.get("https://api.example.com/data")  # Result[Response, String]
body = response.unwrap().body                        # String

# POST with JSON
response = http.post("https://api.example.com/users",
    body: '{"name": "Alice"}',
    headers: %{"Content-Type": "application/json"}
)

# Response fields
response.status         # Int (200, 404, etc.)
response.body           # String
response.headers        # Map[String, String]

Concurrency

import concurrent

# Spawn task
handle = spawn(() => expensiveComputation())

# Wait for result
result = await(handle)

# Sleep
sleep(1000)             # Milliseconds

# Parallel map
results = [1, 2, 3].parMap(x => compute(x))

# Channels
ch = channel()
spawn(() => ch.send(42))
value = ch.receive()    # 42

Nostos End-to-End Examples

Complete working examples for common tasks.

Read JSON File and Extract Data

import file
import json

type User = { id: Int, name: String, email: String }
type UsersFile = { users: List[User] }

main() = {
    # Read and parse JSON file
    content = match file.read("users.json") {
        Ok(s) -> s,
        Err(e) -> {
            println("Failed to read file: " ++ e)
            return ()
        }
    }

    # Parse JSON into typed structure
    data = match json.decode[UsersFile](content) {
        Ok(d) -> d,
        Err(e) -> {
            println("Failed to parse JSON: " ++ e)
            return ()
        }
    }

    # Process the data
    activeUsers = data.users.filter(u => u.email.contains("@"))

    println("Found " ++ show(activeUsers.length()) ++ " users:")
    activeUsers.map(u => println("  - " ++ u.name ++ " <" ++ u.email ++ ">"))
}

Sample users.json:

{
  "users": [
    {"id": 1, "name": "Alice", "email": "alice@example.com"},
    {"id": 2, "name": "Bob", "email": "bob@example.com"}
  ]
}

HTTP Request and Parse Response

import http
import json

type Post = { id: Int, title: String, body: String, userId: Int }

# Fetch a single post
fetchPost(id: Int) -> Result[Post, String] = {
    response = http.get("https://jsonplaceholder.typicode.com/posts/" ++ show(id))
    match response {
        Ok(r) -> {
            if r.status == 200 {
                json.decode[Post](r.body)
            } else {
                Err("HTTP " ++ show(r.status))
            }
        },
        Err(e) -> Err(e)
    }
}

# Fetch multiple posts concurrently
fetchPosts(ids: List[Int]) -> List[Result[Post, String]] = {
    ids.parMap(id => fetchPost(id))
}

main() = {
    # Single request
    match fetchPost(1) {
        Ok(post) -> println("Title: " ++ post.title),
        Err(e) -> println("Error: " ++ e)
    }

    # Multiple concurrent requests
    results = fetchPosts([1, 2, 3, 4, 5])
    successes = results.filter(r => r.isOk()).map(r => r.unwrap())
    println("Fetched " ++ show(successes.length()) ++ " posts")
}

POST Request with JSON Body

import http
import json

type CreateUser = { name: String, email: String }
type UserResponse = { id: Int, name: String, email: String }

createUser(user: CreateUser) -> Result[UserResponse, String] = {
    body = json.encode(user)
    response = http.post(
        "https://api.example.com/users",
        body: body,
        headers: %{
            "Content-Type": "application/json",
            "Authorization": "Bearer " ++ getApiKey()
        }
    )
    match response {
        Ok(r) -> {
            if r.status >= 200 && r.status < 300 {
                json.decode[UserResponse](r.body)
            } else {
                Err("HTTP " ++ show(r.status) ++ ": " ++ r.body)
            }
        },
        Err(e) -> Err(e)
    }
}

getApiKey() = env("API_KEY").unwrapOr("default-key")

main() = {
    newUser = CreateUser(name: "Alice", email: "alice@example.com")
    match createUser(newUser) {
        Ok(created) -> println("Created user with ID: " ++ show(created.id)),
        Err(e) -> println("Failed: " ++ e)
    }
}

Simple CLI Tool

import file
import env

# Word count tool: count lines, words, chars in files

countFile(path: String) -> Result[(Int, Int, Int), String] = {
    match file.read(path) {
        Ok(content) -> {
            lines = content.split("\n").length()
            words = content.split(" ").flatMap(s => s.split("\n")).filter(s => s.length() > 0).length()
            chars = content.length()
            Ok((lines, words, chars))
        },
        Err(e) -> Err(e)
    }
}

printUsage() = {
    println("Usage: wc <file1> [file2] ...")
    println("Count lines, words, and characters in files")
}

main() = {
    args = env.args()  # Get command line arguments

    if args.length() < 2 {
        printUsage()
        return ()
    }

    files = args.drop(1)  # Skip program name

    var totalLines = 0
    var totalWords = 0
    var totalChars = 0

    files.map(path => {
        match countFile(path) {
            Ok((lines, words, chars)) -> {
                println(show(lines) ++ "\t" ++ show(words) ++ "\t" ++ show(chars) ++ "\t" ++ path)
                totalLines = totalLines + lines
                totalWords = totalWords + words
                totalChars = totalChars + chars
            },
            Err(e) -> println("Error reading " ++ path ++ ": " ++ e)
        }
    })

    if files.length() > 1 {
        println(show(totalLines) ++ "\t" ++ show(totalWords) ++ "\t" ++ show(totalChars) ++ "\ttotal")
    } else {
        ()
    }
}

Simple HTTP Server

import http.server
import json

type Todo = { id: Int, title: String, done: Bool }

mvar todos: List[Todo] = [
    Todo(1, "Learn Nostos", false),
    Todo(2, "Build something", false)
]

mvar nextId: Int = 3

handleRequest(req: Request) -> Response = {
    match (req.method, req.path) {
        ("GET", "/todos") -> {
            Response(
                status: 200,
                body: json.encode(todos),
                headers: %{"Content-Type": "application/json"}
            )
        },

        ("POST", "/todos") -> {
            match json.decode[{title: String}](req.body) {
                Ok(data) -> {
                    newTodo = Todo(nextId, data.title, false)
                    nextId = nextId + 1
                    todos = todos.push(newTodo)
                    Response(
                        status: 201,
                        body: json.encode(newTodo),
                        headers: %{"Content-Type": "application/json"}
                    )
                },
                Err(e) -> Response(status: 400, body: "Invalid JSON: " ++ e)
            }
        },

        ("DELETE", path) -> {
            if path.startsWith("/todos/") {
                idStr = path.substring(7, path.length())
                match idStr.parseInt() {
                    Some(id) -> {
                        todos = todos.filter(t => t.id != id)
                        Response(status: 204, body: "")
                    },
                    None -> Response(status: 400, body: "Invalid ID")
                }
            } else {
                Response(status: 404, body: "Not found")
            }
        },

        _ -> Response(status: 404, body: "Not found")
    }
}

main() = {
    println("Server running on http://localhost:8080")
    http.server.run(8080, handleRequest)
}

Data Pipeline with Error Handling

import file
import json

type RawRecord = { id: String, value: String, timestamp: String }
type ProcessedRecord = { id: Int, value: Float, timestamp: Int }

# Parse a raw record, returning errors for invalid data
parseRecord(raw: RawRecord) -> Result[ProcessedRecord, String] = {
    id = match raw.id.parseInt() {
        Some(n) -> n,
        None -> return Err("Invalid id: " ++ raw.id)
    }

    value = match raw.value.parseFloat() {
        Some(f) -> f,
        None -> return Err("Invalid value: " ++ raw.value)
    }

    timestamp = match raw.timestamp.parseInt() {
        Some(t) -> t,
        None -> return Err("Invalid timestamp: " ++ raw.timestamp)
    }

    Ok(ProcessedRecord(id, value, timestamp))
}

# Process a file, collecting successes and errors
processFile(path: String) -> (List[ProcessedRecord], List[String]) = {
    content = file.read(path).unwrapOr("[]")
    records = json.decode[List[RawRecord]](content).unwrapOr([])

    results = records.map(r => parseRecord(r))

    successes = results.filter(r => r.isOk()).map(r => r.unwrap())
    errors = results.filter(r => r.isErr()).map(r => r.unwrapErr())

    (successes, errors)
}

main() = {
    (records, errors) = processFile("data.json")

    println("Processed " ++ show(records.length()) ++ " records")

    if errors.nonEmpty() {
        println("Errors:")
        errors.map(e => println("  - " ++ e))
    } else {
        ()
    }

    # Calculate statistics
    if records.nonEmpty() {
        values = records.map(r => r.value)
        avg = values.sum() / values.length().toFloat()
        println("Average value: " ++ show(avg))
    } else {
        ()
    }
}

WebSocket Client

import websocket
import json

type Message = { type: String, content: String }

main() = {
    ws = websocket.connect("wss://echo.websocket.org")

    # Send a message
    msg = Message(type: "greeting", content: "Hello!")
    ws.send(json.encode(msg))

    # Receive response
    response = ws.receive()
    println("Received: " ++ response)

    # Close connection
    ws.close()
}

Concurrent Task Processing

import concurrent

type Task = { id: Int, data: String }
type TaskResult = { id: Int, result: String, success: Bool }

# Simulate processing a task
processTask(task: Task) -> TaskResult = {
    # Simulate work
    sleep(100)

    if task.data.length() > 0 {
        TaskResult(task.id, "Processed: " ++ task.data.toUpper(), true)
    } else {
        TaskResult(task.id, "Empty data", false)
    }
}

main() = {
    tasks = [
        Task(1, "hello"),
        Task(2, "world"),
        Task(3, ""),
        Task(4, "nostos"),
        Task(5, "rocks")
    ]

    # Process all tasks concurrently
    println("Processing " ++ show(tasks.length()) ++ " tasks...")

    results = tasks.parMap(t => processTask(t))

    successes = results.filter(r => r.success)
    failures = results.filter(r => !r.success)

    println("Completed: " ++ show(successes.length()) ++ " succeeded, " ++
            show(failures.length()) ++ " failed")

    successes.map(r => println("  [" ++ show(r.id) ++ "] " ++ r.result))
}

Configuration with Defaults

import file
import json
import env

type Config = {
    host: String,
    port: Int,
    debug: Bool,
    maxConnections: Int
}

defaultConfig = Config(
    host: "localhost",
    port: 8080,
    debug: false,
    maxConnections: 100
)

# Load config from file, falling back to defaults
loadConfig(path: String) -> Config = {
    # Try file first
    fileConfig = match file.read(path) {
        Ok(content) -> json.decode[Config](content).ok(),
        Err(_) -> None
    }

    match fileConfig {
        Some(c) -> c,
        None -> {
            # Fall back to environment variables
            Config(
                host: env("HOST").unwrapOr(defaultConfig.host),
                port: env("PORT").flatMap(s => s.parseInt()).unwrapOr(defaultConfig.port),
                debug: env("DEBUG").map(s => s == "true").unwrapOr(defaultConfig.debug),
                maxConnections: env("MAX_CONN").flatMap(s => s.parseInt()).unwrapOr(defaultConfig.maxConnections)
            )
        }
    }
}

main() = {
    config = loadConfig("config.json")

    println("Starting server with config:")
    println("  Host: " ++ config.host)
    println("  Port: " ++ show(config.port))
    println("  Debug: " ++ show(config.debug))
    println("  Max connections: " ++ show(config.maxConnections))
}

Nostos Quick Reference Cheat Sheet

One-page syntax and patterns reference.

Basics

# Comments start with hash
x = 42              # Immutable binding
var y = 0           # Mutable local variable
mvar z: Int = 0     # Mutable module-level variable
const PI = 3.14     # Compile-time constant

Types

Int Float Bool String Char ()     # Primitives
List[T] Map[K,V] Set[T]           # Collections
Option[T] Result[T,E]             # Common patterns
(A, B, C)                         # Tuples

type Point = { x: Int, y: Int }   # Record
type Color = Red | Green | Blue   # Variants
type Tree[T] = Leaf(T) | Node(Tree[T], Tree[T])

Literals

42  -17  1_000_000  0xFF  0b1010  # Int
3.14  -0.5  1.0e10                # Float
true  false                       # Bool
"hello"  'c'  '\n'                # String, Char
[1, 2, 3]                         # List
%{"a": 1, "b": 2}                 # Map
(1, "two", true)                  # Tuple

Operators

+  -  *  /  %                     # Arithmetic
==  !=  <  <=  >  >=              # Comparison
&&  ||  !                         # Logical
++                                # String concat
|>                                # Pipe (x |> f  =  f(x))

Functions

# Definition
add(a: Int, b: Int) -> Int = a + b
greet(name) = "Hello, " ++ name   # Types inferred

# Lambdas
x => x * 2
(a, b) => a + b

# Call
add(1, 2)
1.add(2)                          # UFCS

# Pattern matching in definition
factorial(0) = 1
factorial(n) = n * factorial(n - 1)

Control Flow

# If expression
if x > 0 { "positive" } else { "non-positive" }
if x > 0 then "positive" else "non-positive"

# Match
match value {
    0 -> "zero",
    n if n > 0 -> "positive",
    _ -> "negative"
}

# Loops
while condition { body }
for item in list { body }
(1..10).map(i => process(i))      # Prefer this

Pattern Matching

# Literals
match x { 1 -> "one", 2 -> "two", _ -> "other" }

# Destructuring
(a, b) = tuple
{ name, age } = person
[h | t] = list                    # Head and tail
[a, b, c] = threeList             # Exact length

# Variants
match opt {
    Some(x) -> use(x),
    None -> default
}

# Guards
match n {
    x if x > 0 -> "positive",
    x if x < 0 -> "negative",
    _ -> "zero"
}

Collections

# List
[1, 2, 3].map(x => x * 2)         # [2, 4, 6]
[1, 2, 3].filter(x => x > 1)      # [2, 3]
[1, 2, 3].fold(0, (a,b) => a+b)   # 6
list.length()  list[0]  list.get(0)

# Map
map = %{"a": 1, "b": 2}
map["a"]  map.get("a")
map.insert("c", 3)
map.keys()  map.values()

# Set
Set.from([1, 2, 2, 3])            # {1, 2, 3}
set.contains(x)  set.insert(x)
a.union(b)  a.intersection(b)

Option & Result

# Option
Some(42)  None
opt.map(f)  opt.flatMap(f)
opt.unwrap()  opt.unwrapOr(default)
opt.isSome()  opt.isNone()

# Result
Ok(value)  Err(error)
res.map(f)  res.mapErr(f)
res.unwrap()  res.unwrapOr(default)
res.isOk()  res.isErr()

Error Handling

# Try/catch
try {
    riskyOperation()
} catch {
    "specific" -> handleSpecific(),
    e -> handleGeneric(e)
}

# Throw
throw("error message")

# Early return
if bad { return Err("failed") } else { () }

Modules

# math.nos
pub add(a, b) = a + b             # Public
helper(x) = x * 2                 # Private

# main.nos
import math
math.add(1, 2)

Traits

trait Show {
    show(self) -> String
}

impl Show for Point {
    show(self) = "(" ++ show(self.x) ++ "," ++ show(self.y) ++ ")"
}

Templates

template logged(fn) = quote {
    println(">>> " ++ ~fn.name)
    ~fn.body
}

@logged
compute() = 42

# Key operations
quote { code }                    # Capture AST
~expr                             # Splice AST
eval("code string")               # Parse string as code
gensym("prefix")                  # Unique identifier
~fn.name  ~fn.body  ~fn.params    # Function metadata
~typeDef.name  ~typeDef.fields    # Type metadata

Concurrency

import concurrent

handle = spawn(() => computation())
result = await(handle)
sleep(1000)                       # Milliseconds
list.parMap(f)                    # Parallel map

I/O

# Print
print("no newline")
println("with newline")
show(42)                          # Any to String

# Files
import file
file.read("path")                 # Result[String, String]
file.write("path", content)       # Result[(), String]

# HTTP
import http
http.get(url)
http.post(url, body: data, headers: %{...})

# JSON
import json
json.parse(str)                   # Result[Json, String]
json.decode[Type](str)            # Result[Type, String]
json.encode(value)                # String

Common Patterns

# Pipeline
data
    .filter(x => x.valid)
    .map(x => transform(x))
    .fold(init, combine)

# Safe unwrap with default
value = opt.unwrapOr(default)

# Error propagation
match operation() {
    Ok(x) -> continue(x),
    Err(e) -> return Err(e)
}

# Builder pattern
Config()
    .withHost("localhost")
    .withPort(8080)
    .build()

Comparison to Other Languages

Nostos Python Rust JavaScript
# comment # comment // comment // comment
"a" ++ "b" "a" + "b" format!() "a" + "b"
[1,2,3] [1,2,3] vec![1,2,3] [1,2,3]
%{"a":1} {"a":1} HashMap {a:1}
x => x*2 lambda x: x*2 \|x\| x*2 x => x*2
List[Int] list[int] Vec<i32> N/A
Some(x) x or None Some(x) x or null
match {} match (3.10+) match {} switch

Nostos Basics

Comments

# This is a single-line comment
# Comments use the hash symbol

Literals

# Integers
42
-17
1_000_000       # Underscores for readability
0xFF            # Hexadecimal
0b1010          # Binary

# Floats
3.14
-0.5
1.0e10          # Scientific notation

# Booleans
true
false

# Strings (double or single quotes)
"Hello, World!"
'{"key": "value"}'   # Useful for JSON

# Characters
'a'
'\n'            # Newline
'\t'            # Tab

# Unit (empty value, like void)
()

Variables

# Immutable binding (default)
x = 42
name = "Alice"

# Mutable variable (use sparingly)
var counter = 0
counter = counter + 1

# Type annotations (optional, inferred)
x: Int = 42
name: String = "Alice"

Basic Types

Type Description Example
Int 64-bit integer 42
Float 64-bit float 3.14
Bool Boolean true, false
String UTF-8 string "hello"
Char Single character 'a'
() Unit type ()

Operators

# Arithmetic
1 + 2       # Addition
5 - 3       # Subtraction
4 * 2       # Multiplication
10 / 3      # Integer division
10 % 3      # Modulo

# Comparison
x == y      # Equal
x != y      # Not equal
x < y       # Less than
x <= y      # Less or equal
x > y       # Greater than
x >= y      # Greater or equal

# Logical
a && b      # And
a || b      # Or
!a          # Not

# String concatenation
"Hello" ++ " " ++ "World"   # "Hello World"

Printing

print("No newline")
println("With newline")

# Convert to string with show()
println("Value: " ++ show(42))

Hello World

# Every program needs a main() function
main() = {
    println("Hello, World!")
}

# Or single expression
main() = println("Hello, World!")

Constants

# Module-level constants (evaluated at compile time)
const PI = 3.14159
const MAX_SIZE = 1000
const GREETING = "Hello"

main() = println(GREETING ++ ", PI is " ++ show(PI))

Assertions

# assert_eq checks equality, panics on failure
assert_eq(4, 2 + 2)
assert_eq("hello", "hel" ++ "lo")

# Useful in tests
main() = {
    assert_eq(42, 6 * 7)
    println("All assertions passed!")
}

Collections in Nostos

Maps (Dictionaries)

# Create a map with %{ }
ages = %{"alice": 30, "bob": 25, "carol": 35}

# Type annotation
scores: Map[String, Int] = %{"math": 95, "english": 87}

# Empty map
empty: Map[String, Int] = %{}

Map Index Syntax

Maps support convenient bracket syntax for get and set:

m = %{"name": "Alice", "age": 30}

# Get value (returns value or Unit if not found)
name = m["name"]        # "Alice"
age = m["age"]          # 30

# Set value (updates variable with new map)
m["city"] = "Oslo"      # m now has 3 keys
m["age"] = 31           # Update existing key

# Works with any key type
ids = %{1: "one", 2: "two"}
ids[3] = "three"        # Add new entry
val = ids[1]            # "one"

Since maps are immutable, m["key"] = value is equivalent to m = Map.insert(m, "key", value).

Map Operations

m = %{"a": 1, "b": 2, "c": 3}

# Get value (returns Option)
m.get("a")              # Some(1)
m.get("z")              # None

# Get with default
m.getOrDefault("z", 0)  # 0

# Insert (returns new map)
m2 = m.insert("d", 4)   # %{"a": 1, "b": 2, "c": 3, "d": 4}

# Remove (returns new map)
m3 = m.remove("a")      # %{"b": 2, "c": 3}

# Update value
m4 = m.update("a", x => x + 10)  # %{"a": 11, "b": 2, "c": 3}

# Check existence
m.contains("a")         # true
m.contains("z")         # false

# Size
m.size()                # 3
m.isEmpty()             # false

Iterating Maps

m = %{"a": 1, "b": 2, "c": 3}

# Get keys as list
m.keys()                # ["a", "b", "c"]

# Get values as list
m.values()              # [1, 2, 3]

# Get entries as list of tuples
m.entries()             # [("a", 1), ("b", 2), ("c", 3)]

# Map over values
m.mapValues(v => v * 2) # %{"a": 2, "b": 4, "c": 6}

# Filter entries
m.filter((k, v) => v > 1)  # %{"b": 2, "c": 3}

# Fold
m.fold(0, (acc, k, v) => acc + v)  # 6

Merging Maps

m1 = %{"a": 1, "b": 2}
m2 = %{"b": 20, "c": 3}

# Merge (right side wins on conflict)
m1.merge(m2)            # %{"a": 1, "b": 20, "c": 3}

# Merge with custom conflict resolution
m1.mergeWith(m2, (v1, v2) => v1 + v2)  # %{"a": 1, "b": 22, "c": 3}

Sets

# Create a set with #{ }
colors = #{"red", "green", "blue"}

# Type annotation
numbers: Set[Int] = #{1, 2, 3, 4, 5}

# Empty set
empty: Set[String] = #{}

Set Operations

s = #{1, 2, 3, 4, 5}

# Add element (returns new set)
s2 = s.insert(6)        # #{1, 2, 3, 4, 5, 6}

# Remove element (returns new set)
s3 = s.remove(1)        # #{2, 3, 4, 5}

# Check membership
s.contains(3)           # true
s.contains(10)          # false

# Size
s.size()                # 5
s.isEmpty()             # false

# Convert to list
s.toList()              # [1, 2, 3, 4, 5]

Set Index Syntax

Sets support bracket syntax for membership checking:

s = #{1, 2, 3, 4, 5}

# Check membership (returns Bool)
s[3]                    # true
s[10]                   # false

# Use in conditions
if s[3] then {
    println("3 is in the set")
}

# Combine checks
s[1] && !s[100]         # true

# With variables
elem = 3
s[elem]                 # true

This is equivalent to s.contains(elem) but more concise.

Set Math Operations

a = #{1, 2, 3, 4}
b = #{3, 4, 5, 6}

# Union
a.union(b)              # #{1, 2, 3, 4, 5, 6}

# Intersection
a.intersection(b)       # #{3, 4}

# Difference
a.difference(b)         # #{1, 2}

# Subset check
#{1, 2}.isSubset(a)     # true
a.isSuperset(#{1, 2})   # true

Typed Arrays

# Efficient numeric arrays

# Float64Array
floats = Float64Array.new(100)          # 100 zeros
floats = Float64Array.from([1.0, 2.0, 3.0])

# Int64Array
ints = Int64Array.new(100)
ints = Int64Array.from([1, 2, 3, 4, 5])

# Float32Array (for GPU compatibility)
f32 = Float32Array.new(100)

Typed Array Operations

arr = Float64Array.from([1.0, 2.0, 3.0, 4.0, 5.0])

# Index access
arr[0]                  # 1.0
arr[2]                  # 3.0

# Index assignment (returns new array in immutable mode)
arr[0] = 10.0           # [10.0, 2.0, 3.0, 4.0, 5.0]

# Length
arr.length()            # 5

# Map (returns new typed array)
arr.map(x => x * 2)     # [2.0, 4.0, 6.0, 8.0, 10.0]

# Fold/reduce
arr.fold(0.0, (acc, x) => acc + x)  # 15.0

# Sum (optimized)
arr.sum()               # 15.0

# Slice
arr.slice(1, 4)         # [2.0, 3.0, 4.0]

Buffer Type

# Growable byte buffer

buf = Buffer.new()
buf = buf.append("Hello")
buf = buf.append(" World")
buf.toString()          # "Hello World"

# Useful for building strings or binary data
buildCsv(rows: List[List[String]]) -> String = {
    buf = Buffer.new()
    rows.forEach(row => {
        buf = buf.append(row.join(","))
        buf = buf.append("\n")
    })
    buf.toString()
}

Tuples

# Fixed-size, mixed-type collection
point = (10, 20)
named = ("Alice", 30, true)

# Destructuring
(x, y) = point
(name, age, active) = named

# Access by pattern matching
getFirst((a, _, _)) = a
getSecond((_, b, _)) = b

# Nested tuples
nested = ((1, 2), (3, 4))
((a, b), (c, d)) = nested

Converting Between Collections

# List to Set (removes duplicates)
[1, 2, 2, 3, 3, 3].toSet()      # #{1, 2, 3}

# Set to List
#{1, 2, 3}.toList()             # [1, 2, 3]

# List of pairs to Map
[("a", 1), ("b", 2)].toMap()    # %{"a": 1, "b": 2}

# Map to list of pairs
%{"a": 1, "b": 2}.entries()     # [("a", 1), ("b", 2)]

# List to typed array
[1.0, 2.0, 3.0].toFloat64Array()

# Typed array to list
Float64Array.from([1.0, 2.0]).toList()

Common Patterns

# Count occurrences
countWords(words: List[String]) -> Map[String, Int] =
    words.fold(%{}, (acc, word) => {
        count = acc.getOrDefault(word, 0)
        acc.insert(word, count + 1)
    })

# Group by key
groupBy(items: List[T], keyFn: T -> K) -> Map[K, List[T]] =
    items.fold(%{}, (acc, item) => {
        key = keyFn(item)
        existing = acc.getOrDefault(key, [])
        acc.insert(key, existing ++ [item])
    })

# Index lookup table
createIndex(items: List[T], keyFn: T -> K) -> Map[K, T] =
    items.fold(%{}, (acc, item) => acc.insert(keyFn(item), item))

# Deduplicate while preserving order
dedupe(items: List[T]) -> List[T] = {
    (result, _) = items.fold(([], #{}), ((acc, seen), item) => {
        if seen.contains(item) then (acc, seen)
        else (acc ++ [item], seen.insert(item))
    })
    result
}

Performance Considerations

# Maps and Sets: O(log n) for most operations
# - Use for lookup-heavy workloads
# - Immutable, returns new collection on modification

# Lists: O(n) for index access, O(1) for head/cons
# - Good for sequential processing
# - Use fold/map instead of index loops

# Typed Arrays: O(1) index access
# - Use for numeric computation
# - More memory efficient than List[Float]
# - Good for interop with FFI

# Choose based on:
# - Access pattern (random vs sequential)
# - Data type (numeric vs mixed)
# - Mutability needs

Concurrency in Nostos

Spawning Processes

# spawn creates a lightweight process
pid = spawn(() => {
    println("Hello from spawned process!")
})

# Process runs concurrently with main
println("Main continues immediately")

# Spawn with argument
worker(id: Int) = {
    println("Worker " ++ show(id) ++ " started")
}

spawn(() => worker(1))
spawn(() => worker(2))

Message Passing

# Processes communicate via messages
# self() returns current process ID

# Send a message
send(pid, "Hello!")

# Receive a message (blocks until received)
msg = receive()

# Basic ping-pong
main() = {
    parent = self()

    child = spawn(() => {
        msg = receive()
        send(parent, "Got: " ++ msg)
    })

    send(child, "Hello")
    reply = receive()
    println(reply)      # "Got: Hello"
}

Receive with Pattern Matching

# Receive with pattern matching
result = receive {
    "ping" -> "pong",
    ("add", a, b) -> show(a + b),
    ("quit") -> {
        println("Shutting down")
        "bye"
    },
    other -> "Unknown: " ++ show(other)
}

Receive with Timeout

# Receive with timeout (milliseconds)
result = receiveTimeout(1000) {
    msg -> "Got: " ++ msg
}

# Returns Option - None if timeout
match result {
    Some(value) -> println(value),
    None -> println("Timed out!")
}

MVar (Mutable Variable)

# MVar is a synchronized mutable container
# Can be empty or full

# Create empty MVar
mv = MVar.new()

# Create MVar with initial value
mv = MVar.newWith(42)

# Put value (blocks if full)
mv.put(100)

# Take value (blocks if empty)
value = mv.take()

# Read without removing
value = mv.read()

# Try operations (non-blocking, returns Option)
result = mv.tryTake()
result = mv.tryPut(42)

Worker Pool Pattern

# Process pool for parallel work

workerLoop(id: Int) = {
    match receive() {
        ("task", data, replyTo) -> {
            result = processData(data)
            send(replyTo, ("result", id, result))
            workerLoop(id)
        },
        "stop" -> ()
    }
}

createWorkers(n: Int) -> List[Pid] = {
    range(1, n + 1).map(id => spawn(() => workerLoop(id)))
}

main() = {
    workers = createWorkers(4)
    me = self()

    # Distribute work
    tasks = [1, 2, 3, 4, 5, 6, 7, 8]
    tasks.zip(cycle(workers)).forEach((task, worker) => {
        send(worker, ("task", task, me))
    })

    # Collect results
    results = tasks.map(_ => receive())
    println(show(results))

    # Stop workers
    workers.forEach(w => send(w, "stop"))
}

Parallel Map

# Parallel map over a list
parallelMap(items: List[T], f: T -> R) -> List[R] = {
    me = self()

    # Spawn a process for each item
    pids = items.map(item => spawn(() => {
        result = f(item)
        send(me, result)
    }))

    # Collect results (in order)
    items.map(_ => receive())
}

# Usage
squares = parallelMap([1, 2, 3, 4, 5], x => x * x)

Process Linking

# Link processes - if one dies, the other is notified
spawnLink(() => {
    # If this crashes, parent receives exit message
    riskyOperation()
})

# Handle exit messages
result = receive {
    ("EXIT", pid, reason) -> "Process " ++ show(pid) ++ " exited: " ++ reason,
    normalMsg -> handleNormal(normalMsg)
}

Supervisors

# Restart failed processes
supervisor(childFn) = {
    child = spawnLink(childFn)

    match receive() {
        ("EXIT", _, _) -> {
            println("Child crashed, restarting...")
            supervisor(childFn)
        },
        msg -> {
            send(child, msg)
            supervisor(childFn)
        }
    }
}

Ring Benchmark

# Classic concurrency benchmark: message ring

ringNode(next: Pid) = {
    match receive() {
        0 -> send(next, 0),
        n -> {
            send(next, n - 1)
            ringNode(next)
        }
    }
}

main() = {
    n = 1000    # Ring size
    m = 10000   # Messages

    # Create ring of processes
    first = self()
    last = range(1, n).fold(first, (prev, _) => {
        spawn(() => ringNode(prev))
    })

    # Connect last to first
    send(last, m)

    # Wait for completion
    receive()
    println("Ring complete!")
}

Async Sleep

# Sleep without blocking other processes
sleep(1000)     # Sleep 1 second

# Delayed message
sendAfter(pid, msg, delay) = {
    spawn(() => {
        sleep(delay)
        send(pid, msg)
    })
}

# Timer pattern
startTimer(duration: Int, callback: () -> ()) = {
    spawn(() => {
        sleep(duration)
        callback()
    })
}

Channel Pattern

# Implement channels using MVars
type Channel[T] = { queue: MVar[List[T]] }

newChannel() -> Channel[T] = Channel(MVar.newWith([]))

channelSend(ch: Channel[T], value: T) = {
    items = ch.queue.take()
    ch.queue.put(items ++ [value])
}

channelReceive(ch: Channel[T]) -> T = {
    items = ch.queue.take()
    match items {
        [h | t] -> {
            ch.queue.put(t)
            h
        },
        [] -> {
            ch.queue.put([])
            # Wait and retry
            sleep(10)
            channelReceive(ch)
        }
    }
}

Best Practices

# 1. Prefer message passing over shared state
# Good: send(worker, data)
# Avoid: shared mutable variables

# 2. Use timeouts to prevent deadlocks
receiveTimeout(5000) { ... }

# 3. Handle process failures
spawnLink for supervised processes

# 4. Keep messages small
# Send IDs, not large data structures

# 5. Use MVars for simple synchronization
counter = MVar.newWith(0)
increment() = {
    n = counter.take()
    counter.put(n + 1)
}

See Also


Control Flow in Nostos

If/Then/Else

# Expression form (returns a value)
max = if a > b then a else b

# Statement form with blocks
result = if condition then {
    doSomething()
    value1
} else {
    doOther()
    value2
}

# Nested conditionals
grade = if score >= 90 then "A"
        else if score >= 80 then "B"
        else if score >= 70 then "C"
        else "F"

# If without else (returns unit)
if debug then
    println("Debug mode")
else
    ()

Pattern Matching (match)

# Basic match
describe(n: Int) = match n {
    0 -> "zero",
    1 -> "one",
    _ -> "other"
}

# Match on variants
type Option[T] = Some(T) | None

getValue(opt: Option[Int]) = match opt {
    Some(x) -> x,
    None -> 0
}

# Match with guards
classify(n: Int) = match n {
    x if x < 0 -> "negative",
    0 -> "zero",
    x if x > 100 -> "large",
    _ -> "positive"
}

# Match on tuples
handlePoint((x, y)) = match (x, y) {
    (0, 0) -> "origin",
    (0, _) -> "on y-axis",
    (_, 0) -> "on x-axis",
    _ -> "elsewhere"
}

# Match on lists
describe(lst) = match lst {
    [] -> "empty",
    [x] -> "single: " ++ show(x),
    [x, y] -> "pair",
    [h | t] -> "head: " ++ show(h) ++ ", tail has " ++ show(t.length())
}

# Match on records
type Person = { name: String, age: Int }

greet(p: Person) = match p {
    { name: "Alice", age } -> "Hi Alice, you're " ++ show(age),
    { name, age } if age < 18 -> "Hello young " ++ name,
    { name, _ } -> "Hello " ++ name
}

While Loops

# Basic while loop
main() = {
    var i = 0
    while i < 5 {
        println(show(i))
        i = i + 1
    }
}

# Sum with while
sumTo(n: Int) -> Int = {
    var sum = 0
    var i = 1
    while i <= n {
        sum = sum + i
        i = i + 1
    }
    sum
}

# Early exit with return
findIndex(items: List[Int], target: Int) -> Int = {
    var i = 0
    while i < items.length() {
        if items[i] == target then
            return i
        else
            ()
        i = i + 1
    }
    -1
}

Functional Iteration (Preferred)

# Instead of while loops, prefer functional style:

# Map over a list
doubled = [1, 2, 3].map(x => x * 2)

# Filter elements
evens = [1, 2, 3, 4].filter(x => x % 2 == 0)

# Reduce/fold
sum = [1, 2, 3].fold(0, (acc, x) => acc + x)

# Range-based iteration
# Create a range and process
range(1, 5).map(x => x * x)     # [1, 4, 9, 16]
range(1, 10).filter(x => x % 2 == 0)  # [2, 4, 6, 8]

# forEach for side effects
[1, 2, 3].forEach(x => println(show(x)))

Early Return

# Return exits the function immediately
checkAge(age: Int) -> String = {
    if age < 0 then
        return "Invalid age"
    else
        ()

    if age < 18 then
        return "Minor"
    else
        ()

    "Adult"
}

# Works in nested contexts
process(items: List[Int]) -> Int = {
    var total = 0
    var i = 0
    while i < items.length() {
        item = items[i]
        if item < 0 then
            return -1  # Error: negative found
        else
            ()
        total = total + item
        i = i + 1
    }
    total
}

Blocks

# Blocks are expressions, return last value
result = {
    x = 10
    y = 20
    x + y   # This is returned
}

# Nested blocks
outer = {
    a = {
        temp = 5
        temp * 2
    }
    b = {
        temp = 3
        temp * 3
    }
    a + b   # 10 + 9 = 19
}

Match as Expression

# Match returns a value
status = match code {
    200 -> "OK",
    404 -> "Not Found",
    500 -> "Server Error",
    _ -> "Unknown"
}

# Used inline
println("Status: " ++ match code { 200 -> "OK", _ -> "Error" })

Combining Control Flow

processItems(items: List[Int]) -> String = {
    if items.length() == 0 then
        return "Empty list"
    else
        ()

    var result = ""
    var i = 0
    while i < items.length() {
        item = items[i]
        category = match item {
            x if x < 0 -> "negative",
            0 -> "zero",
            x if x > 100 -> "large",
            _ -> "normal"
        }
        result = result ++ category ++ " "
        i = i + 1
    }
    result
}

Database Operations in Nostos

PostgreSQL Connection

# Connect to PostgreSQL
conn = Pg.connect("host=localhost dbname=mydb user=postgres password=secret")

# Or with more options
conn = Pg.connect("host=localhost port=5432 dbname=mydb user=postgres password=secret sslmode=prefer")

# Close connection when done
Pg.close(conn)

Basic Queries

# Query returns list of tuples
rows = Pg.query(conn, "SELECT name, email FROM users", [])

# Access columns positionally
rows.map(row => println(row.0 ++ ": " ++ row.1))

# Parameterized queries (prevent SQL injection)
rows = Pg.query(conn, "SELECT * FROM users WHERE active = $1 AND age > $2", [true, 18])

# Single row access
firstRow = head(rows)
name = firstRow.0
email = firstRow.1

Execute (Non-Query Operations)

# INSERT, UPDATE, DELETE don't return rows
Pg.execute(conn, "INSERT INTO users (name, email) VALUES ($1, $2)", ["Alice", "alice@example.com"])

Pg.execute(conn, "UPDATE users SET active = $1 WHERE id = $2", [true, 42])

Pg.execute(conn, "DELETE FROM users WHERE id = $1", [42])

Typed Results with Introspection

Map query results to typed records using the stdlib.db module:

use stdlib.db.{rowsToRecords, rowToRecord, queryAs}

type User = { id: Int, name: String, email: String }

main() = {
    conn = Pg.connect("host=localhost user=postgres password=postgres")

    # Query and map to typed records
    rows = Pg.query(conn, "SELECT id, name, email FROM users", [])
    users: List[User] = rowsToRecords("User", rows)

    # Now use field names instead of positional access
    users.map(u => println(u.name ++ " <" ++ u.email ++ ">"))

    # Filter by field
    active = users.filter(u => u.id > 10)

    # Map by field
    emails = users.map(u => u.email)

    Pg.close(conn)
}

Important: Column order in SELECT must match field order in the type definition.

Type Conversions

PostgreSQL types map to Nostos types:

PostgreSQL Nostos
INTEGER, BIGINT Int
REAL, DOUBLE Float
TEXT, VARCHAR String
BOOLEAN Bool
JSON, JSONB String (parse with jsonParse)

Transactions

# Begin transaction
Pg.execute(conn, "BEGIN", [])

# Do work
Pg.execute(conn, "INSERT INTO orders (user_id, amount) VALUES ($1, $2)", [1, 100])
Pg.execute(conn, "UPDATE users SET balance = balance - $1 WHERE id = $2", [100, 1])

# Commit (or ROLLBACK on error)
Pg.execute(conn, "COMMIT", [])

Connection Pooling

For production apps, use a connection pool:

use stdlib.pool.*

# Create pool with max 10 connections
pool = Pool.create(10, () => Pg.connect("host=localhost user=postgres password=postgres"))

# Get connection from pool
conn = pool.acquire()

# Use connection
rows = Pg.query(conn, "SELECT * FROM users", [])

# Return to pool
pool.release(conn)

Error Handling

main() = {
    result = try {
        conn = Pg.connect("host=localhost user=postgres password=wrong")
        Pg.query(conn, "SELECT * FROM users", [])
    } catch e {
        println("Database error: " ++ e)
        []  # Return empty list on error
    }
    result
}

Prepared Statements

For repeated queries, prepare once and execute many:

# Prepare statement
Pg.execute(conn, "PREPARE get_user AS SELECT * FROM users WHERE id = $1", [])

# Execute prepared statement multiple times
user1 = Pg.query(conn, "EXECUTE get_user(1)", [])
user2 = Pg.query(conn, "EXECUTE get_user(2)", [])
user3 = Pg.query(conn, "EXECUTE get_user(3)", [])

# Deallocate when done
Pg.execute(conn, "DEALLOCATE get_user", [])

Listen/Notify (Pub/Sub)

# In subscriber process
conn1 = Pg.connect("host=localhost user=postgres password=postgres")
Pg.execute(conn1, "LISTEN my_channel", [])

# Wait for notification (blocking)
notification = Pg.waitForNotification(conn1)
println("Got: " ++ notification)

# In publisher process
conn2 = Pg.connect("host=localhost user=postgres password=postgres")
Pg.execute(conn2, "NOTIFY my_channel, 'hello'", [])

Error Handling in Nostos

Option Type

type Option[T] = Some(T) | None

# Representing missing values
findUser(id: Int) -> Option[User] = {
    if id == 1 then Some(User("Alice"))
    else None
}

# Pattern matching
result = match findUser(1) {
    Some(user) -> "Found: " ++ user.name,
    None -> "Not found"
}

Option Methods

opt = Some(42)
none: Option[Int] = None

# Map - transform the value if present
opt.map(x => x * 2)         # Some(84)
none.map(x => x * 2)        # None

# getOrElse - extract with default
opt.getOrElse(0)            # 42
none.getOrElse(0)           # 0

# isSome / isNone
opt.isSome()                # true
none.isNone()               # true

# flatMap - for chained optionals
opt.flatMap(x => if x > 0 then Some(x) else None)

Result Type

type Result[T, E] = Ok(T) | Err(E)

# Representing success or failure
parseNumber(s: String) -> Result[Int, String] = {
    if s.all(c => c.isDigit()) then
        Ok(s.parseInt())
    else
        Err("Invalid number: " ++ s)
}

# Pattern matching
result = match parseNumber("42") {
    Ok(n) -> "Parsed: " ++ show(n),
    Err(e) -> "Error: " ++ e
}

Result Methods

ok: Result[Int, String] = Ok(42)
err: Result[Int, String] = Err("failed")

# Map - transform success value
ok.map(x => x * 2)          # Ok(84)
err.map(x => x * 2)         # Err("failed")

# mapErr - transform error value
ok.mapErr(e => "Error: " ++ e)   # Ok(42)
err.mapErr(e => "Error: " ++ e)  # Err("Error: failed")

# getOrElse
ok.getOrElse(0)             # 42
err.getOrElse(0)            # 0

# isOk / isErr
ok.isOk()                   # true
err.isErr()                 # true

Chaining Results

# Sequential operations that may fail
processData(input: String) -> Result[Int, String] = {
    match parseNumber(input) {
        Ok(n) -> {
            if n > 0 then Ok(n * 2)
            else Err("Number must be positive")
        },
        Err(e) -> Err(e)
    }
}

# Using flatMap for cleaner chaining
process(input: String) -> Result[Int, String] =
    parseNumber(input)
        .flatMap(n => if n > 0 then Ok(n) else Err("Must be positive"))
        .map(n => n * 2)

Try/Catch for Exceptions

# Some operations can throw exceptions
# Use try/catch to handle them

result = try {
    riskyOperation()
} catch e {
    "Error occurred: " ++ e
}

# Catch specific patterns
result = try {
    parseAndProcess(input)
} catch {
    "parse error" -> "Invalid input format",
    "not found" -> "Resource not found",
    e -> "Unknown error: " ++ e
}

Throwing Exceptions

# Throw an exception
divide(a: Int, b: Int) -> Int = {
    if b == 0 then
        throw "Division by zero"
    else
        a / b
}

# Will be caught by try/catch
result = try {
    divide(10, 0)
} catch e {
    0  # Default value on error
}

Converting Between Types

# Option to Result
optToResult(opt: Option[T], err: E) -> Result[T, E] = match opt {
    Some(x) -> Ok(x),
    None -> Err(err)
}

# Result to Option (loses error info)
resultToOpt(res: Result[T, E]) -> Option[T] = match res {
    Ok(x) -> Some(x),
    Err(_) -> None
}

# Example
findUser(id).optToResult("User not found")

Early Return with Pattern Matching

# Extract or return early
processUser(id: Int) -> Result[String, String] = {
    user = match findUser(id) {
        Some(u) -> u,
        None -> return Err("User not found")
    }

    profile = match getProfile(user) {
        Some(p) -> p,
        None -> return Err("Profile not found")
    }

    Ok(profile.summary)
}

Collecting Results

# Process list, collect all errors or all successes
processAll(items: List[String]) -> Result[List[Int], List[String]] = {
    results = items.map(parseNumber)
    errors = results.filter(r => r.isErr()).map(r => match r { Err(e) -> e, _ -> "" })

    if errors.isEmpty() then
        Ok(results.map(r => match r { Ok(x) -> x, _ -> 0 }))
    else
        Err(errors)
}

Best Practices

# 1. Prefer Result over exceptions for expected errors
parseConfig(path: String) -> Result[Config, String]  # Good
# vs throwing exceptions                              # Avoid

# 2. Use Option for "might not exist"
findById(id: Int) -> Option[User]

# 3. Use Result for "might fail with reason"
saveUser(user: User) -> Result[(), String]

# 4. Provide context in errors
Err("Failed to parse config at line " ++ show(line) ++ ": " ++ reason)

# 5. Use early return for cleaner error handling
processRequest(req: Request) -> Result[Response, Error] = {
    user = match authenticate(req) {
        Ok(u) -> u,
        Err(e) -> return Err(AuthError(e))
    }

    data = match fetchData(user) {
        Ok(d) -> d,
        Err(e) -> return Err(DataError(e))
    }

    Ok(Response(data))
}

Custom Error Types

# Define specific error variants
type AppError =
    | NotFound(String)
    | InvalidInput(String)
    | NetworkError(String)
    | DatabaseError(String)

# Use in Result
fetchUser(id: Int) -> Result[User, AppError] = {
    if id < 0 then
        Err(InvalidInput("ID must be positive"))
    else if id > 1000 then
        Err(NotFound("User " ++ show(id)))
    else
        Ok(User("User" ++ show(id)))
}

# Handle specific errors
match fetchUser(id) {
    Ok(user) -> handleUser(user),
    Err(NotFound(msg)) -> show404(msg),
    Err(InvalidInput(msg)) -> show400(msg),
    Err(e) -> show500(show(e))
}

See Also


File I/O in Nostos

Reading Files

# Read entire file as string
content = File.readAll("config.txt")
println(content)

# Read file as lines
lines = File.readLines("data.txt")
lines.each(line => println(line))

# Read with error handling
content = try {
    File.readAll("maybe-missing.txt")
} catch e {
    println("File error: " ++ e)
    ""  # Default empty string
}

Writing Files

# Write string to file (creates or overwrites)
File.writeAll("output.txt", "Hello, World!")

# Write lines
lines = ["Line 1", "Line 2", "Line 3"]
File.writeAll("output.txt", lines.join("\n"))

# Append to file
File.append("log.txt", "New log entry\n")

File Handles (Streaming)

# Open file for reading
handle = File.open("large.txt", "r")

# Read line by line
line = File.readLine(handle)
while line != "" {
    processLine(line)
    line = File.readLine(handle)
}

# Close when done
File.close(handle)

# Open modes: "r" (read), "w" (write), "a" (append)

Safe File Operations

# Read with automatic cleanup
withFile(path: String, mode: String, fn: Handle -> T) -> T = {
    handle = File.open(path, mode)
    try {
        fn(handle)
    } finally {
        File.close(handle)
    }
}

# Usage
result = withFile("data.txt", "r", handle => {
    lines = []
    line = File.readLine(handle)
    while line != "" {
        lines = lines ++ [line]
        line = File.readLine(handle)
    }
    lines
})

File Existence and Info

# Check if file exists
if File.exists("config.json") then {
    config = File.readAll("config.json")
} else {
    config = "{}"
}

# Get file size
size = File.size("data.bin")
println("File size: " ++ show(size) ++ " bytes")

Directory Operations

# List files in directory
files = File.listDir("./data")
files.each(f => println(f))

# Create directory
File.mkdir("./output")

# Check if path is directory
if File.isDir("./data") then {
    processDirectory("./data")
}

Path Operations

# Join paths
fullPath = Path.join("data", "users", "alice.json")
# "data/users/alice.json"

# Get filename from path
name = Path.filename("/home/user/doc.txt")  # "doc.txt"

# Get directory from path
dir = Path.dirname("/home/user/doc.txt")    # "/home/user"

# Get file extension
ext = Path.extension("photo.jpg")           # "jpg"

Binary Files

# Read binary data
bytes = File.readBytes("image.png")

# Write binary data
File.writeBytes("output.bin", bytes)

# Work with typed arrays for efficiency
data = Float64Array.from([1.0, 2.0, 3.0])
File.writeBytes("floats.bin", data.toBytes())

CSV Processing

# Read CSV
parseCsv(content: String) -> List[List[String]] = {
    content.split("\n")
        .filter(line => line != "")
        .map(line => line.split(","))
}

csv = File.readAll("data.csv")
rows = parseCsv(csv)

# With header
header = rows[0]
data = rows.tail()

# Access by column name
getColumn(rows, name: String) = {
    idx = header.indexOf(name)
    data.map(row => row[idx])
}

names = getColumn(rows, "name")

JSON Configuration Files

type Config = {
    host: String,
    port: Int,
    debug: Bool
}

loadConfig(path: String) -> Config = {
    content = File.readAll(path)
    fromJson[Config](content)
}

saveConfig(path: String, config: Config) = {
    content = json.stringifyPretty(toJson(config))
    File.writeAll(path, content)
}

# Usage
config = loadConfig("config.json")
config.port = 9000
saveConfig("config.json", config)

Log File Pattern

mvar logHandle: Option[Handle] = None

initLog(path: String) = {
    logHandle = Some(File.open(path, "a"))
}

log(level: String, message: String) = {
    match logHandle {
        Some(handle) -> {
            timestamp = formatTime(currentTimeMillis())
            line = timestamp ++ " [" ++ level ++ "] " ++ message ++ "\n"
            File.write(handle, line)
        }
        None -> ()
    }
}

closeLog() = {
    match logHandle {
        Some(handle) -> {
            File.close(handle)
            logHandle = None
        }
        None -> ()
    }
}

# Usage
initLog("app.log")
log("INFO", "Application started")
log("ERROR", "Something went wrong")
closeLog()

Temporary Files

# Create temp file
tempPath = File.tempFile("prefix", ".txt")
File.writeAll(tempPath, "temporary data")

# Use temp file
data = processFile(tempPath)

# Clean up
File.delete(tempPath)

File Watching Pattern

watchFile(path: String, onChange: String -> ()) = {
    lastContent = File.readAll(path)

    loop() = {
        sleep(1000)  # Check every second
        currentContent = File.readAll(path)
        if currentContent != lastContent then {
            onChange(currentContent)
            lastContent = currentContent
        }
        loop()
    }
    spawn(loop)
}

# Usage
watchFile("config.json", newContent => {
    println("Config changed!")
    reloadConfig(newContent)
})

Parallel File Processing

# Process multiple files in parallel
processFiles(paths: List[String]) -> List[Result] = {
    me = self()

    paths.each((path, i) => spawn(() => {
        result = try {
            content = File.readAll(path)
            Ok(processContent(content))
        } catch e {
            Err(e)
        }
        send(me, (i, result))
    }))

    # Collect in order
    results = paths.map(_ => receive())
    results.sortBy(r => r.0).map(r => r.1)
}

Error Recovery

# Retry on transient errors
readWithRetry(path: String, maxRetries: Int) -> Option[String] = {
    attempt(retries) = {
        if retries <= 0 then None
        else {
            try {
                Some(File.readAll(path))
            } catch e {
                println("Read failed: " ++ e ++ ", retrying...")
                sleep(100)
                attempt(retries - 1)
            }
        }
    }
    attempt(maxRetries)
}

Functions in Nostos

Basic Function Definition

# Single expression (no braces needed)
add(a: Int, b: Int) -> Int = a + b

# Multi-statement with block
greet(name: String) -> String = {
    prefix = "Hello, "
    prefix ++ name ++ "!"
}

# Type inference (return type can be omitted)
double(x) = x * 2

# Unit return (for side effects)
logMessage(msg: String) = println(msg)

Calling Functions

result = add(2, 3)          # 5
message = greet("Alice")    # "Hello, Alice!"

# UFCS (Uniform Function Call Syntax)
# First argument can be receiver
5.double()                  # Same as double(5)
"Alice".greet()             # Same as greet("Alice")

Named and Default Parameters

# Named parameters
connect(host: String, port: Int, timeout: Int) =
    println("Connecting to " ++ host ++ ":" ++ show(port))

# Call with named arguments
connect(host: "localhost", port: 8080, timeout: 30)

# Default values
greet(name: String, greeting: String = "Hello") =
    greeting ++ ", " ++ name ++ "!"

greet("Alice")              # "Hello, Alice!"
greet("Alice", "Hi")        # "Hi, Alice!"

Closures (Anonymous Functions)

# Lambda syntax
double = x => x * 2
add = (a, b) => a + b

# Multi-statement closure
process = x => {
    y = x * 2
    y + 1
}

# Used with higher-order functions
[1, 2, 3].map(x => x * 2)           # [2, 4, 6]
[1, 2, 3].filter(x => x > 1)        # [2, 3]
[1, 2, 3].fold(0, (acc, x) => acc + x)  # 6

Closures Capturing Variables

makeCounter() = {
    var count = 0
    () => {
        count = count + 1
        count
    }
}

counter = makeCounter()
counter()   # 1
counter()   # 2
counter()   # 3

Recursion

# Simple recursion
factorial(0) = 1
factorial(n) = n * factorial(n - 1)

# Tail recursion (optimized)
factorialTail(n) = go(n, 1)
go(0, acc) = acc
go(n, acc) = go(n - 1, n * acc)

# Recursive list processing
sum([]) = 0
sum([h | t]) = h + sum(t)

Generic Functions

# Type parameter in brackets
identity[T](x: T) -> T = x

# Multiple type parameters
pair[A, B](a: A, b: B) -> (A, B) = (a, b)

# With trait bounds
printAll[T: Show](items: List[T]) =
    items.map(x => x.show()).join(", ")

Early Return

findFirst(items: List[Int], target: Int) -> Int = {
    var i = 0
    while i < items.length() {
        if items[i] == target then
            return i
        else
            ()
        i = i + 1
    }
    -1  # Not found
}

Function Composition

# Compose functions
double(x) = x * 2
addOne(x) = x + 1

# Manual composition
composed(x) = addOne(double(x))

# Using pipe-style with UFCS
result = 5.double().addOne()    # 11

# Method chaining
[1, 2, 3]
    .map(x => x * 2)
    .filter(x => x > 2)
    .fold(0, (a, b) => a + b)

Higher-Order Functions

# Function taking a function
applyTwice(f, x) = f(f(x))

applyTwice(x => x * 2, 3)   # 12

# Function returning a function
multiplier(n) = x => x * n

triple = multiplier(3)
triple(4)   # 12

# Common patterns
[1, 2, 3].map(x => x * 2)           # Transform each
[1, 2, 3].filter(x => x > 1)        # Keep matching
[1, 2, 3].fold(0, (a, b) => a + b)  # Reduce to one
[1, 2, 3].any(x => x > 2)           # true if any match
[1, 2, 3].all(x => x > 0)           # true if all match
[1, 2, 3].find(x => x > 1)          # Some(2)

See Also


HTTP in Nostos

HTTP Client

# Simple GET request
response = Http.get("https://api.example.com/users")
println(response.body)

# With headers
response = Http.get("https://api.example.com/users", %{
    "Authorization": "Bearer token123",
    "Accept": "application/json"
})

# POST with JSON body
response = Http.post("https://api.example.com/users",
    json.stringify(%{"name": "Alice", "email": "alice@example.com"}),
    %{"Content-Type": "application/json"}
)

# Other HTTP methods
Http.put(url, body, headers)
Http.patch(url, body, headers)
Http.delete(url, headers)

Response Handling

response = Http.get("https://api.example.com/data")

# Check status
if response.status == 200 then {
    data = json.parse(response.body)
    processData(data)
} else {
    println("Error: " ++ show(response.status))
}

# Response fields
response.status      # Int: HTTP status code
response.body        # String: response body
response.headers     # Map[String, String]: response headers

HTTP Server

use stdlib.server.*

# Basic server
handler(req) = {
    match req.path {
        "/" -> respondText(req, "Hello, World!")
        "/health" -> respondText(req, "OK")
        _ -> respond404(req)
    }
}

main() = serve(8080, handler)

Request Object

handler(req) = {
    # Request fields
    req.path        # String: URL path
    req.method      # String: GET, POST, etc.
    req.body        # String: request body
    req.headers     # Map[String, String]: request headers
    req.query       # Map[String, String]: query parameters
    req.id          # Int: unique request ID for responding
}

Response Helpers

use stdlib.server.*

handler(req) = {
    # Text response
    respondText(req, "Hello")

    # JSON response
    respondJson(req, %{"status": "ok", "count": 42})

    # HTML response
    respondHtml(req, "<h1>Hello</h1>")

    # Custom status and headers
    respond(req, 201, %{"X-Custom": "value"}, "Created")

    # Error responses
    respond404(req)
    respond500(req, "Internal error")
}

Route Matching

use stdlib.server.*

handler(req) = match req.path {
    "/" -> respondText(req, "Home")
    "/api/users" -> handleUsers(req)
    "/api/posts" -> handlePosts(req)
    path when path.startsWith("/static/") -> serveStatic(req)
    _ -> respond404(req)
}

# With path parameters (manual parsing)
handleUserById(req) = {
    # /users/123 -> extract "123"
    parts = req.path.split("/")
    userId = parts[2].toInt()
    user = findUser(userId)
    respondJson(req, user)
}

Query Parameters

# URL: /search?q=nostos&limit=10
handler(req) = {
    query = req.query.get("q").getOrElse("")
    limit = req.query.get("limit").map(s => s.toInt()).getOrElse(20)

    results = search(query, limit)
    respondJson(req, results)
}

JSON API Pattern

use stdlib.server.*

type User = { id: Int, name: String, email: String }

# GET /users
getUsers(req) = {
    users = fetchAllUsers()
    respondJson(req, users)
}

# POST /users
createUser(req) = {
    data = json.parse(req.body)
    user = User(
        id: generateId(),
        name: data["name"],
        email: data["email"]
    )
    saveUser(user)
    respond(req, 201, %{}, json.stringify(user))
}

# Router
handler(req) = match (req.method, req.path) {
    ("GET", "/users") -> getUsers(req)
    ("POST", "/users") -> createUser(req)
    _ -> respond404(req)
}

Middleware Pattern

# Logging middleware
withLogging(handler) = req => {
    println(req.method ++ " " ++ req.path)
    start = currentTimeMillis()
    result = handler(req)
    elapsed = currentTimeMillis() - start
    println("Completed in " ++ show(elapsed) ++ "ms")
    result
}

# Auth middleware
withAuth(handler) = req => {
    token = req.headers.get("Authorization")
    match token {
        Some(t) when isValidToken(t) -> handler(req)
        _ -> respond(req, 401, %{}, "Unauthorized")
    }
}

# Compose middlewares
main() = {
    handler = withLogging(withAuth(apiHandler))
    serve(8080, handler)
}

Error Handling

handler(req) = {
    try {
        data = processRequest(req)
        respondJson(req, data)
    } catch {
        "not found" -> respond404(req)
        "unauthorized" -> respond(req, 401, %{}, "Unauthorized")
        e -> respond500(req, "Error: " ++ e)
    }
}

Concurrent Requests

# Fetch multiple URLs in parallel
fetchAll(urls: List[String]) -> List[String] = {
    me = self()

    # Spawn request for each URL
    urls.each((url, i) => spawn(() => {
        response = Http.get(url)
        send(me, (i, response.body))
    }))

    # Collect results in order
    results = urls.map(_ => receive())
    results.sortBy(r => r.0).map(r => r.1)
}

Graceful Shutdown

mvar running: Bool = true

handler(req) = match req.path {
    "/shutdown" -> {
        running = false
        respondText(req, "Shutting down...")
    }
    _ -> respondText(req, "Hello")
}

serverLoop(handle) = {
    if running then {
        req = Server.accept(handle)
        spawn(() => handler(req))
        serverLoop(handle)
    } else {
        Server.close(handle)
    }
}

main() = {
    handle = Server.bind(8080)
    println("Server running on :8080")
    serverLoop(handle)
}

CORS Headers

withCors(handler) = req => {
    corsHeaders = %{
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
        "Access-Control-Allow-Headers": "Content-Type, Authorization"
    }

    if req.method == "OPTIONS" then
        respond(req, 204, corsHeaders, "")
    else {
        response = handler(req)
        # Add CORS headers to response
        response
    }
}

JSON in Nostos

Parsing JSON

# Parse JSON string to dynamic value
data = json.parse('{"name": "Alice", "age": 30}')

# Access fields with bracket syntax
name = data["name"]     # "Alice"
age = data["age"]       # 30

# Nested access
config = json.parse('{"server": {"host": "localhost", "port": 8080}}')
host = config["server"]["host"]  # "localhost"

# Arrays
items = json.parse('[1, 2, 3, 4, 5]')
first = items[0]        # 1

Generating JSON

# Convert map to JSON string
data = %{"name": "Bob", "active": true, "scores": [95, 87, 92]}
jsonStr = json.stringify(data)
# '{"name":"Bob","active":true,"scores":[95,87,92]}'

# Pretty print
prettyJson = json.stringifyPretty(data)

Type-Safe JSON with Records

use stdlib.json.*

type User = { name: String, email: String, age: Int }

# Parse JSON directly to typed record
jsonStr = '{"name": "Alice", "email": "alice@example.com", "age": 30}'
user: User = fromJson[User](jsonStr)

println(user.name)   # "Alice"
println(user.age)    # 30

# Convert record to JSON
userJson = toJson(user)

Handling Variants

type Status = Active | Inactive(String) | Pending { since: Int }

# Variant serialization
statusJson = toJson(Active)           # {"variant": "Active"}
statusJson = toJson(Inactive("left")) # {"variant": "Inactive", "value": "left"}
statusJson = toJson(Pending { since: 1234 })
# {"variant": "Pending", "fields": {"since": 1234}}

# Parse back to variant
status: Status = fromJson[Status]('{"variant": "Active"}')

Nested Records

type Address = { street: String, city: String }
type Person = { name: String, address: Address }

jsonStr = '''
{
    "name": "Alice",
    "address": {
        "street": "123 Main St",
        "city": "Oslo"
    }
}
'''

person: Person = fromJson[Person](jsonStr)
println(person.address.city)  # "Oslo"

Lists of Records

type Product = { id: Int, name: String, price: Float }

jsonStr = '''
[
    {"id": 1, "name": "Widget", "price": 9.99},
    {"id": 2, "name": "Gadget", "price": 19.99}
]
'''

products: List[Product] = fromJson[List[Product]](jsonStr)
products.each(p => println(p.name ++ ": $" ++ show(p.price)))

Optional Fields

type Config = {
    host: String,
    port: Int,
    debug: Option[Bool]  # Optional field
}

# JSON without optional field
config1: Config = fromJson[Config]('{"host": "localhost", "port": 8080}')
config1.debug  # None

# JSON with optional field
config2: Config = fromJson[Config]('{"host": "localhost", "port": 8080, "debug": true}')
config2.debug  # Some(true)

Error Handling

# Safe parsing
result = try {
    data = json.parse(inputStr)
    Some(data)
} catch e {
    println("Parse error: " ++ e)
    None
}

# Type-safe parsing with Result
parseUser(jsonStr: String) -> Result[User, String] = {
    try {
        Ok(fromJson[User](jsonStr))
    } catch e {
        Err("Failed to parse user: " ++ e)
    }
}

Dynamic JSON Access

# Check field existence
data = json.parse('{"name": "Alice"}')
hasEmail = data.contains("email")  # false

# Get with default
email = data.get("email").getOrElse("no-email@example.com")

# Iterate over object fields
data = json.parse('{"a": 1, "b": 2, "c": 3}')
data.keys().each(key => println(key ++ ": " ++ show(data[key])))

JSON Path-like Access

# Deep access helper
getPath(data, path: List[String]) = {
    path.fold(data, (current, key) => current[key])
}

config = json.parse('''
{
    "database": {
        "connection": {
            "host": "localhost"
        }
    }
}
''')

host = getPath(config, ["database", "connection", "host"])
# "localhost"

Building JSON Dynamically

# Start with empty map
builder = %{}

# Add fields
builder["name"] = "Alice"
builder["age"] = 30
builder["tags"] = ["developer", "nostos"]

# Convert to JSON string
json.stringify(builder)

JSON API Response Pattern

type ApiResponse[T] = {
    success: Bool,
    data: Option[T],
    error: Option[String]
}

successResponse(data) = ApiResponse {
    success: true,
    data: Some(data),
    error: None
}

errorResponse(msg: String) = ApiResponse {
    success: false,
    data: None,
    error: Some(msg)
}

# In handler
handler(req) = {
    result = try {
        data = processRequest(req)
        successResponse(data)
    } catch e {
        errorResponse(e)
    }
    respondJson(req, toJson(result))
}

JSON Transformation

# Transform JSON structure
transformUser(data) = %{
    "fullName": data["firstName"] ++ " " ++ data["lastName"],
    "contact": %{
        "email": data["email"],
        "phone": data["phone"]
    }
}

input = json.parse('{"firstName": "Alice", "lastName": "Smith", "email": "a@b.com", "phone": "123"}')
output = transformUser(input)
json.stringify(output)

Round-Trip Testing

# Verify JSON encode/decode preserves data
testRoundTrip(value: T) -> Bool = {
    encoded = toJson(value)
    decoded: T = fromJson[T](encoded)
    decoded == value
}

# Test
user = User { name: "Alice", email: "a@b.com", age: 30 }
assert(testRoundTrip(user))

JSON with Reflection

# Get type info at runtime
info = typeInfo("User")
fields = info["fields"]  # List of field definitions

# Dynamic record construction from JSON
buildFromJson(typeName: String, jsonStr: String) = {
    data = json.parse(jsonStr)
    makeRecordByName(typeName, data)
}

Lists in Nostos

Creating Lists

# Empty list
empty = []

# List with elements
numbers = [1, 2, 3, 4, 5]
strings = ["a", "b", "c"]
mixed = [1, "two", true]   # Heterogeneous (not recommended)

# Type annotation
nums: List[Int] = [1, 2, 3]

Cons Operator (Prepend)

# | prepends element to list
list = [1 | [2, 3]]         # [1, 2, 3]
list = [0 | [1, 2, 3]]      # [0, 1, 2, 3]

# Building lists
addFront(x, lst) = [x | lst]
addFront(0, [1, 2])         # [0, 1, 2]

Pattern Matching on Lists

# Match empty vs non-empty
describe([]) = "empty"
describe([h | t]) = "head: " ++ show(h)

# Match specific lengths
handleList([]) = "empty"
handleList([x]) = "single: " ++ show(x)
handleList([x, y]) = "pair: " ++ show(x) ++ ", " ++ show(y)
handleList(_) = "many elements"

# Extract head and tail
firstTwo([a, b | _]) = (a, b)

Basic Operations

lst = [1, 2, 3, 4, 5]

# Length
lst.length()            # 5

# Access by index (0-based)
lst[0]                  # 1
lst[2]                  # 3
lst.get(0)              # 1

# Head and tail
lst.head()              # Some(1)
lst.tail()              # [2, 3, 4, 5]
lst.last()              # Some(5)

# Check empty
lst.isEmpty()           # false
[].isEmpty()            # true

Transformations

# Map: transform each element
[1, 2, 3].map(x => x * 2)           # [2, 4, 6]
["a", "b"].map(s => s.toUpper())    # ["A", "B"]

# Filter: keep matching elements
[1, 2, 3, 4].filter(x => x % 2 == 0)    # [2, 4]
["apple", "banana", "apricot"].filter(s => s.startsWith("a"))  # ["apple", "apricot"]

# Fold/Reduce: combine into single value
[1, 2, 3, 4].fold(0, (acc, x) => acc + x)   # 10 (sum)
[1, 2, 3, 4].fold(1, (acc, x) => acc * x)   # 24 (product)

More List Methods

# Take/Drop
[1, 2, 3, 4, 5].take(3)     # [1, 2, 3]
[1, 2, 3, 4, 5].drop(2)     # [3, 4, 5]

# Reverse
[1, 2, 3].reverse()         # [3, 2, 1]

# Concatenation
[1, 2] ++ [3, 4]            # [1, 2, 3, 4]
[[1, 2], [3, 4]].flatten()  # [1, 2, 3, 4]

# Sort
[3, 1, 4, 1, 5].sort()      # [1, 1, 3, 4, 5]

# Unique
[1, 2, 2, 3, 3, 3].unique() # [1, 2, 3]

Searching

lst = [10, 20, 30, 40]

# Find first match
lst.find(x => x > 15)       # Some(20)
lst.find(x => x > 100)      # None

# Check existence
lst.any(x => x > 30)        # true
lst.all(x => x > 5)         # true
lst.contains(20)            # true

# Index of
lst.indexOf(30)             # 2
lst.indexOf(99)             # -1

FlatMap

# Map then flatten
[[1, 2], [3, 4]].flatMap(lst => lst.map(x => x * 2))
# [2, 4, 6, 8]

# Useful for optional results
users.flatMap(u => u.email)  # Skips None values

Zipping

# Combine two lists element-wise
zip([1, 2, 3], ["a", "b", "c"])  # [(1, "a"), (2, "b"), (3, "c")]

# Zip with function
zipWith((a, b) => a + b, [1, 2], [10, 20])  # [11, 22]

Partitioning

# Split by predicate
(evens, odds) = [1, 2, 3, 4, 5].partition(x => x % 2 == 0)
# evens = [2, 4], odds = [1, 3, 5]

# Group by
words = ["apple", "banana", "apricot", "blueberry"]
words.groupBy(w => w.charAt(0))
# {'a': ["apple", "apricot"], 'b': ["banana", "blueberry"]}

Recursive List Functions

# Sum
sum([]) = 0
sum([h | t]) = h + sum(t)

# Length
len([]) = 0
len([_ | t]) = 1 + len(t)

# Map (manual implementation)
myMap([], _) = []
myMap([h | t], f) = [f(h) | myMap(t, f)]

# Filter (manual)
myFilter([], _) = []
myFilter([h | t], p) =
    if p(h) then [h | myFilter(t, p)]
    else myFilter(t, p)

Method Chaining

# Fluent API style
result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .filter(x => x % 2 == 0)      # [2, 4, 6, 8, 10]
    .map(x => x * x)              # [4, 16, 36, 64, 100]
    .take(3)                      # [4, 16, 36]
    .fold(0, (a, b) => a + b)     # 56

# Processing data
orders
    .filter(o => o.status == "completed")
    .map(o => o.total)
    .fold(0.0, (a, b) => a + b)

Ranges

# Create a range
range(1, 5)         # [1, 2, 3, 4]
range(0, 10)        # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# With step
rangeStep(0, 10, 2) # [0, 2, 4, 6, 8]

# Common patterns
range(1, n + 1).map(x => x * x)  # Squares from 1 to n
range(0, n).fold(1, (acc, _) => acc * base)  # Power

ForEach (Side Effects)

# When you need side effects
[1, 2, 3].forEach(x => println(show(x)))

# vs map which returns values
results = [1, 2, 3].map(x => {
    println("Processing: " ++ show(x))
    x * 2
})

Modules in Nostos

File Structure

project/
├── main.nos          # Entry point with main()
├── utils.nos         # Module file
├── models/
│   ├── user.nos      # Nested module
│   └── order.nos
└── nostos.toml       # Optional project config

Importing Modules

# Import a module (file in same directory)
import utils

# Use qualified names
utils.helper()
utils.Config

# Import nested module
import models.user

models.user.createUser("Alice")

Using Imports

# Import and bring names into scope
import utils
use utils.*             # Import all public names

helper()                # Can use directly now
Config                  # Type is available

# Selective import
use utils.{helper, Config}

# Alias imports
import models.user as u
u.createUser("Alice")

Public vs Private

# In utils.nos:

# Public function (accessible from other modules)
pub helper() = "I'm public"

# Public type
pub type Config = { name: String, value: Int }

# Private function (only in this module)
internalHelper() = "I'm private"

# Private type
type InternalState = { data: List[Int] }

Exporting

# Everything marked `pub` is exported

# Public function
pub greet(name: String) = "Hello, " ++ name

# Public type
pub type User = { name: String, email: String }

# Public constant
pub const MAX_USERS = 100

# Public trait
pub trait Serializable
    serialize(self) -> String
end

Module Example

# math_utils.nos

pub const PI = 3.14159

pub square(x: Float) = x * x

pub cube(x: Float) = x * x * x

pub circleArea(radius: Float) = PI * square(radius)

# Private helper
clamp(x, min, max) = {
    if x < min then min
    else if x > max then max
    else x
}

pub clampedSquare(x: Float) = square(clamp(x, 0.0, 100.0))
# main.nos

import math_utils
use math_utils.*

main() = {
    println("PI = " ++ show(PI))
    println("5^2 = " ++ show(square(5.0)))
    println("Circle area = " ++ show(circleArea(3.0)))
}

Standard Library Imports

# The stdlib is auto-imported, but you can be explicit
import json
import io
import url
import logging

# Using stdlib
json.parse('{"key": "value"}')
io.readFile("data.txt")

Circular Dependencies

# Nostos handles circular imports
# But prefer avoiding them for clarity

# a.nos
import b
pub aFunc() = b.bFunc() + 1

# b.nos
import a
pub bFunc() = 10
# Avoid calling a.aFunc() here - would cause infinite loop

Project Configuration (nostos.toml)

[project]
name = "myproject"
version = "0.1.0"

[dependencies]
# External packages (from GitHub)
nalgebra = { git = "https://github.com/user/nalgebra-nos" }

[extensions]
# Native extensions
glam = { version = "0.1.0" }

Visibility Rules

# Public items are accessible from anywhere
pub type PublicType = { data: Int }
pub publicFunc() = 42

# Private items only in same module
type PrivateType = { secret: String }
privateFunc() = "hidden"

# Trait implementations follow the type's visibility
PublicType: Show
    show(self) = show(self.data)
end

Nested Modules

# models/user.nos
pub type User = { id: Int, name: String }

pub createUser(name: String) -> User = User(0, name)

pub validateEmail(email: String) -> Bool =
    email.contains("@")
# main.nos
import models.user
use models.user.{User, createUser}

main() = {
    user = createUser("Alice")
    println(user.name)
}

Re-exporting

# lib.nos - re-export from submodules

import models.user
import models.order
import utils

# Re-export for convenient access
pub use models.user.User
pub use models.order.Order
pub use utils.helper
# main.nos
import lib
use lib.*

# Can now use User, Order, helper directly
user = User(1, "Alice")

Module Initialization

# Code at module level runs when imported
# Use for initialization

# config.nos
println("Config module loaded")  # Runs on import

pub const DEBUG = true
pub var connectionCount = 0      # Module-level mutable state

Best Practices

# 1. One module = one responsibility
# Good: user.nos for user-related code
# Bad: utils.nos with everything

# 2. Explicit exports
pub type User = ...     # Document public API

# 3. Use qualified names for clarity
import json
json.parse(data)        # Clear where parse comes from

# 4. Group related imports
import models.user
import models.order
import models.product

use models.user.User
use models.order.Order
use models.product.Product

Reactive Programming in Nostos

Reactive Records

Reactive records automatically track changes to their fields:

# Define a reactive type
reactive Counter = { value: Int }

main() = {
    counter = Counter { value: 0 }

    # Register change listener
    counter.onChange("value", (old, new) => {
        println("Value changed: " ++ show(old) ++ " -> " ++ show(new))
    })

    # Changes trigger the callback
    counter.value = 1   # "Value changed: 0 -> 1"
    counter.value = 5   # "Value changed: 1 -> 5"
}

Multiple Callbacks

reactive State = { count: Int, name: String }

main() = {
    state = State { count: 0, name: "initial" }

    # Multiple callbacks on same field
    state.onChange("count", (_, new) => println("Logger: count = " ++ show(new)))
    state.onChange("count", (_, new) => updateUI(new))

    # Callbacks on different fields
    state.onChange("name", (_, new) => println("Name changed to: " ++ new))

    state.count = 42    # Both count callbacks fire
    state.name = "updated"  # Name callback fires
}

Change History Tracking

reactive Document = { title: String, content: String }

mvar history: List[(String, String, String)] = []

trackChanges(doc: Document) = {
    doc.onChange("title", (old, new) => {
        history = history ++ [("title", old, new)]
    })
    doc.onChange("content", (old, new) => {
        history = history ++ [("content", old, new)]
    })
}

main() = {
    doc = Document { title: "Untitled", content: "" }
    trackChanges(doc)

    doc.title = "My Document"
    doc.content = "Hello, world!"
    doc.title = "My Great Document"

    # Print change history
    history.each((field, old, new) => {
        println(field ++ ": '" ++ old ++ "' -> '" ++ new ++ "'")
    })
}

Reactive Variants

reactive Status = Idle | Loading | Success(String) | Error(String)

main() = {
    status = Idle

    # Track state transitions
    status.onChange((old, new) => {
        println("Status: " ++ show(old) ++ " -> " ++ show(new))
    })

    status.set(Loading)           # "Status: Idle -> Loading"
    status.set(Success("Done"))   # "Status: Loading -> Success(Done)"
}

State Machine Pattern

reactive ConnectionState =
    | Disconnected
    | Connecting
    | Connected { sessionId: String }
    | Reconnecting { attempt: Int }

handleConnection(state: ConnectionState) = {
    state.onChange((old, new) => {
        match new {
            Connected { sessionId } -> {
                println("Connected! Session: " ++ sessionId)
                startHeartbeat()
            }
            Disconnected -> {
                println("Disconnected")
                stopHeartbeat()
            }
            Reconnecting { attempt } -> {
                println("Reconnecting... attempt " ++ show(attempt))
            }
            _ -> ()
        }
    })
}

RWeb: Reactive Web Framework

use stdlib.rweb.*

# Define reactive state for the page
reactive AppState = { count: Int, message: String }

# Session setup function
sessionSetup(writerId) = {
    state = AppState { count: 0, message: "Welcome!" }

    # Render function - called on state changes
    renderPage = () => RHtml(div([
        h1(state.message),
        p("Count: " ++ show(state.count)),
        button("Increment", dataAction: "increment"),
        button("Reset", dataAction: "reset")
    ]))

    # Action handler
    onAction = (action, params) => match action {
        "increment" -> { state.count = state.count + 1 }
        "reset" -> { state.count = 0 }
        _ -> ()
    }

    (renderPage, onAction)
}

main() = startRWeb(8080, "Counter App", sessionSetup)

Component Pattern

use stdlib.rweb.*

# Reusable counter component
counterComponent(state, fieldName: String) = {
    count = state[fieldName]

    component("counter-" ++ fieldName, () => RHtml(div(class: "counter", [
        span(show(count)),
        button("+", dataAction: "inc-" ++ fieldName),
        button("-", dataAction: "dec-" ++ fieldName)
    ])))
}

# Use in page
renderPage = () => RHtml(div([
    h1("Multi-Counter"),
    counterComponent(state, "counter1"),
    counterComponent(state, "counter2"),
    counterComponent(state, "counter3")
]))

Form Handling

use stdlib.rweb.*

reactive FormState = {
    username: String,
    email: String,
    errors: Map[String, String]
}

validate(state: FormState) = {
    errors = %{}
    if state.username.length() < 3 then {
        errors["username"] = "Username too short"
    }
    if !state.email.contains("@") then {
        errors["email"] = "Invalid email"
    }
    state.errors = errors
    errors.isEmpty()
}

sessionSetup(writerId) = {
    state = FormState { username: "", email: "", errors: %{} }

    renderPage = () => RHtml(form([
        div([
            label("Username:"),
            input(name: "username", value: state.username),
            errorMsg(state.errors["username"])
        ]),
        div([
            label("Email:"),
            input(name: "email", value: state.email),
            errorMsg(state.errors["email"])
        ]),
        button("Submit", dataAction: "submit")
    ]))

    onAction = (action, params) => match action {
        "submit" -> {
            state.username = params["username"]
            state.email = params["email"]
            if validate(state) then {
                saveUser(state.username, state.email)
            }
        }
        _ -> ()
    }

    (renderPage, onAction)
}

Parent/Child Introspection

reactive Parent = { children: List[Child] }
reactive Child = { name: String, value: Int }

main() = {
    parent = Parent { children: [] }
    child1 = Child { name: "first", value: 10 }
    child2 = Child { name: "second", value: 20 }

    parent.children = [child1, child2]

    # Check what holds a reference to child1
    parents = child1.parents
    println("child1 is held by " ++ show(parents.length()) ++ " parent(s)")

    # Get all children
    allChildren = parent.children
    println("Parent has " ++ show(allChildren.length()) ++ " children")
}

Computed Values Pattern

reactive Cart = { items: List[CartItem] }
reactive CartItem = { name: String, price: Float, quantity: Int }

# Computed total (not reactive, but derived)
cartTotal(cart: Cart) -> Float = {
    cart.items.fold(0.0, (sum, item) => {
        sum + (item.price * item.quantity.toFloat())
    })
}

# Re-render when items change
cart.onChange("items", (_, _) => {
    total = cartTotal(cart)
    updateTotalDisplay(total)
})

Debounced Updates

reactive SearchState = { query: String, results: List[String] }

# Debounce search to avoid too many API calls
debounce(ms: Int, fn: () -> ()) = {
    mvar timer: Option[Pid] = None

    () => {
        # Cancel previous timer
        match timer {
            Some(pid) -> send(pid, "cancel")
            None -> ()
        }

        # Start new timer
        me = self()
        newTimer = spawn(() => {
            match receiveTimeout(ms) {
                None -> send(me, "fire")  # Timeout = execute
                Some("cancel") -> ()       # Cancelled
            }
        })
        timer = Some(newTimer)
    }
}

main() = {
    state = SearchState { query: "", results: [] }
    debouncedSearch = debounce(300, () => {
        state.results = searchApi(state.query)
    })

    state.onChange("query", (_, _) => debouncedSearch())
}

Undo/Redo Pattern

reactive Editor = { content: String }

type EditorHistory = {
    past: List[String],
    future: List[String]
}

mvar history: EditorHistory = EditorHistory { past: [], future: [] }

trackHistory(editor: Editor) = {
    editor.onChange("content", (old, new) => {
        history = EditorHistory {
            past: history.past ++ [old],
            future: []  # Clear redo stack on new edit
        }
    })
}

undo(editor: Editor) = {
    match history.past {
        [] -> ()
        [..rest, last] -> {
            current = editor.content
            editor.content = last  # This won't trigger onChange for history
            history = EditorHistory {
                past: rest,
                future: [current] ++ history.future
            }
        }
    }
}

redo(editor: Editor) = {
    match history.future {
        [] -> ()
        [next, ..rest] -> {
            current = editor.content
            editor.content = next
            history = EditorHistory {
                past: history.past ++ [current],
                future: rest
            }
        }
    }
}

Strings in Nostos

String Literals

# Double quotes
greeting = "Hello, World!"

# Single quotes (useful for embedded double quotes)
json = '{"name": "Alice", "age": 30}'

# Escape sequences
newline = "Line 1\nLine 2"
tab = "Col1\tCol2"
quote = "She said \"Hello\""
backslash = "C:\\path\\file"

String Concatenation

# Use ++ operator
full = "Hello" ++ " " ++ "World"

# Building strings
name = "Alice"
age = 30
message = "Name: " ++ name ++ ", Age: " ++ show(age)

# Multi-part concatenation
result = "Part 1" ++
         " Part 2" ++
         " Part 3"

Converting to String

# show() converts any value to string
show(42)           # "42"
show(3.14)         # "3.14"
show(true)         # "true"
show([1, 2, 3])    # "[1, 2, 3]"

# Common pattern
println("Value: " ++ show(someValue))

String Methods

s = "Hello, World!"

# Length
s.length()          # 13

# Case conversion
s.toUpper()         # "HELLO, WORLD!"
s.toLower()         # "hello, world!"

# Substring
s.substring(0, 5)   # "Hello"
s.substring(7, 12)  # "World"

# Contains/starts/ends
s.contains("World")     # true
s.startsWith("Hello")   # true
s.endsWith("!")         # true

# Finding
s.indexOf("World")      # 7 (or -1 if not found)

# Trimming whitespace
"  hello  ".trim()      # "hello"
"  hello  ".trimStart() # "hello  "
"  hello  ".trimEnd()   # "  hello"

Split and Join

# Split string into list
"a,b,c".split(",")          # ["a", "b", "c"]
"hello world".split(" ")     # ["hello", "world"]

# Join list into string
["a", "b", "c"].join(",")   # "a,b,c"
["hello", "world"].join(" ") # "hello world"

# Useful pattern
csv = "1,2,3,4,5"
numbers = csv.split(",").map(s => parseInt(s))

Replace

# Replace first occurrence
"hello world".replace("world", "there")  # "hello there"

# Replace all occurrences
"ababa".replaceAll("a", "x")  # "xbxbx"

Character Access

s = "Hello"

# Get character at index
s.charAt(0)         # 'H'
s.charAt(4)         # 'o'

# Get as list of characters
s.chars()           # ['H', 'e', 'l', 'l', 'o']

String Comparison

"abc" == "abc"      # true
"abc" != "def"      # true
"abc" < "abd"       # true (lexicographic)
"ABC" < "abc"       # true (uppercase < lowercase)

Parsing Strings

# Parse to Int
"42".parseInt()     # 42

# Parse to Float
"3.14".parseFloat() # 3.14

# Safe parsing (returns Result)
"42".tryParseInt()      # Ok(42)
"invalid".tryParseInt() # Err("...")

Multiline Strings

# Use regular strings with \n
multiline = "Line 1\nLine 2\nLine 3"

# Or concatenation for readability
poem = "Roses are red,\n" ++
       "Violets are blue,\n" ++
       "Nostos is fun,\n" ++
       "And so are you!"

String Building Pattern

# Building a string incrementally
buildReport(items: List[String]) -> String = {
    var result = "Report:\n"
    var i = 0
    while i < items.length() {
        result = result ++ "- " ++ items[i] ++ "\n"
        i = i + 1
    }
    result
}

# Functional style (preferred)
buildReport(items: List[String]) -> String =
    "Report:\n" ++ items.map(s => "- " ++ s).join("\n")

URL Encoding

import url

# Encode special characters
url.encode("hello world")   # "hello%20world"
url.encode("a=b&c=d")       # "a%3Db%26c%3Dd"

# Decode
url.decode("hello%20world") # "hello world"

Common Patterns

# Check if empty
s.length() == 0
s == ""

# Default for empty
name = if input == "" then "Anonymous" else input

# Format number with padding
padLeft(s: String, len: Int, char: String) -> String = {
    if s.length() >= len then s
    else padLeft(char ++ s, len, char)
}

padLeft(show(42), 5, "0")   # "00042"

# Repeat string
repeat(s: String, n: Int) -> String = {
    if n <= 0 then ""
    else s ++ repeat(s, n - 1)
}

repeat("ab", 3)   # "ababab"

Templates & Metaprogramming in Nostos

Templates let you write code that generates code at compile time. This is powerful for eliminating boilerplate, creating DSLs, and building reusable patterns.

Core Concepts

Templates are compile-time functions that manipulate code as data: - quote { code } - captures code as AST (not executed, just stored) - ~expr - splices an AST value into a quote (inserts the code) - @decorator - applies a template to a function or type

Function Decorators

Decorators wrap or transform functions at compile time:

# Double the return value of any function
template double(fn) = quote {
    result = ~fn.body    # splice in the original function body
    result * 2
}

@double
getValue() = 21

main() = getValue()  # Returns 42 (21 * 2)

Available function metadata: - ~fn.name - function name as String - ~fn.body - the function body (AST) - ~fn.params - list of parameters with name/type - ~fn.returnType - return type as String

Type Decorators

Generate code based on type structure:

# Auto-generate getters for all fields
template withGetters(typeDef) = quote {
    ~typeDef
    ~typeDef.fields.map(f =>
        eval("get_" ++ f.name ++ "(r: " ++ ~typeDef.name ++ ") = r." ++ f.name))
}

@withGetters
type Point = Point { x: Int, y: Int }

main() = {
    p = Point(x: 10, y: 20)
    get_x(p) + get_y(p)  # 30
}

Type introspection: - ~typeDef.name - type name as String - ~typeDef.fields - list of fields (for single-constructor types) - ~typeDef.fields[i].name - field name at index - ~typeDef.fields[i].ty - field type at index (note: ty not type)

Code Generation with eval

eval("code") parses a string as code at compile time:

# Generate a function dynamically
template makeAdder(typeDef, amount) = quote {
    ~typeDef
    ~eval("add" ++ ~amount ++ "(x: Int) = x + " ++ ~amount)
}

@makeAdder("10")
type Config = Config {}

main() = add10(5)  # 15

Compile-Time Conditionals

Use ~if for conditional code generation:

template maybeLog(fn, shouldLog) = quote {
    ~if ~shouldLog {
        quote {
            println("Calling " ++ ~fn.name)
            ~fn.body
        }
    } else {
        quote { ~fn.body }
    }
}

@maybeLog(true)
debug() = 42      # prints "Calling debug", returns 42

@maybeLog(false)
release() = 42    # just returns 42, no logging

Exception Handling in Templates

Add try/catch for error handling patterns:

# Wrap any function with a fallback value
template withFallback(fn, fallback) = quote {
    try {
        ~fn.body
    } catch {
        _ -> ~fallback
    }
}

@withFallback("error")
risky() = throw("oops")

main() = risky()  # Returns "error" instead of throwing

Retry Pattern

Retry failed operations multiple times:

template retry3(fn) = quote {
    try { ~fn.body } catch {
        _ -> try { ~fn.body } catch {
            _ -> ~fn.body
        }
    }
}

mvar attempts: Int = 0

@retry3
flaky() = {
    attempts = attempts + 1
    if attempts < 3 { throw("not ready") }
    "success"
}

main() = flaky()  # Succeeds on 3rd attempt

Unique Identifiers with gensym

Avoid naming collisions in generated code:

template withHelper(typeDef) = quote {
    ~typeDef
    ~eval(~gensym("helper") ++ "() = 42")
}

@withHelper
type A = A {}

@withHelper
type B = B {}

# Generates helper_0() and helper_1() - no collision!
main() = helper_0() + helper_1()  # 84

Parameter Access with param()

Reference function parameters with ~param(n):

template validatePositive(fn) = quote {
    if ~param(0) <= 0 {
        throw(~fn.name ++ ": must be positive")
    }
    ~fn.body
}

@validatePositive
sqrt(n: Int) = n * n

main() = sqrt(5)    # 25
# sqrt(-1) would throw "sqrt: must be positive"

The ~param(n) shorthand is equivalent to ~toVar(fn.params[n].name).

Compile-Time Computation

Execute code at compile time with comptime:

# String syntax
template withDefault(fn, expr) = quote {
    value = ~comptime(~expr)  # evaluated at compile time
    ~fn.body + value
}

@withDefault("21 * 2")
add(x: Int) = x

main() = add(0)  # 42

# Block syntax
template computed(fn, useSquare) = quote {
    ~comptime({
        base = 10
        if ~useSquare { base * base } else { base * 2 }
    })
}

Practical Patterns

Logging Decorator

template logged(fn) = quote {
    println(">>> " ++ ~fn.name)
    result = ~fn.body
    println("<<< " ++ ~fn.name)
    result
}

Builder Pattern

template builder(typeDef) = quote {
    ~typeDef
    ~typeDef.fields.map(f =>
        eval("with_" ++ f.name ++ "(r: " ++ ~typeDef.name ++ ", v: " ++ f.ty ++ ") = " ++
             ~typeDef.name ++ "(" ++ f.name ++ ": v)"))
}

@builder
type Config = Config { timeout: Int, debug: Bool }

# Generates: with_timeout(r, v), with_debug(r, v)

Feature Flags

template featureFlag(fn, enabled, msg) = quote {
    ~if ~enabled {
        quote { ~fn.body }
    } else {
        quote { throw(~msg) }
    }
}

@featureFlag(true, "Beta disabled")
betaFeature() = "works!"

@featureFlag(false, "Experimental disabled")
experimental() = "never runs"

Key Points for Code Generation

  1. Templates run at compile time - no runtime overhead
  2. quote captures code - doesn't execute it
  3. ~ splices values - inserts AST into quotes
  4. eval parses strings - for dynamic function names
  5. gensym creates unique names - prevents collisions
  6. toVar references parameters - for validation patterns
  7. ~if generates conditionally - compile-time branching
  8. comptime executes early - pre-compute values
  9. try/catch works in templates - for error handling patterns

See Also


Testing in Nostos

Basic Assertions

# Assert condition is true
assert(1 + 1 == 2)

# Assert with message
assert(user.age >= 18, "User must be adult")

# Assert equality
assert_eq(actual, expected)
assert_eq(sum([1, 2, 3]), 6)

# Assert not equal
assert_ne(a, b)

Test File Format

Test files use special comments to specify expected behavior:

# expect: 42
# This test expects main() to return 42

main() = 21 * 2
# expect_error: type mismatch
# This test expects a compilation error containing "type mismatch"

main() = "hello" + 5

Organizing Tests

# Group related assertions
testArithmetic() = {
    assert_eq(1 + 1, 2)
    assert_eq(10 - 3, 7)
    assert_eq(4 * 5, 20)
    assert_eq(15 / 3, 5)
    println("Arithmetic tests passed!")
}

testStrings() = {
    assert_eq("hello" ++ " world", "hello world")
    assert_eq("abc".length(), 3)
    assert_eq("HELLO".toLower(), "hello")
    println("String tests passed!")
}

main() = {
    testArithmetic()
    testStrings()
    0
}

Testing with Setup/Teardown

# Setup helper
withTestData(test: List[Int] -> T) -> T = {
    data = [1, 2, 3, 4, 5]  # Setup
    result = test(data)
    # Cleanup (if needed)
    result
}

# Usage
main() = {
    withTestData(data => {
        assert_eq(data.length(), 5)
        assert_eq(data.sum(), 15)
    })
    0
}

Testing Option/Result

# Test Option values
testOption() = {
    some = Some(42)
    none: Option[Int] = None

    assert(some.isSome())
    assert(none.isNone())
    assert_eq(some.getOrElse(0), 42)
    assert_eq(none.getOrElse(0), 0)
}

# Test Result values
testResult() = {
    ok: Result[Int, String] = Ok(42)
    err: Result[Int, String] = Err("failed")

    assert(ok.isOk())
    assert(err.isErr())
    assert_eq(ok.getOrElse(0), 42)
    assert_eq(err.getOrElse(0), 0)
}

Testing Exceptions

# Test that exception is thrown
testThrows() = {
    threw = try {
        divide(10, 0)
        false
    } catch _ {
        true
    }
    assert(threw, "Should have thrown")
}

# Test exception message
testErrorMessage() = {
    message = try {
        parseNumber("abc")
        ""
    } catch e {
        e
    }
    assert(message.contains("invalid"), "Should mention 'invalid'")
}

Property-Based Testing Pattern

# Test properties hold for many inputs
testProperty(name: String, gen: () -> T, prop: T -> Bool, iterations: Int) = {
    for i = 0 to iterations {
        value = gen()
        if !prop(value) then {
            println("Property '" ++ name ++ "' failed for: " ++ show(value))
            assert(false)
        }
    }
    println("Property '" ++ name ++ "' passed " ++ show(iterations) ++ " tests")
}

# Usage
main() = {
    # Test that reversing a list twice gives original
    testProperty(
        "reverse-reverse",
        () => randomList(10),
        list => list.reverse().reverse() == list,
        100
    )
    0
}

Testing Async/Concurrent Code

# Test with timeout
testWithTimeout(name: String, timeoutMs: Int, test: () -> T) = {
    me = self()
    spawn(() => {
        result = test()
        send(me, ("done", result))
    })

    match receiveTimeout(timeoutMs) {
        Some(("done", result)) -> {
            println("Test '" ++ name ++ "' passed")
            result
        }
        None -> {
            println("Test '" ++ name ++ "' timed out!")
            assert(false)
        }
    }
}

# Test message passing
testMessagePassing() = {
    parent = self()
    child = spawn(() => {
        msg = receive()
        send(parent, msg * 2)
    })
    send(child, 21)
    result = receive()
    assert_eq(result, 42)
}

Mocking Pattern

# Define interface as functions
type Database = {
    query: String -> List[Row],
    execute: String -> Int
}

# Real implementation
realDb = Database {
    query: sql => Pg.query(conn, sql, []),
    execute: sql => Pg.execute(conn, sql, [])
}

# Mock implementation
mockDb = Database {
    query: _ => [("Alice", 30), ("Bob", 25)],
    execute: _ => 1
}

# Function uses interface
getUsers(db: Database) = db.query("SELECT * FROM users")

# Test with mock
testGetUsers() = {
    users = getUsers(mockDb)
    assert_eq(users.length(), 2)
}

Test Fixtures

# Create test data
createTestUser(id: Int) = User {
    id: id,
    name: "User" ++ show(id),
    email: "user" ++ show(id) ++ "@test.com"
}

createTestUsers(n: Int) = range(1, n + 1).map(createTestUser)

# Usage in tests
testUserFiltering() = {
    users = createTestUsers(10)
    adults = users.filter(u => u.id > 5)
    assert_eq(adults.length(), 5)
}

Testing HTTP Handlers

# Create mock request
mockRequest(path: String, method: String, body: String) = {
    Request {
        path: path,
        method: method,
        body: body,
        headers: %{},
        query: %{},
        id: 0
    }
}

# Capture response
type CapturedResponse = { status: Int, body: String }
mvar capturedResponse: Option[CapturedResponse] = None

mockRespond(req, status: Int, body: String) = {
    capturedResponse = Some(CapturedResponse { status, body })
}

# Test handler
testGetUsersEndpoint() = {
    req = mockRequest("/users", "GET", "")
    handler(req)  # Uses mockRespond

    match capturedResponse {
        Some(resp) -> {
            assert_eq(resp.status, 200)
            assert(resp.body.contains("Alice"))
        }
        None -> assert(false, "No response captured")
    }
}

Benchmark Pattern

benchmark(name: String, iterations: Int, fn: () -> T) = {
    start = currentTimeMillis()
    for i = 0 to iterations {
        fn()
    }
    elapsed = currentTimeMillis() - start
    perOp = elapsed / iterations
    println(name ++ ": " ++ show(elapsed) ++ "ms total, " ++ show(perOp) ++ "ms/op")
}

# Usage
main() = {
    benchmark("list append", 10000, () => {
        [1, 2, 3] ++ [4, 5, 6]
    })

    benchmark("map insert", 10000, () => {
        %{"a": 1}.insert("b", 2)
    })
    0
}

Test Runner Pattern

type TestResult = Pass(String) | Fail(String, String)

runTest(name: String, test: () -> ()) -> TestResult = {
    try {
        test()
        Pass(name)
    } catch e {
        Fail(name, e)
    }
}

runSuite(tests: List[(String, () -> ())]) = {
    results = tests.map((name, test) => runTest(name, test))

    passed = results.filter(r => match r { Pass(_) -> true, _ -> false }).length()
    failed = results.filter(r => match r { Fail(_, _) -> true, _ -> false }).length()

    results.each(r => match r {
        Pass(name) -> println("[PASS] " ++ name)
        Fail(name, err) -> println("[FAIL] " ++ name ++ ": " ++ err)
    })

    println("\n" ++ show(passed) ++ " passed, " ++ show(failed) ++ " failed")
    failed == 0
}

# Usage
main() = {
    success = runSuite([
        ("arithmetic", testArithmetic),
        ("strings", testStrings),
        ("options", testOption)
    ])
    if success then 0 else 1
}

Traits in Nostos

Defining Traits

# Basic trait definition
trait Show
    show(self) -> String
end

trait Eq
    eq(self, other: Self) -> Bool
end

# Trait with multiple methods
trait Comparable
    compare(self, other: Self) -> Int
    lt(self, other: Self) -> Bool
    gt(self, other: Self) -> Bool
end

Implementing Traits

type Person = { name: String, age: Int }

# Implement Show for Person
Person: Show
    show(self) = self.name ++ " (age " ++ show(self.age) ++ ")"
end

# Implement Eq for Person
Person: Eq
    eq(self, other) = self.name == other.name && self.age == other.age
end

# Use the trait methods
alice = Person("Alice", 30)
bob = Person("Bob", 25)

alice.show()        # "Alice (age 30)"
alice.eq(bob)       # false

Supertraits (Trait Inheritance)

# Base trait
trait Displayable
    display(self) -> String
end

# Child trait requires Displayable
trait Formattable: Displayable
    format(self, prefix: String) -> String
end

type Item = { id: Int, name: String }

# Must implement Displayable FIRST
Item: Displayable
    display(self) = self.name
end

# Then can implement Formattable
Item: Formattable
    format(self, prefix) = prefix ++ ": " ++ self.display()
end

Multiple Supertraits

trait Printable
    print(self) -> String
end

trait Sortable
    sortKey(self) -> Int
end

# Requires both
trait Listable: Printable, Sortable
    listItem(self) -> String
end

Trait Bounds on Functions

# Function accepting any Show type
printItem[T: Show](item: T) = println(item.show())

# Multiple bounds with +
printAndCompare[T: Show + Eq](a: T, b: T) = {
    println(a.show())
    println(b.show())
    a.eq(b)
}

# Alternative syntax with when
process(item: T) when T: Show = item.show()

Generic Trait Implementations

type Box[T] = { value: T }

# Implement Show for Box when T has Show
Box[T]: Show when T: Show
    show(self) = "Box(" ++ self.value.show() ++ ")"
end

# Now works for any Box[T] where T: Show
intBox = Box(42)
intBox.show()       # "Box(42)"

Operator Overloading via Traits

# Num trait for arithmetic operators
trait Num
    add(self, other: Self) -> Self
    sub(self, other: Self) -> Self
    mul(self, other: Self) -> Self
    div(self, other: Self) -> Self
end

type Vec2 = { x: Int, y: Int }

Vec2: Num
    add(self, other) = Vec2(self.x + other.x, self.y + other.y)
    sub(self, other) = Vec2(self.x - other.x, self.y - other.y)
    mul(self, other) = Vec2(self.x * other.x, self.y * other.y)
    div(self, other) = Vec2(self.x / other.x, self.y / other.y)
end

# Now operators work!
v1 = Vec2(1, 2)
v2 = Vec2(3, 4)
v3 = v1 + v2        # Vec2(4, 6)

Index Trait

trait Index
    index(self, i: Int) -> Float
end

trait IndexMut
    indexMut(self, i: Int, value: Float) -> Self
end

type Vector = { data: List[Float] }

Vector: Index
    index(self, i) = self.data[i]
end

Vector: IndexMut
    indexMut(self, i, value) = Vector(self.data.set(i, value))
end

# Now bracket notation works
v = Vector([1.0, 2.0, 3.0])
v[0]            # 1.0
v[1] = 5.0      # Vector([1.0, 5.0, 3.0])

Heterogeneous Collections with Sum Types

trait Drawable
    draw(self) -> String
end

type Circle = { radius: Float }
type Square = { side: Float }

Circle: Drawable
    draw(self) = "Circle(r=" ++ show(self.radius) ++ ")"
end

Square: Drawable
    draw(self) = "Square(s=" ++ show(self.side) ++ ")"
end

# Sum type wrapper
type Shape = C(Circle) | S(Square)

Shape: Drawable
    draw(self) = match self { C(c) -> c.draw(), S(s) -> s.draw() }
end

# Now heterogeneous list works!
shapes: List[Shape] = [C(Circle(1.0)), S(Square(2.0))]
shapes.map(s => s.draw())   # ["Circle(r=1)", "Square(s=2)"]

Built-in Traits

# Show - convert to string
trait Show
    show(self) -> String
end

# Eq - equality comparison
trait Eq
    eq(self, other: Self) -> Bool
end

# Ord - ordering comparison
trait Ord
    lt(self, other: Self) -> Bool
    gt(self, other: Self) -> Bool
    lte(self, other: Self) -> Bool
    gte(self, other: Self) -> Bool
end

# Hash - for use in Maps/Sets
trait Hash
    hash(self) -> Int
end

# Copy - value can be copied (default for simple types)
trait Copy end

# Default - has a default value
trait Default
    default() -> Self
end

Deriving Traits

# Automatically derive common traits
type Point = { x: Int, y: Int }
    deriving (Show, Eq, Hash, Copy)

# Now these work automatically
p = Point(1, 2)
p.show()            # "Point { x: 1, y: 2 }"
p.eq(Point(1, 2))   # true
p.hash()            # some hash value

Types in Nostos

Built-in Types

Int         # 64-bit integer
Float       # 64-bit floating point
Bool        # true or false
String      # UTF-8 string
Char        # Single character
()          # Unit type (empty value)
List[T]     # List of elements
(A, B)      # Tuple
Map[K, V]   # Map/dictionary
Set[T]      # Set

Type Annotations

# Variables (usually inferred)
x: Int = 42
name: String = "Alice"
items: List[Int] = [1, 2, 3]

# Function parameters and return
add(a: Int, b: Int) -> Int = a + b

# Complex types
data: Map[String, List[Int]] = %{"a": [1, 2], "b": [3, 4]}

Record Types

# Define a record type
type Person = { name: String, age: Int }

# Create instance
alice = Person("Alice", 30)
bob = Person(name: "Bob", age: 25)

# Access fields
alice.name      # "Alice"
alice.age       # 30

# Records are immutable - create new with changes
older = Person(alice.name, alice.age + 1)

Variant Types (Sum Types)

# Define variants
type Color = Red | Green | Blue

# Variants with data
type Shape = Circle(Float) | Rectangle(Float, Float) | Point

# Use pattern matching
describe(c: Color) = match c {
    Red -> "red",
    Green -> "green",
    Blue -> "blue"
}

area(s: Shape) = match s {
    Circle(r) -> 3.14159 * r * r,
    Rectangle(w, h) -> w * h,
    Point -> 0.0
}

# Common pattern: Option
type Option[T] = Some(T) | None

# Common pattern: Result
type Result[T, E] = Ok(T) | Err(E)

Variant with Named Fields

# Variants can have named fields
type Event =
    | Click { x: Int, y: Int }
    | KeyPress { key: Char, modifiers: List[String] }
    | Scroll { delta: Int }

# Pattern match with field names
handle(e: Event) = match e {
    Click { x, y } -> "Clicked at " ++ show(x) ++ "," ++ show(y),
    KeyPress { key, _ } -> "Pressed " ++ show(key),
    Scroll { delta } -> "Scrolled " ++ show(delta)
}

Generic Types

# Generic record
type Box[T] = { value: T }

intBox: Box[Int] = Box(42)
strBox: Box[String] = Box("hello")

# Generic variant
type Tree[T] = Leaf(T) | Node(Tree[T], Tree[T])

# Multiple type parameters
type Pair[A, B] = { first: A, second: B }
type Either[L, R] = Left(L) | Right(R)

Type Aliases

# Simple alias
type UserId = Int
type Email = String

# Generic alias
type StringMap[V] = Map[String, V]
type Callback[T] = (T) -> ()

# Using aliases
users: StringMap[Person] = %{"alice": Person("Alice", 30)}

Tuples

# Tuple types
point: (Int, Int) = (10, 20)
triple: (String, Int, Bool) = ("hello", 42, true)

# Access by index (pattern matching)
(x, y) = point
(name, age, active) = triple

# Tuple in function return
divmod(a: Int, b: Int) -> (Int, Int) = (a / b, a % b)
(quotient, remainder) = divmod(17, 5)

Nested Types

type Address = { street: String, city: String, zip: String }
type Company = { name: String, address: Address }
type Employee = { name: String, company: Company }

# Accessing nested fields
emp = Employee("Alice", Company("Acme", Address("123 Main", "NYC", "10001")))
emp.company.address.city    # "NYC"

Recursive Types

# Linked list
type LinkedList[T] = Nil | Cons(T, LinkedList[T])

# Binary tree
type BinaryTree[T] = Empty | Node(T, BinaryTree[T], BinaryTree[T])

# JSON-like structure
type Json =
    | JsonNull
    | JsonBool(Bool)
    | JsonNumber(Float)
    | JsonString(String)
    | JsonArray(List[Json])
    | JsonObject(Map[String, Json])

Type Inference

# Types are inferred when possible
x = 42                  # x: Int
y = "hello"             # y: String
z = [1, 2, 3]           # z: List[Int]

# Inference through functions
double(x) = x * 2       # Inferred: Int -> Int
greet(name) = "Hi " ++ name  # Inferred: String -> String

# Sometimes annotation needed
identity(x: T) -> T = x  # Generic needs explicit parameter

Working with Option

type Option[T] = Some(T) | None

# Creating
found: Option[Int] = Some(42)
missing: Option[Int] = None

# Pattern match
getValue(opt: Option[Int]) -> Int = match opt {
    Some(x) -> x,
    None -> 0
}

# Common methods
Some(42).map(x => x * 2)        # Some(84)
None.map(x => x * 2)            # None
Some(42).getOrElse(0)           # 42
None.getOrElse(0)               # 0

Working with Result

type Result[T, E] = Ok(T) | Err(E)

# Creating
success: Result[Int, String] = Ok(42)
failure: Result[Int, String] = Err("not found")

# Pattern match
handle(r: Result[Int, String]) = match r {
    Ok(value) -> "Got: " ++ show(value),
    Err(msg) -> "Error: " ++ msg
}

# Common methods
Ok(42).map(x => x * 2)          # Ok(84)
Err("fail").map(x => x * 2)     # Err("fail")
Ok(42).mapErr(e => "Error: " ++ e)  # Ok(42)

Constructing Records/Variants

type Person = { name: String, age: Int }

# Positional
p1 = Person("Alice", 30)

# Named (order doesn't matter)
p2 = Person(age: 25, name: "Bob")

# Variants
type Status = Active | Inactive(String)

s1 = Active
s2 = Inactive("vacation")

See Also


WebSockets in Nostos

WebSocket Server

use stdlib.server.*

handler(req) = {
    if WebSocket.isUpgrade(req) then {
        ws = WebSocket.accept(req.id)
        handleWebSocket(ws)
    } else {
        respondText(req, "WebSocket endpoint")
    }
}

handleWebSocket(ws) = {
    msg = WebSocket.recv(ws)
    WebSocket.send(ws, "Echo: " ++ msg)
    handleWebSocket(ws)  # Loop for more messages
}

main() = serve(8080, handler)

WebSocket Client

# Connect to WebSocket server
ws = WebSocket.connect("wss://echo.websocket.org")

# Send message
WebSocket.send(ws, "Hello, WebSocket!")

# Receive message
response = WebSocket.recv(ws)
println("Got: " ++ response)

# Close connection
WebSocket.close(ws)

Message Types

# Text messages (default)
WebSocket.send(ws, "Hello")
msg = WebSocket.recv(ws)  # String

# JSON messages
data = %{"type": "chat", "text": "Hello"}
WebSocket.send(ws, json.stringify(data))

received = json.parse(WebSocket.recv(ws))
msgType = received["type"]

Chat Server Pattern

mvar clients: List[WebSocket] = []

broadcast(message: String) = {
    clients.each(ws => {
        try { WebSocket.send(ws, message) }
        catch _ { () }  # Ignore send errors
    })
}

handleClient(ws) = {
    # Add to client list
    clients = clients ++ [ws]

    loop() = {
        try {
            msg = WebSocket.recv(ws)
            broadcast(msg)
            loop()
        } catch _ {
            # Remove on disconnect
            clients = clients.filter(c => c != ws)
        }
    }
    loop()
}

handler(req) = {
    if WebSocket.isUpgrade(req) then {
        ws = WebSocket.accept(req.id)
        spawn(() => handleClient(ws))
    } else {
        respondText(req, "Chat server")
    }
}

main() = serve(8080, handler)

Typed Message Protocol

type ClientMessage =
    | Join { username: String }
    | Chat { text: String }
    | Leave

type ServerMessage =
    | Welcome { users: List[String] }
    | UserJoined { username: String }
    | Message { from: String, text: String }
    | UserLeft { username: String }

parseClientMessage(raw: String) -> ClientMessage = {
    data = json.parse(raw)
    match data["type"] {
        "join" -> Join { username: data["username"] }
        "chat" -> Chat { text: data["text"] }
        "leave" -> Leave
    }
}

sendServerMessage(ws, msg: ServerMessage) = {
    json = match msg {
        Welcome { users } -> %{"type": "welcome", "users": users}
        UserJoined { username } -> %{"type": "joined", "username": username}
        Message { from, text } -> %{"type": "message", "from": from, "text": text}
        UserLeft { username } -> %{"type": "left", "username": username}
    }
    WebSocket.send(ws, json.stringify(json))
}

Room-Based Chat

type Room = { name: String, clients: List[WebSocket] }

mvar rooms: Map[String, Room] = %{}

joinRoom(roomName: String, ws: WebSocket) = {
    room = rooms[roomName].getOrElse(Room { name: roomName, clients: [] })
    updatedRoom = Room { name: roomName, clients: room.clients ++ [ws] }
    rooms[roomName] = updatedRoom
}

broadcastToRoom(roomName: String, message: String) = {
    match rooms[roomName] {
        Some(room) -> room.clients.each(ws => WebSocket.send(ws, message))
        None -> ()
    }
}

leaveRoom(roomName: String, ws: WebSocket) = {
    match rooms[roomName] {
        Some(room) -> {
            updated = Room { name: roomName, clients: room.clients.filter(c => c != ws) }
            rooms[roomName] = updated
        }
        None -> ()
    }
}

Ping/Pong Heartbeat

handleWithHeartbeat(ws) = {
    lastPing = currentTimeMillis()

    # Spawn heartbeat checker
    spawn(() => {
        while true {
            sleep(30000)  # Check every 30s
            if currentTimeMillis() - lastPing > 60000 then {
                WebSocket.close(ws)
                break
            }
        }
    })

    loop() = {
        msg = WebSocket.recv(ws)
        if msg == "ping" then {
            lastPing = currentTimeMillis()
            WebSocket.send(ws, "pong")
        } else {
            handleMessage(ws, msg)
        }
        loop()
    }
    loop()
}

Binary Data

# Send binary data as base64
sendBinary(ws, data: List[Int]) = {
    encoded = base64Encode(data)
    WebSocket.send(ws, encoded)
}

# Receive and decode
receiveBinary(ws) -> List[Int] = {
    encoded = WebSocket.recv(ws)
    base64Decode(encoded)
}

Error Handling

handleWebSocket(ws) = {
    try {
        loop() = {
            msg = WebSocket.recv(ws)
            response = processMessage(msg)
            WebSocket.send(ws, response)
            loop()
        }
        loop()
    } catch {
        "connection closed" -> println("Client disconnected")
        e -> println("WebSocket error: " ++ e)
    }
}

Reconnection (Client)

connectWithRetry(url: String, maxRetries: Int) -> Option[WebSocket] = {
    attempt(retries) = {
        if retries <= 0 then None
        else {
            try {
                ws = WebSocket.connect(url)
                Some(ws)
            } catch _ {
                println("Connection failed, retrying in 1s...")
                sleep(1000)
                attempt(retries - 1)
            }
        }
    }
    attempt(maxRetries)
}

Broadcast with Sender Filtering

mvar clientMap: Map[Int, WebSocket] = %{}
mvar nextId: Int = 0

addClient(ws: WebSocket) -> Int = {
    id = nextId
    nextId = nextId + 1
    clientMap[id] = ws
    id
}

broadcastExcept(senderId: Int, message: String) = {
    clientMap.entries().each((id, ws) => {
        if id != senderId then {
            try { WebSocket.send(ws, message) }
            catch _ { clientMap = clientMap.remove(id) }
        }
    })
}