For AI Assistants: Use this as a skill file for Claude, Cursor, Copilot, or other LLMs.
Download raw markdown fileNostos Language Reference
Complete reference for the Nostos programming language. This document combines all skill documentation into a single file.
Table of Contents
- Language Overview
- Gotchas & Common Mistakes
- Standard Library Reference
- End-to-End Examples
- Quick Reference Cheat Sheet
- Basics
- Collections
- Concurrency
- Control Flow
- Database
- Error Handling
- File I/O
- Functions
- HTTP
- JSON
- Lists
- Modules
- Reactive Programming
- Strings
- Templates & Metaprogramming
- Testing
- Traits
- Types
- 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
- Scripting and automation - Concise syntax, fast startup
- Data transformation - Pattern matching, list operations
- Web services - Built-in HTTP server/client, JSON support
- DSLs - Templates enable domain-specific abstractions
- Learning FP - Clean syntax without ceremony
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
- error_handling.md - Handle exceptions in spawned processes with try/catch
- 03_examples.md - End-to-end concurrent task processing example
- templates.md - Use
@retrytemplate pattern for flaky concurrent operations
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
- types.md - Defining custom error types with variants
- concurrency.md - Error handling in spawned processes, process linking for crash handling
- templates.md -
@withFallbackand@retrypatterns for automatic error recovery - 02_stdlib_reference.md - Full Option and Result method reference
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
- templates.md - Transform functions at compile time with decorators (
@logged,@retry) - traits.md - Generic functions with trait bounds (
[T: Show]) - 02_stdlib_reference.md - Full list of collection methods (map, filter, fold, etc.)
- 03_examples.md - Functions in real-world contexts
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
- Templates run at compile time - no runtime overhead
quotecaptures code - doesn't execute it~splices values - inserts AST into quotesevalparses strings - for dynamic function namesgensymcreates unique names - prevents collisionstoVarreferences parameters - for validation patterns~ifgenerates conditionally - compile-time branchingcomptimeexecutes early - pre-compute values- try/catch works in templates - for error handling patterns
See Also
- types.md - Type definitions that templates can introspect (
~typeDef.fields) - traits.md - Templates can generate trait implementations
- functions.md - Function syntax that templates transform (
~fn.body,~fn.params) - error_handling.md - Try/catch syntax used in template error patterns
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
- templates.md - Generate code based on type structure (
~typeDef.fields) - traits.md - Add behavior to types with trait implementations
- error_handling.md -
Option[T]andResult[T, E]patterns - 02_stdlib_reference.md - Methods available on Option, Result, and collections
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) }
}
})
}