Skip to content

Standard Library Reference

This page documents all built-in functions available in Endo. Functions are grouped by category. Each example is executable and verified by the documentation test suite.

Contents


15.1 Output

print

Signature: print value

Prints a value to stdout without a trailing newline.

print 42   # => 42
print "hello"   # => hello

println

Signature: println value

Prints a value to stdout followed by a newline.

println "hello"   # => hello

15.2 Type Conversion

string_length

Signature: string_length str : int

Returns the number of grapheme clusters (user-perceived characters) in a string. This is equivalent to grapheme_length. For byte count, use byte_length.

print (string_length "hello")   # => 5
print (string_length "")   # => 0

int_of_string

Signature: int_of_string str : int

Converts a string to an integer.

let n = int_of_string "7"
print (n + 3)   # => 10

string_of_int

Signature: string_of_int n : str

Converts an integer to its string representation.

print (string_of_int 42)   # => 42
let r = 42 |> string_of_int |> string_length
print r   # => 2

string

Signature: string value : str

Universal conversion to string. Works with integers, floats, booleans, and strings (passthrough).

println (string 42)      # => 42
println (string 3.14)    # => 3.14
println (string true)    # => true
println (string "hi")    # => hi
42 |> string |> println   # => 42

not

Signature: not value : bool

Boolean negation.

let x = not true
print x   # => false
let x = not false
print x   # => true

15.3 String Operations

trim

Signature: trim str : str

Removes leading and trailing whitespace.

print (trim "  hello  ")   # => hello
print (trim "   ")
print ("  hello  " |> trim)   # => hello

toLower

Signature: toLower str : str

Converts a string to lowercase.

print (toLower "HELLO")   # => hello
print ("HELLO" |> toLower)   # => hello

toUpper

Signature: toUpper str : str

Converts a string to uppercase.

print (toUpper "hello")   # => HELLO
print ("hello" |> toUpper)   # => HELLO

contains

Signature: contains substr text : bool

Returns true if text contains the substring substr.

print (contains "world" "hello world")   # => true
print (contains "xyz" "hello")   # => false
print ("hello world" |> contains "world")   # => true

startsWith

Signature: startsWith prefix text : bool

Returns true if text starts with the given prefix.

print (startsWith "hel" "hello")   # => true
print (startsWith "xyz" "hello")   # => false
print ("hello" |> startsWith "hel")   # => true

endsWith

Signature: endsWith suffix text : bool

Returns true if text ends with the given suffix.

print (endsWith "llo" "hello")   # => true
print (endsWith "xyz" "hello")   # => false
print ("hello" |> endsWith "llo")   # => true

replace

Signature: replace old new str : str

Replaces all occurrences of old with new in the string.

print (replace "l" "r" "hello")   # => herro
print (replace "," "" "a,b,c")   # => abc
print ("a,b,c" |> replace "," "-")   # => a-b-c

split

Signature: split separator str : list<str>

Splits a string by the given separator, returning a list of substrings.

let parts = split "," "a,b,c"
print (length parts)   # => 3
let parts = "a:b:c" |> split ":"
print (length parts)   # => 3
print ("a:b:c" |> split ":" |> join "-")   # => a-b-c

lines

Signature: lines str : list<str>

Splits a string into a list of lines by newline characters. Handles both \n and \r\n line endings, stripping \r from each line. A trailing newline does not produce an extra empty element.

print (lines "a\nb\nc" |> length)    # => 3
("hello\nworld") |> lines |> each println
# => hello
# => world

join

Signature: join separator list : str

Joins a list of strings with the given separator.

print (join "-" (split "," "a,b,c"))   # => a-b-c
let r = ["a"; "b"; "c"] |> join "-"
print r   # => a-b-c
print (join "," [])

String Repetition (*)

Repeats a string a given number of times.

print ("ha" * 3)   # => hahaha
print (3 * "ab")   # => ababab

15.3.1 Unicode String Operations

Endo provides Unicode-aware string operations for decomposing strings and counting at different levels of granularity. Strings are stored as UTF-8 internally.

  • Bytes: Raw UTF-8 bytes (0–255)
  • Codepoints: Unicode scalar values (U+0000 to U+10FFFF)
  • Graphemes: User-perceived characters (grapheme clusters, per UAX #29)

For ASCII strings, all three counts are identical. They differ for multi-byte characters (e.g., é = 2 bytes, 1 codepoint) and combining sequences (e.g., e + combining accent = 2 codepoints, 1 grapheme).

bytes

Signature: bytes str : list<int>

Returns the list of UTF-8 byte values (0–255).

print (bytes "hi")   # => [104; 105]

codepoints

Signature: codepoints str : list<int>

Returns the list of Unicode codepoint values.

print (codepoints "hi")   # => [104; 105]

graphemes

Signature: graphemes str : list<string>

Returns the list of grapheme clusters (user-perceived characters) as strings.

print (graphemes "café")   # => ["c"; "a"; "f"; "é"]

byte_length

Signature: byte_length str : int

Returns the number of UTF-8 bytes. Equivalent to bytes str |> length.

print (byte_length "café")   # => 5

codepoint_length

Signature: codepoint_length str : int

Returns the number of Unicode codepoints.

print (codepoint_length "café")   # => 4

grapheme_length

Signature: grapheme_length str : int

Returns the number of grapheme clusters. This is what .length returns.

print (grapheme_length "café")   # => 4

These functions also work as dot-properties and in pipelines:

println ("café".byte_length)         # => 5
println ("café".grapheme_length)     # => 4
println ("café" |> graphemes |> length)   # => 4

15.4 List -- Basic Operations

Signature: head list : option<T>

Returns the first element wrapped in Some, or None for an empty list.

let r = match head [1; 2; 3] with
  | Some v -> v
  | None -> -1
print r   # => 1
let r = match head [] with
  | Some v -> v
  | None -> -1
print r   # => -1

tail

Signature: tail list : list<T>

Returns all elements except the first. Returns [] for an empty list.

print (tail [1; 2; 3])   # => [2; 3]
print (tail [])   # => []
print (tail [1])   # => []

length

Signature: length list : int

Returns the number of elements in a list.

print (length [1; 2; 3])   # => 3
print (length [])   # => 0

isEmpty

Signature: isEmpty list : bool

Returns true if the list has no elements.

print (isEmpty [])   # => true
print (isEmpty [1; 2])   # => false

nth

Signature: nth index list : option<T>

Returns the element at the given zero-based index, wrapped in Some. Returns None if out of bounds.

let r = match nth 1 [10; 20; 30] with
  | Some v -> v
  | None -> -1
print r   # => 20
let r = match nth 5 [10; 20; 30] with
  | Some v -> v
  | None -> -1
print r   # => -1

last

Signature: last list : option<T>

Returns the last element wrapped in Some, or None for an empty list.

let r = match last [1; 2; 3] with
  | Some v -> v
  | None -> -1
print r   # => 3
let r = match last [] with
  | Some v -> v
  | None -> -1
print r   # => -1

replicate

Signature: replicate n value : list<T>

Creates a list of n copies of the given value.

print (replicate 3 42)   # => [42; 42; 42]
print (replicate 0 1)   # => []

15.5 List -- Higher-Order Functions

map

Signature: map f list : list<U>

Applies f to each element and returns a new list of results.

print (map (fun x -> x * 2) [1; 2; 3])   # => [2; 4; 6]
print ([1; 2; 3] |> map (fun x -> x + 1) |> map (fun x -> x * 2))   # => [4; 6; 8]
print (map (_ * 2) [1; 2; 3])   # => [2; 4; 6]

filter

Signature: filter predicate list : list<T>

Returns a list of elements for which the predicate returns true.

print (filter (fun x -> x % 2 == 0) [1; 2; 3; 4; 5; 6])   # => [2; 4; 6]
print (filter (_ > 2) [1; 2; 3; 4; 5])   # => [3; 4; 5]

fold

Signature: fold initial f list : T

Reduces a list to a single value by applying f to the accumulator and each element, starting from initial.

print (fold 0 (fun acc x -> acc + x) [1; 2; 3; 4; 5])   # => 15
print (fold 1 (fun acc x -> acc * x) [1; 2; 3; 4; 5])   # => 120
print (fold 42 (fun acc x -> acc + x) [])   # => 42

reduce

Signature: reduce f list : option<T>

Like fold, but uses the first element as the initial value. Returns Some result or None for empty lists.

let r = reduce (fun a b -> a + b) [1; 2; 3; 4]
print (r ?| 0)   # => 10
let r = reduce (fun a b -> a + b) []
print (r ?| 0)   # => 0

find

Signature: find predicate list : option<T>

Returns Some element for the first element matching the predicate, or None if no element matches.

print (find (fun x -> x > 1) [1; 2; 3] ?| -1)   # => 2
print (find (fun x -> x > 10) [1; 2; 3] ?| -1)   # => -1

exists

Signature: exists predicate list : bool

Returns true if any element satisfies the predicate.

print (exists (fun x -> x > 3) [1; 2; 3; 4; 5])   # => true
print (exists (fun x -> x > 10) [1; 2; 3])   # => false
print (exists (fun x -> x > 0) [])   # => false

forall

Signature: forall predicate list : bool

Returns true if all elements satisfy the predicate. Returns true for empty lists.

print (forall (fun x -> x > 0) [1; 2; 3])   # => true
print (forall (fun x -> x > 2) [1; 2; 3])   # => false
print (forall (fun x -> x > 0) [])   # => true

each

Signature: each f list : unit

Applies f to each element for side effects. Returns unit.

each (fun x -> print x) [1; 2; 3]   # => 123
let _ = [1; 2; 3] |> each print   # => 123

15.6 List -- Transformations

sort

Signature: sort list : list<int>

Sorts a list of integers in ascending order.

print (sort [3; 1; 2])   # => [1; 2; 3]
print (sort [3; -1; 0; -5; 2])   # => [-5; -1; 0; 2; 3]
print (sort [])   # => []

reverse

Signature: reverse list : list<T>

Returns a new list with elements in reverse order.

print (reverse [1; 2; 3])   # => [3; 2; 1]
print (reverse [])   # => []

distinct

Signature: distinct list : list<int>

Removes duplicate elements, preserving first-seen order.

print (distinct [1; 2; 3; 2; 1])   # => [1; 2; 3]
print (distinct [5; 5; 5; 5])   # => [5]

sortBy

Signature: sortBy keyFn list : list<T>

Sorts a list by a key function. Uses stable sort (preserves relative order for equal keys).

print (sortBy (fun x -> x) [3; 1; 2])   # => [1; 2; 3]
print (sortBy (fun x -> 0 - x) [3; 1; 2])   # => [3; 2; 1]

groupBy

Signature: groupBy keyFn list : list<(key, list<T>)>

Groups elements by a key function. Returns a list of (key, elements) tuples.

print (groupBy (fun x -> x % 2) [1; 2; 3; 4; 5])   # => [(1, [1; 3; 5]); (0, [2; 4])]
print (groupBy (fun x -> x) [])   # => []

take

Signature: take n list : list<T>

Returns the first n elements. If the list has fewer than n elements, returns the whole list.

print (take 3 [1; 2; 3; 4; 5])   # => [1; 2; 3]
print (take 0 [1; 2; 3])   # => []
print (take 5 [1; 2; 3])   # => [1; 2; 3]

drop

Signature: drop n list : list<T>

Returns all elements after the first n.

print (drop 3 [1; 2; 3; 4; 5])   # => [4; 5]
print (drop 0 [1; 2; 3])   # => [1; 2; 3]
print (drop 3 [1; 2; 3])   # => []

zip

Signature: zip listA listB : list<(A, B)>

Combines two lists into a list of tuples. Stops at the shorter list.

print (zip [1; 2; 3] [4; 5; 6])   # => [(1, 4); (2, 5); (3, 6)]
print (zip [1; 2] [4; 5; 6])   # => [(1, 4); (2, 5)]
print (zip [] [1; 2; 3])   # => []

flatten

Signature: flatten listOfLists : list<T>

Flattens a list of lists into a single list.

print (flatten [[1; 2]; [3; 4]; [5; 6]])   # => [1; 2; 3; 4; 5; 6]
print (flatten [])   # => []
print (flatten [[]; [1; 2]; []])   # => [1; 2]

15.7 List -- Operators

:: (cons)

Prepends an element to a list. Right-associative.

print (1 :: [])   # => [1]
print (1 :: 2 :: 3 :: [])   # => [1; 2; 3]
print (0 :: [1; 2; 3])   # => [0; 1; 2; 3]

@ (concat)

Concatenates two lists.

print ([1; 2] @ [3; 4])   # => [1; 2; 3; 4]
print ([] @ [1; 2])   # => [1; 2]
print ([] @ [])   # => []

15.8 Option Combinators

Option.map

Signature: Option.map f option : option<U>

Applies f to the inner value of Some, or returns None unchanged. Can also be used as method-style: opt.map f or in pipelines.

print (Option.map (fun x -> x * 2) (Some 21) ?| 0)   # => 42
print (Option.map (fun x -> x * 2) None ?| 0)   # => 0
print (Some 21 |> Option.map (fun x -> x * 2) |> Option.defaultValue 0)   # => 42

Option.bind

Signature: Option.bind f option : option<U>

Applies f (which itself returns an option) to the inner value of Some. Returns None if the input is None or if f returns None. Can also be used as method-style: opt.bind f.

let half (x: int) = if x % 2 == 0 then Some (x / 2) else None
print (Option.bind half (Some 10) ?| 0)   # => 5
let half (x: int) = if x % 2 == 0 then Some (x / 2) else None
print (Option.bind half None ?| 0)   # => 0
let half (x: int) = if x % 2 == 0 then Some (x / 2) else None
print (Option.bind half (Some 3) ?| 0)   # => 0

Option.defaultValue

Signature: Option.defaultValue default option : T

Returns the inner value of Some, or the given default for None. Can also be used as method-style: opt.defaultValue def or in pipelines.

print (Option.defaultValue 0 (Some 42))   # => 42
print (Option.defaultValue 0 None)   # => 0
print (Some 42 |> Option.defaultValue 0)   # => 42
print (None |> Option.defaultValue 99)   # => 99

15.9 Environment & System

env

Signature: env name : option<str>

Looks up an environment variable. Returns Some value if the variable is set, None otherwise.

match env "HOME" with
| Some h -> println $"Home: {h}"
| None   -> println "HOME not set"

# With the ? operator
let user = (env "USER")?
println user

which

Signature: which name : option<str>

Searches $PATH for a program. Returns Some path if found, None otherwise.

match which "git" with
| Some p -> println $"git is at {p}"
| None   -> println "git not found"

# With default value
print (which "git" ?| "/default")

15.10 Random

rand

Signature: rand : int or rand low high : int

With no arguments, returns a random positive integer. With two arguments, returns a random integer in the inclusive range [low, high]. Calling rand with one argument is a compile-time error.

# Random positive integer
let x = rand
println x

# Random integer between 1 and 100
let y = rand 1 100
println y

15.11 Number Formatting

formatNumber

Signatures: - formatNumber separator number : string — uses the given separator - formatNumber number : string — uses the user's locale thousand separator

Formats an integer with thousand separators for readability. In the 2-argument form, separator is a string inserted every 3 digits from the right. In the 1-argument form, the thousand separator is determined by the user's system locale. Negative numbers are handled correctly (the sign is preserved).

print (formatNumber "," 1234567)       # => 1,234,567
print (formatNumber "." 1234567)       # => 1.234.567
print (formatNumber " " 1000000)       # => 1 000 000
print (formatNumber "," 42)            # => 42
print (formatNumber "," -9876543)      # => -9,876,543

Works in pipelines too:

print (1234567 |> formatNumber ",")    # => 1,234,567

15.12 Composition Examples

These examples combine multiple standard library functions.

print ([3; 1; 2; 1; 3] |> sort |> distinct)   # => [1; 2; 3]
print ([5; 3; 8; 1; 4] |> filter (fun x -> x > 2) |> sort)   # => [3; 4; 5; 8]
print ([1; 2; 3] |> map (fun x -> x * 2) |> reverse)   # => [6; 4; 2]
print ([1; 2; 3; 4; 5] |> take 3 |> map (fun x -> x * 10))   # => [10; 20; 30]
print ([1; 2; 3; 4; 5] |> drop 2 |> fold 0 (fun a x -> a + x))   # => 12

15.13 Size Type

The Size type represents byte quantities with human-readable display formatting.

Record fields: bytes: int

Size Literals

Size values can be written directly using numeric suffixes:

print 42B     # => 42 B
print 1KB     # => 1 KB
print 5MB     # => 5 MB
print 2GB     # => 2 GB
print 1TB     # => 1 TB

Float values are supported (except for B, which requires whole numbers):

print 3.5KB   # => 3.5 KB

Size.fromBytes

Signature: Size.fromBytes n : Size

Creates a Size from a raw byte count.

print (Size.fromBytes 1024)   # => 1 KB

Size.fromKB

Signature: Size.fromKB n : Size

Creates a Size from kilobytes (n × 1024 bytes).

print (Size.fromKB 5)   # => 5 KB

Size.fromMB

Signature: Size.fromMB n : Size

Creates a Size from megabytes (n × 1024² bytes).

print (Size.fromMB 1)   # => 1 MB

Size.fromGB

Signature: Size.fromGB n : Size

Creates a Size from gigabytes (n × 1024³ bytes).

print (Size.fromGB 1)   # => 1 GB

Size.fromTB

Signature: Size.fromTB n : Size

Creates a Size from terabytes (n × 1024⁴ bytes).

print (Size.fromTB 1)   # => 1 TB

Field Access

Access the raw byte count via the .bytes field:

print (1KB).bytes   # => 1024

Size Comparison

Size values support comparison operators:

print (1KB < 1MB)   # => true
print (1KB == Size.fromBytes 1024)   # => true

15.14 DateTime Type

The DateTime type represents a point in time (UTC) with named field access.

Record fields: year: int, month: int, day: int, hour: int, minute: int, second: int, epoch: int

DateTime.now

Signature: DateTime.now : DateTime

Returns the current UTC date and time.

let dt = DateTime.now
println dt              # 2024-06-15 14:30:00
println dt.year         # 2024
println dt.month        # 6

DateTime.fromEpoch

Signature: DateTime.fromEpoch epoch : DateTime

Converts a Unix epoch timestamp to a DateTime record.

let dt = DateTime.fromEpoch 1700000000
println dt.year         # 2023
println dt.month        # 11
println dt.day          # 14

formatDateTime

Signature: formatDateTime epoch : str

Formats a Unix epoch timestamp as YYYY-MM-DD HH:MM:SS.

print (formatDateTime 1700000000)   # 2023-11-14 22:13:20

15.15 TimeSpan Type

The TimeSpan type represents a duration of time with millisecond precision and human-readable display formatting.

Record fields: milliseconds: int

TimeSpan Literals

TimeSpan values can be written directly using numeric suffixes:

print 100ms   # => 100ms
print 5s      # => 5s
print 2min    # => 2m
print 1h      # => 1h

Float values are supported (except for ms, which requires whole numbers):

print 1.5h    # => 1h 30m
print 2.5s    # => 2s 500ms

TimeSpan.fromMilliseconds

Signature: TimeSpan.fromMilliseconds n : TimeSpan

Creates a TimeSpan from a raw millisecond count.

print (TimeSpan.fromMilliseconds 1500)   # => 1s 500ms

TimeSpan.fromSeconds

Signature: TimeSpan.fromSeconds n : TimeSpan

Creates a TimeSpan from seconds (n × 1000 ms).

print (TimeSpan.fromSeconds 5)   # => 5s

TimeSpan.fromMinutes

Signature: TimeSpan.fromMinutes n : TimeSpan

Creates a TimeSpan from minutes (n × 60000 ms).

print (TimeSpan.fromMinutes 2)   # => 2m

TimeSpan.fromHours

Signature: TimeSpan.fromHours n : TimeSpan

Creates a TimeSpan from hours (n × 3600000 ms).

print (TimeSpan.fromHours 1)   # => 1h

TimeSpan.fromDays

Signature: TimeSpan.fromDays n : TimeSpan

Creates a TimeSpan from days (n × 86400000 ms).

print (TimeSpan.fromDays 3)   # => 3d

Field Access

let t = TimeSpan.fromSeconds 5
print t.milliseconds   # => 5000

sleep

Signature: sleep ts : unit

Pauses execution for the given TimeSpan duration.

sleep 100ms
1s |> sleep

formatTimeSpan

Signature: formatTimeSpan ts : str

Formats a TimeSpan as a human-readable duration string.

print (formatTimeSpan (TimeSpan.fromSeconds 90))   # => 1m 30s

15.16 Shell Data Commands

These built-in commands return structured records that support dot access, pattern matching, and pipeline operations.

ls

Signature: ls : list<FileInfo> or ls path : list<FileInfo>

Lists directory contents as structured records.

FileInfo fields:

Field Type Description
name str File name
size Size File size
mode FileMode Unix file permissions
mtime DateTime Last modification time
isDir bool Whether entry is a directory
isSymlink bool Whether entry is a symbolic link
target str Symbolic link target (empty for non-symlinks)
path str Absolute path of the entry

The isDir, isSymlink, target, and path fields are available for field access and pipelines but are not shown as columns in the default ls table; a symlink is instead rendered inline as name -> target.

name is only the final path component, so use path whenever you need to address the file itself rather than display it -- it stays correct after the listing has been filtered, mapped, or concatenated with another listing:

ls /etc |> filter _.isDir |> map _.path
# List all file names
ls |> map _.name

# Find large files (> 1 MB)
ls |> filter (_.size.bytes > 1048576) |> map _.name

# Sort by modification time
ls |> sortBy _.mtime.epoch

# Filter directories only
ls |> filter _.isDir |> map _.name

# Show where symbolic links point
ls |> filter _.isSymlink |> map (fun f -> $"{f.name} -> {f.target}")

# Access nested fields
match head ls with
| Some f -> println $"{f.name}: {f.size}"
| None   -> println "empty directory"

ps

Signature: ps : list<ProcessInfo>

Lists running processes as structured records.

ProcessInfo fields:

Field Type Description
pid int Process ID
ppid int Parent process ID
user str Owning user
cpu float CPU usage percentage
mem float Memory usage percentage
command str Command name
# Filter by CPU usage
ps |> filter (_.cpu > 5.0) |> sortBy _.cpu

# Find processes by name
ps |> filter (_.command |> contains "firefox")

# Count processes per user
ps |> groupBy _.user |> map (fun (u, procs) -> (u, length procs))

jobs

Signature: jobs : list<JobInfo>

Lists background jobs as structured records.

JobInfo fields:

Field Type Description
id int Job number
state str Job state (e.g. "Running", "Stopped")
command str Command string
pid int Process ID
# List running background jobs
jobs |> filter (_.state == "Running") |> map _.command

15.17 FileMode Type

The FileMode type represents Unix file permissions with human-readable display.

Field Type Description
bits int Raw permission bits

Computed properties:

Property Type Description
isReadable bool Any read permission bit set
isWritable bool Any write permission bit set
isExecutable bool Any execute permission bit set
owner int Owner digit (0-7)
group int Group digit (0-7)
other int Other digit (0-7)

FileMode.fromBits

Signature: FileMode.fromBits n : FileMode

Creates a FileMode from raw Unix permission bits.

let m = FileMode.fromBits 493
print m              # rwxr-xr-x
print m.isExecutable # true
print m.owner        # 7

15.18 File Permission Helpers

These functions operate on Unix file modes (accept both FileMode objects and raw int permission bits).

formatMode

Signature: formatMode mode : str

Formats a Unix file mode as a rwxrwxrwx permission string.

ls |> map (fun f -> (f.name, formatMode f.mode))

isReadable

Signature: isReadable mode : bool

Tests if any read permission bit is set.

ls |> filter (fun f -> isReadable f.mode) |> map _.name

isWritable

Signature: isWritable mode : bool

Tests if any write permission bit is set.

ls |> filter (fun f -> isWritable f.mode) |> map _.name

isExecutable

Signature: isExecutable mode : bool

Tests if any execute permission bit is set.

ls |> filter (fun f -> isExecutable f.mode) |> map _.name

15.19 Display Formatting

toText

Signature: toText value : str

Converts any value to its string representation. Useful for converting structured records to plain text.

ls |> map toText |> each println

string

Signature: string value : str

Universal conversion to string. Works with integers, floats, booleans, and strings.

print (string 42)      # => 42
print (string true)    # => true

15.20 HTTP

fetch

Signature: fetch url : result<str, str>

Fetches content from a URL. Returns Ok body on success, Error message on failure.

match fetch "https://example.com" with
| Ok body  -> println body
| Error e  -> println $"Failed: {e}"

httpServe

Signature: httpServe port handler : int

Starts an HTTP server on port (bound to 127.0.0.1). For each request, handler is called with the request path and must return the response body string; the server replies with 200 OK and that body. httpServe blocks, serving connections on the coroutine reactor until interrupted (Ctrl+C), then returns 0.

The handler runs on the single reactor thread, so it should not call blocking builtins; one request is served at a time.

It returns 0 on a clean shutdown (Ctrl+C) or 1 if the port could not be bound. Call it as a bare statement to run a server, or bind the result (let rc = ...) if you want to inspect that exit status.

let handler path = "you asked for " + path
httpServe 8080 handler

Connections are handled asynchronously by the runtime's reactor (non-blocking sockets parked on readiness), not by blocking a thread per connection.


15.21 Timing

time

Signature: time { body } -> TimeSpan

Measures the wall-clock execution time of a computation expression. Returns a TimeSpan that can be inspected, formatted, or piped.

let t = time { [1; 2; 3] |> map (_ * 2) }
print (formatTimeSpan t)
time { sleep (TimeSpan.fromMilliseconds 100) }

15.22 JSON

Json.query

Signature: Json.query path json -> list<string>

Extracts values from a JSON string using a dotted path with [] for array iteration. Returns a list<string> of all matching leaf values (numeric/boolean values converted to strings). On parse error or missing key, returns an empty list.

Path syntax: - .key — access an object property - [] — iterate all array elements - [N] — access a single array element by zero-based index - Chained: .key1[].key2 — access key1 (array), iterate elements, access key2 from each

Use [N] to address one specific element (e.g. .items[0].name). Unlike [], which flattens and skips missing keys — so parallel arrays misalign — [N] isolates a single object, letting you read its fields independently. An out-of-range index yields an empty list.

let json = "{\"name\": \"hello\"}"
let r = Json.query ".name" json
r |> each println
let json = "{\"items\":[{\"name\":\"alpha\"},{\"name\":\"beta\"}]}"
let r = Json.query ".items[].name" json
r |> each println

Indexed access selects a single element:

let json = "{\"items\":[{\"name\":\"alpha\"},{\"name\":\"beta\"}]}"
let r = Json.query ".items[1].name" json
r |> each println

Pipeline usage:

let json = "{\"name\": \"hello\"}"
json |> Json.query ".name" |> each println

List append pattern (useful for merging results from multiple JSON files):

let r = Json.query ".presets[].name" json1 @ Json.query ".presets[].name" json2
r |> each println

15.23 Lazy Evaluation

force

Signature: force lazy<'T> : 'T

Forces evaluation of a lazy value. On first call, evaluates the deferred expression and caches the result. Subsequent calls return the cached value.

let x = lazy (1 + 2)
println (force x)
let x = lazy 42
x |> force |> println

15.24 Lazy Sequences

Lazy sequences build on lazy evaluation to create sequences whose elements are computed on demand.

seq { yield ...; yield! ... }

Syntax: seq { yield expr; yield! seq_expr }

Creates a lazy sequence. yield produces a single element; yield! splices another sequence.

let s = seq { yield 1; yield 2; yield 3 }
s |> toList |> println

toList

Signature: toList seq<'a> : list<'a>

Forces a lazy sequence into an eagerly-evaluated list.

let s = seq { yield 1; yield 2; yield 3 }
s |> toList |> println

15.25 Path

The Path module provides cross-platform filesystem path utilities. The join/dirname/ basename/normalize/isAbsolute operations are purely lexical (they do not touch the filesystem) and use the platform-native separator (\ on Windows, / elsewhere).

Path.temporary_directory

Signature: Path.temporary_directory -> str

Returns the platform's temporary directory path (e.g. /tmp on Linux, C:\Users\...\AppData\Local\Temp on Windows).

let tmp = Path.temporary_directory
println tmp

Path.join

Signature: Path.join a b -> str

Joins two path segments with the platform-native separator. If b is absolute it replaces a (matching typical Path.Combine/os.path.join semantics).

println (Path.basename (Path.join "src" "main.endo"))   # => main.endo

Path.dirname

Signature: Path.dirname p -> str

Returns the parent-directory portion of a path (empty for a bare file name).

println (Path.dirname "a/b/c")   # => a/b

Path.basename

Signature: Path.basename p -> str

Returns the final component (file name) of a path.

println (Path.basename "a/b/c.txt")   # => c.txt

Path.normalize

Signature: Path.normalize p -> str

Lexically collapses . and .. segments and normalizes separators to the platform-native form. Does not touch the filesystem.

println (Path.basename (Path.normalize "a/b/../c"))   # => c

Path.isAbsolute

Signature: Path.isAbsolute p -> bool

Returns true if the path is absolute.

println (Path.isAbsolute "relative/path")   # => false

Path.separator

Signature: Path.separator -> str

The platform's directory separator (\ on Windows, / elsewhere).

Path.delimiter

Signature: Path.delimiter -> str

The platform's PATH-list separator (; on Windows, : elsewhere) — useful when prepending to $PATH.

let sep = Path.delimiter
let export PATH = "/opt/mytool/bin" + sep + (env "PATH" ?| "")

15.26 File I/O

The File module provides file operations. Functions that can fail return result<T, str>.

File.open

Signature: File.open path mode : result<FileHandle, str>

Opens a file. Mode can be "r" (read), "w" (write/truncate), "a" (append), or "rw" (read/write).

File.close

Signature: File.close fd : unit

Closes a file handle.

File.readLine

Signature: File.readLine fd : option<str>

Reads one line from the file. Returns None at end of file.

File.readAll

Signature: File.readAll path : result<str, str>

Reads the entire file as a string.

File.writeAll

Signature: File.writeAll path content : result<unit, str>

Writes a string to a file, creating or truncating it.

File.appendAll

Signature: File.appendAll path content : result<unit, str>

Appends a string to a file.

File.size

Signature: File.size path : result<int, str>

Returns the file size in bytes.

File.exists

Signature: File.exists path : bool

Returns true if the file exists.

println (File.exists "/etc/hostname")

File.delete

Signature: File.delete path : result<unit, str>

Deletes a file.

15.27 Resource Management

let use

Syntax: let use name = expr

Binds a disposable resource and automatically calls its dispose function at scope exit (LIFO order). Types with a registered dispose function (such as FileHandle) require either use or manual.

let use fd = File.open "data.txt" "r"
# fd is automatically closed when the scope exits

let manual

Syntax: let manual name = expr

Binds a disposable resource without automatic cleanup. The user is responsible for calling the dispose function manually. Use when the resource lifetime extends beyond the current scope.


See also: Lists & Collections | Operators & Pipelines | Error Handling | Lazy Evaluation