Get the code: learnnostos.nos
Nostos is a functional-first programming language with lightweight concurrency, pattern matching, and non-blocking I/O. Built in Rust, it features a register-based VM with optional JIT compilation, Erlang-style processes, and Hindley-Milner type inference. The name comes from Greek (nostos) meaning "homecoming."
# Single-line comments start with a hash.
#* Multi-line comments
are wrapped like this. *#
## Documentation comments use double hash.
## They are available via introspection at runtime.
## 1. Primitive Types
# Integers (64-bit signed)
42
-17
0xFF # hexadecimal
0b1010 # binary
1_000_000 # underscores for readability
# Floats (64-bit IEEE 754)
3.14159
1.2e-10
# Decimals (arbitrary precision)
0.1d # exact decimal, no floating-point surprises
# Booleans
true
false
# Characters
'a'
'\n'
# Strings (UTF-8, double-quoted)
"Hello, world!"
"Interpolation: ${40 + 2}" # => "Interpolation: 42"
# Unit (like void, but is a value)
()
## 2. Variables
# Immutable by default
x = 5
name = "Alice"
# Mutable variables use `var`
var count = 0
count = count + 1 # OK
count += 1 # shorthand works too
# Variables are block-scoped. Rebinding an immutable variable to
# a different value is a runtime error (like Erlang's unification).
x = 5
x = 5 # OK — same value
# x = 10 # ERROR — x is already bound to 5
# Shadowing in inner scopes is allowed
result = {
x = 10 # shadows outer x within this block
x + 1 # => 11
}
# x is still 5 here
# Type annotations are optional (inference handles most cases)
Int age = 30
String greeting = "hi"
List[Int] nums = [1, 2, 3]
## 3. Operators
# Arithmetic
1 + 2 # => 3
10 - 3 # => 7
4 * 5 # => 20
10 / 3 # => 3 (integer division)
10 % 3 # => 1 (modulo)
2 ** 10 # => 1024 (exponentiation)
# Comparison
1 == 1 # => true
1 != 2 # => true
3 < 5 # => true
5 >= 5 # => true
# Logical (short-circuit)
true && false # => false
false || true # => true
!true # => false
# String concatenation
"Hello" ++ " " ++ "world" # => "Hello world"
# List cons (prepend)
1 :: [2, 3] # => [1, 2, 3]
## 4. Collections
# Lists (immutable, linked)
numbers = [1, 2, 3, 4, 5]
empty = []
nested = [[1, 2], [3, 4]]
# Head and tail via pattern matching
[head | tail] = [1, 2, 3]
# head => 1, tail => [2, 3]
# Maps (immutable, key-value)
person = %{"name": "Alice", "city": "Paris"}
squares = %{1: 1, 2: 4, 3: 9}
empty_map = %{}
# Sets (immutable, unique elements)
tags = #{"urgent", "review", "bug"}
unique = #{1, 2, 3, 2, 1} # => #{1, 2, 3}
empty_set = #{}
# Tuples (fixed-size, mixed types)
pair = (1, "hello")
triple = (true, 3.14, "yes")
# Typed arrays (mutable, contiguous memory)
ints = newInt64Array(10)
floats = newFloat64Array(5)
ints[0] = 42 # index assignment
x = ints[0] # index access
## 5. Functions
# Simple function definition
double(x) = x * 2
add(a, b) = a + b
# Multi-line functions use braces
process(data) = {
validated = validate(data)
transformed = transform(validated)
save(transformed) # last expression is the return value
}
# Type annotations on functions
length(s: String) -> Int = s.length()
# Lambda expressions
square = x => x * 2
sum = (a, b) => a + b
# Multi-line lambdas
transform = x => {
y = x * 2
y + 1
}
# Functions are first-class values
apply(f, x) = f(x)
apply(double, 5) # => 10
# Function composition
compose(f, g) = x => f(g(x))
# UFCS: any function can be called as a method
# length(list) and list.length() are equivalent
[1, 2, 3].length() # => 3
"hello".length() # => 5
# Method chaining via UFCS
result = [1, 2, 3, 4, 5]
.filter(x => x % 2 == 0) # [2, 4]
.map(x => x * x) # [4, 16]
.fold(0, (a, b) => a + b) # 20
## 6. Pattern Matching
# Multi-clause functions match on arguments.
# All clauses must be consecutive (no other code in between)
# and in the same file. They are tried top-to-bottom.
fib(0) = 0
fib(1) = 1
fib(n) = fib(n - 1) + fib(n - 2)
# Pattern matching on lists
sum([]) = 0
sum([head | tail]) = head + sum(tail)
# Quicksort in three lines
quicksort([]) = []
quicksort([pivot | rest]) =
quicksort(rest.filter(x => x < pivot))
++ [pivot]
++ quicksort(rest.filter(x => x >= pivot))
# Guards with `when`
abs(n) when n >= 0 = n
abs(n) = -n
classify(n) when n > 0 = "positive"
classify(n) when n < 0 = "negative"
classify(0) = "zero"
# Match expressions
describe(n) = match n {
0 -> "zero"
1 -> "one"
_ -> "many"
}
# Pattern matching on algebraic types (see section 8)
area(Circle(r)) = 3.14159 * r * r
area(Rectangle(w, h)) = w * h
# Tuple pattern matching
compare(a, b) = match (a > b, a < b) {
(true, _) -> "greater"
(_, true) -> "less"
_ -> "equal"
}
# Destructuring assignment
(a, b) = (1, 2)
{name: n, age: a} = %{"name": "Alice", "age": 30}
[first | rest] = [1, 2, 3]
# Wildcard _ ignores a value
(_, second) = (1, 2) # second => 2
## 7. Control Flow
# If expression (always returns a value)
max(a, b) = if a > b then a else b
sign(n) =
if n > 0 then 1
else if n < 0 then -1
else 0
# For loops (end is exclusive)
main() = {
var total = 0
for i = 1 to 11 {
total = total + i
}
# total => 55
# While loops
var n = 10
while n > 0 {
println(show(n))
n = n - 1
}
# Break and continue
var found = -1
for i = 0 to 100 {
if i * i > 50 then {
found = i
break
} else ()
}
# Early return
found
}
## 8. Types
# Record types (product types)
type Point = { x: Float, y: Float }
type User = { name: String, age: Int }
# Construction
p = Point(3.0, 4.0) # positional
p = Point(x: 3.0, y: 4.0) # named fields
# Field access
p.x # => 3.0
# Functional update (creates a new record)
q = Point(p, x: 10.0) # copy p with x overridden
# Sum types (algebraic data types / variants)
type Shape =
| Circle(Float)
| Rectangle(Float, Float)
| Triangle(Float, Float, Float)
type Direction = North | South | East | West
# Generic types
type Tree[T] = Leaf | Node(T, Tree[T], Tree[T])
type Option[T] = None | Some(T)
type Result[T, E] = Ok(T) | Err(E)
# Pattern matching on types
depth(Leaf) = 0
depth(Node(_, left, right)) =
1 + max(depth(left), depth(right))
# Mutable record types
var type MutPoint = { x: Float, y: Float }
mp = MutPoint(1.0, 2.0)
mp.x = 5.0 # direct mutation OK
## 9. Traits
# Define a trait (interface)
trait Animal
speak(self) -> String
describe(self) -> String
end
# Implement a trait for a type
type Dog = { name: String, age: Int }
type Cat = { name: String, lives: Int }
Dog: Animal
speak(self) = "Woof! I'm " ++ self.name
describe(self) = self.name ++ ", age " ++ show(self.age)
end
Cat: Animal
speak(self) = "Meow! I'm " ++ self.name
describe(self) = self.name ++ ", " ++ show(self.lives) ++ " lives"
end
# Supertraits (trait inheritance)
# trait Serializable: Show
# toJson(self) -> String
# end
# Using traits
dog = Dog("Buddy", 5)
dog.speak() # => "Woof! I'm Buddy"
dog.describe() # => "Buddy, age 5"
## 10. Modules and Imports
# Import everything from a module
# use math.*
# Import specific items
# use stdlib.server.{serve, respondText}
# Import with alias
# use graphics.{draw as graphicsDraw}
# Public visibility with `pub` (private by default)
# module Geometry
# pub type Point = { x: Float, y: Float }
# pub distance(a: Point, b: Point) = ...
# square(x) = x * x # private helper
# end
## 11. Error Handling
# Throw any value as an exception
safe_divide(a, b) =
if b == 0 then throw("division by zero")
else a / b
# Try/catch with pattern matching
result = try { safe_divide(10, 0) }
catch {
"division by zero" -> -1
other -> -2
}
# Nested try/catch
outer =
try {
try { throw("deep") }
catch { "deep" -> "handled" }
}
catch { e -> "outer: " ++ e }
# => "handled"
## 12. Concurrency
# Nostos uses lightweight processes (like Erlang).
# Each process is ~2KB. You can spawn millions of them.
# All I/O is non-blocking — no async/await needed.
# Spawn a process
pid = spawn { some_function() }
# Get current process ID
me = self()
# Send a message with <-
pid <- "hello"
pid <- 42
pid <- (x, y, z)
# Receive messages with pattern matching
receive {
"hello" -> println("got hello")
n -> println("got: " ++ show(n))
}
# A stateful counter server using message passing
type Msg = Inc(Pid) | Get(Pid) | Reply(Int)
counter_loop(state) = receive {
Inc(sender) -> {
sender <- Reply(state + 1)
counter_loop(state + 1)
}
Get(sender) -> {
sender <- Reply(state)
counter_loop(state)
}
}
increment(counter) = {
counter <- Inc(self())
receive { Reply(val) -> val }
}
# Ping-pong between two processes
type PingMsg = Ping(Pid, Int) | Pong(Int) | Done(Int) | Stop
ping(pong_pid, 0, parent) = parent <- Done(0)
ping(pong_pid, n, parent) = {
pong_pid <- Ping(self(), n)
receive {
Pong(m) -> ping(pong_pid, m - 1, parent)
}
}
pong() = receive {
Ping(sender, n) -> {
sender <- Pong(n)
pong()
}
Stop -> ()
}
# Thread-safe global variables with mvar (type annotation required)
mvar requestCount: Int = 0
# Reads and writes are atomic within a function — no other
# process can modify the mvar between read and write here.
increment() = { requestCount = requestCount + 1; requestCount }
## 13. Standard Library
# List operations (all chainable via UFCS)
[1, 2, 3].map(x => x * 2) # => [2, 4, 6]
[1, 2, 3, 4].filter(x => x > 2) # => [3, 4]
[1, 2, 3].fold(0, (acc, x) => acc + x) # => 6
[3, 1, 2].sort() # => [1, 2, 3]
[1, 2, 3].reverse() # => [3, 2, 1]
[1, 2, 3].length() # => 3
[1, 2, 3].take(2) # => [1, 2]
[1, 2, 3].drop(1) # => [2, 3]
[[1, 2], [3, 4]].flatten() # => [1, 2, 3, 4]
[1, 2].zip(["a", "b"]) # => [(1, "a"), (2, "b")]
[1, 2, 3].any(x => x > 2) # => true
[1, 2, 3].all(x => x > 0) # => true
# String operations
"hello".length() # => 5
"hello".toUpper() # => "HELLO"
" hello ".trim() # => "hello"
"hello".contains("ell") # => true
"hello".startsWith("he") # => true
"hello".replace("l", "L") # => "heLlo"
"hello".reverse() # => "olleh"
show(42) # => "42" (any value to string)
# JSON
data = jsonParse("{\"name\": \"Alice\"}")
str = jsonStringify(data)
# File I/O
(status, content) = File.readAll("config.txt")
(status, _) = File.writeAll("output.txt", "Hello!")
(status, exists) = File.exists("config.txt")
# Encoding
encoded = Base64.encode("Hello!")
encoded = Url.encode("a=1&b=2")
# Time
now = Time.now()
formatted = Time.format(now, "%Y-%m-%d %H:%M:%S")
# Crypto
hash = Crypto.sha256("data")
bcrypt = Crypto.bcryptHash("password", 12)
valid = Crypto.bcryptVerify("password", bcrypt)
## 14. HTTP and Networking
# HTTP client
(status, resp) = Http.get("https://api.example.com/data")
(status, resp) = Http.post("https://api.example.com", body)
# HTTP server (handler receives a single request argument)
use stdlib.server.*
handler(req) =
Server.respond(req.id, 200,
[("Content-Type", "text/plain")],
"Hello, World!")
main() = serve(8080, handler) # each request is spawned as a process
# TCP sockets
server = Tcp.listen(9000)
client = Tcp.accept(server)
Tcp.send(client, "Hello!")
message = Tcp.receive(client)
# WebSockets
ws = WebSocket.connect("wss://echo.websocket.org")
WebSocket.send(ws, "Hello!")
response = WebSocket.recv(ws)
## 15. PostgreSQL
# Connect and query
conn = Pg.connect(
"host=localhost dbname=mydb user=postgres password=secret")
# Parameterized queries (prevents SQL injection)
rows = Pg.query(conn,
"SELECT name, email FROM users WHERE age > $1",
(18))
rows.map(row => println(row.0 ++ ": " ++ row.1))
# Typed results
use stdlib.db.{query}
type User = { name: String, email: String }
users: List[User] = query[User](conn,
"SELECT name, email FROM users", ())
# Transactions
Pg.begin(conn)
Pg.query(conn, "INSERT INTO users (name) VALUES ($1)", ("Alice"))
Pg.commit(conn)
Pg.close(conn)
## 16. Introspection
# Nostos has full runtime introspection.
# Value introspection
typeOf(42) # get the type of any value
point.fields() # list field names of a record
point.toMap() # convert record to map
# Function introspection
fib.name # => "fib"
fib.arity # => 1
fib.source # => source code as string
# REPL tools
# :profile fib(35) — measure execution time
# :debug factorial — set a breakpoint
# :type expr — show inferred type
## 17. FFI (Rust Extensions)
# Nostos can call into Rust crates via extensions.
# In nostos.toml:
# [extensions]
# glam = { git = "https://github.com/pegesund/nostos-glam" }
# In code:
# import glam
# v = glam.vec3(1.0, 2.0, 3.0)
# normalized = v.normalize()
## 18. Putting It All Together
# A complete binary search tree example
type Tree[T] = Leaf | Node(T, Tree[T], Tree[T])
size(Leaf) = 0
size(Node(_, left, right)) = 1 + size(left) + size(right)
sum_tree(Leaf) = 0
sum_tree(Node(v, left, right)) =
v + sum_tree(left) + sum_tree(right)
inorder(Leaf) = []
inorder(Node(v, left, right)) =
inorder(left) ++ [v] ++ inorder(right)
map_tree(_, Leaf) = Leaf
map_tree(f, Node(v, left, right)) =
Node(f(v), map_tree(f, left), map_tree(f, right))
main() = {
tree = Node(4,
Node(2, Node(1, Leaf, Leaf), Node(3, Leaf, Leaf)),
Node(6, Node(5, Leaf, Leaf), Node(7, Leaf, Leaf))
)
println("Size: " ++ show(size(tree))) # 7
println("Sum: " ++ show(sum_tree(tree))) # 28
println("Sorted: " ++ show(inorder(tree))) # [1,2,3,4,5,6,7]
doubled = map_tree(x => x * 2, tree)
println("Doubled sum: " ++ show(sum_tree(doubled))) # 56
}
Got a suggestion? A correction, perhaps? Open an Issue on the GitHub Repo, or make a pull request yourself!
Originally contributed by Hallvard Ystad, and updated by 1 contributor.