polar

Standard Library

The standard library is written in Polar and lives in the repository's std/ folder. Import a module with uses Std.<Name>, then call its functions through the module name.

Signatures use the notation from the main concepts: lowercase names are type variables, / {…} lists effects, {| e} passes effects through, and where asks for a trait.

List

List<a> = Nil | Cons(a, List<a>) derive(Eq, Show)

Write lists with [1, 2, 3] rather than Cons. List syntax needs uses Std.List.

Function Description
range(from: Int, to: Int) -> List<Int> Integers from from, up to but not including to
is_empty(xs: List<a>) -> Bool Whether the list has no items
length(xs: List<a>) -> Int The number of items
map(xs: List<a>, f: function(a) -> b / {| e}) -> List<b> / {| e} Applies f to every item
filter(xs: List<a>, keep: function(a) -> Bool / {| e}) -> List<a> / {| e} The items where keep is true
fold(xs: List<a>, initial: b, f: function(b, a) -> b / {| e}) -> b / {| e} Combines the items from left to right, starting with initial
head(xs: List<a>) -> Option<a> The first item
nth(xs: List<a>, index: Int) -> Option<a> The item at index, counting from 0
find(xs: List<a>, test: function(a) -> Bool / {| e}) -> Option<a> / {| e} The first item that passes test
any(xs: List<a>, test: function(a) -> Bool / {| e}) -> Bool / {| e} Whether at least one item passes
all(xs: List<a>, test: function(a) -> Bool / {| e}) -> Bool / {| e} Whether every item passes
append(xs: List<a>, ys: List<a>) -> List<a> xs followed by ys
concat_map(xs: List<a>, f: function(a) -> List<b> / {| e}) -> List<b> / {| e} Maps each item to a list, and joins the lists
reverse(xs: List<a>) -> List<a> The items in reverse order
join(xs: List<String>, separator: String) -> String Joins strings with separator between them

Option

Option<a> = None | Some(a) derive(Eq, Show)
Function Description
map(option: Option<a>, f: function(a) -> b / {| e}) -> Option<b> / {| e} Applies f to the value, if there is one
with_default(option: Option<a>, default: a) -> a The value, or default for None
and_then(option: Option<a>, f: function(a) -> Option<b> / {| e}) -> Option<b> / {| e} Chains a step that might also return None

Result

Result<e, a> = Err(e) | Ok(a) derive(Eq, Show)
Function Description
map(result: Result<e, a>, f: function(a) -> b / {| fx}) -> Result<e, b> / {| fx} Applies f to an Ok value
map_err(result: Result<e, a>, f: function(e) -> g / {| fx}) -> Result<g, a> / {| fx} Applies f to an Err value
and_then(result: Result<e, a>, f: function(a) -> Result<e, b> / {| fx}) -> Result<e, b> / {| fx} Chains a step that might also fail

Map

Map<k, v> = Empty | Entry(k, v, Map<k, v>)

Keys are compared with Eq. Every change returns a new map.

Function Description
empty() -> Map<k, v> A map with no entries
is_empty(map: Map<k, v>) -> Bool Whether there are no entries
size(map: Map<k, v>) -> Int The number of entries
get(map: Map<k, v>, key: k) -> Option<v> where Eq<k> The value for key
has(map: Map<k, v>, key: k) -> Bool where Eq<k> Whether key is present
insert(map: Map<k, v>, key: k, value: v) -> Map<k, v> where Eq<k> Adds or replaces an entry
remove(map: Map<k, v>, key: k) -> Map<k, v> where Eq<k> Removes an entry
keys(map: Map<k, v>) -> List<k> All keys
values(map: Map<k, v>) -> List<v> All values
to_list(map: Map<k, v>) -> List<{ key: k, value: v }> All entries as records

Json

traits
  Json<a> {
    to_json(value: a) -> JsonValue
    from_json(value: JsonValue, path: String) -> Result<JsonError, a>
  }

types
  JsonValue =
    | JNull
    | JBool(Bool)
    | JNumber(Float)
    | JString(String)
    | JArray(List<JsonValue>)
    | JObject(List<{ key: String, value: JsonValue }>)

  JsonError = { path: String, expected: String, found: String }

Derive Json for your own types. Int, Float, String, Bool, List, Option and Result implement it already. See Working with JSON.

Function Description
encode(value: a) -> String where Json<a> JSON text for a value
decode(text: String) -> Result<JsonError, a> where Json<a> Parses and checks JSON text
message(error: JsonError) -> String A readable description of an error

For writing a Json implementation by hand, the module also exports object, tagged, object_fields, field, variant and unknown_variant. They're the building blocks derive(Json) uses.

Ref

Ref<a>

A mutable cell. See Mutable State.

Function Description
new(value: a) -> Ref<a> A new cell holding value
get(ref: Ref<a>) -> a / {Mut} The current value
set(ref: Ref<a>, value: a) -> {} / {Mut} Replaces the value
update(ref: Ref<a>, f: function(a) -> a) -> {} / {Mut} Replaces the value with f(current)
modify(ref: Ref<a>, f: function(a) -> { value: a, result: b }) -> b / {Mut} Replaces the value and returns result

Table

Table<r> = Ref<{ next: Int, rows: List<{ id: Int, row: r }> }>

An in-memory table with automatic numeric ids.

Function Description
new() -> Table<r> An empty table
find(table: Table<r>, id: Int) -> Option<r> / {Mut} The row with that id
all(table: Table<r>) -> List<r> / {Mut} Every row, in insertion order
size(table: Table<r>) -> Int / {Mut} The number of rows
insert(table: Table<r>, make: function(Int) -> r) -> r / {Mut} Adds the row make(id) and returns it
update(table: Table<r>, id: Int, row: r) -> Bool / {Mut} Replaces a row. Returns whether it existed
delete(table: Table<r>, id: Int) -> Bool / {Mut} Removes a row. Returns whether it existed

Id

Id<a> = Id(Int)

A typed id. An Id<Post> and an Id<User> are different types, so you can't pass one where the other is expected. Id implements Eq, Show and Json.

Function Description
value(id: Id<a>) -> Int The number inside

Http

Header = { name: String, value: String }

Request = {
  method: String,
  path: String,
  query: String,
  headers: List<Header>,
  body: String,
}

Response = { status: Int, headers: List<Header>, body: String }
Function Description
header(headers: List<Header>, name: String) -> String The value of a header, matched without regard to case, or "" if it's missing

Url

Pair = { name: String, value: String } derive(Eq, Show)
Function Description
encode(text: String) -> String Percent-encodes text for a URL component
decode(text: String) -> String Percent-decodes text. Never throws: invalid input comes back unchanged
parse_query(text: String) -> List<Pair> Parses a=1&b=2, with or without a leading ?. + becomes a space
build_query(pairs: List<Pair>) -> String The reverse of parse_query

Dom

Provides the Browser host and the Dom effect. See Running in the Browser.

effects
  Dom in Browser {
    append_text(tag: String, text: String) -> {}
    append_html(html: String) -> {}
    has(id: String) -> Bool
    set_html(id: String, html: String) -> {}
  }

Process

Provides the Node host and the Process effect. See Command-Line Programs.

types
  EnvVar = { name: String, value: String }

  RunOptions = { cwd: Option<String>, env: List<EnvVar> }

  Output = { code: Int, stdout: String, stderr: String }

effects
  Process in Node {
    args() -> List<String>
    env(name: String) -> Option<String>
    cwd() -> String
    set_exit_code(code: Int) -> {}
    run(command: String, args: List<String>, options: RunOptions) -> Int
    output(command: String, args: List<String>, options: RunOptions) -> Output
  }
Function Description
inherit() -> RunOptions Run in the current directory and environment
in_dir(options: RunOptions, dir: String) -> RunOptions Run in dir instead
with_env(options: RunOptions, name: String, value: String) -> RunOptions Add an environment variable

Fs

Reads and writes text files on the Node host. See Command-Line Programs.

types
  FsError = { code: String, path: String, message: String } derive(Show)

effects
  Fs in Node {
    read(path: String) -> Result<FsError, String>
    write(path: String, text: String) -> Result<FsError, {}>
    append(path: String, text: String) -> Result<FsError, {}>
    exists(path: String) -> Bool
    is_dir(path: String) -> Bool
    mkdir_all(path: String) -> Result<FsError, {}>
    list(path: String) -> Result<FsError, List<String>>
    walk(path: String) -> Result<FsError, List<String>>
    remove(path: String) -> Result<FsError, {}>
    remove_all(path: String) -> Result<FsError, {}>
  }
Operation Description
read The file's text, as UTF-8
write Creates or truncates the file. A missing parent directory is ENOENT
append Creates the file or adds to its end
exists, is_dir Whether the path exists, or is a directory. Never fail
mkdir_all Creates a directory and its parents, like mkdir -p. Fine if it exists
list The names in a directory, sorted
walk Every file below a directory, as sorted paths relative to it with / separators. Doesn't follow symlinks
remove Removes a file or an empty directory
remove_all Removes a path and everything below it. Fine if it's missing

An FsError's code is Node's: ENOENT, EEXIST, ENOTDIR, EACCES and so on, so you can match on it.

Path

Pure functions on POSIX paths, written in Polar. They don't touch the file system and work on every host.

Function Description
join(base: String, part: String) -> String Joins and normalizes. An absolute part wins: join("a", "/b") is /b
dirname(path: String) -> String dirname("/a/b.px") is /a, dirname("b.px") is .
basename(path: String) -> String basename("/a/b.px") is b.px
extension(path: String) -> String .px, or "" when there is none. extension(".gitignore") is ""
normalize(path: String) -> String Collapses ., .. and //, keeping a leading /. normalize("") is .
is_absolute(path: String) -> Bool Whether the path starts with /
relative(from: String, to: String) -> String The path from from to to: relative("/w/ticket", "/w/apps/demo") is ../apps/demo

Regex

Regular expressions, using the host's JavaScript syntax with the u flag and no other flags. Matching is unanchored, so write ^…$ to match the whole text. It works on both Node and Browser. A pattern with catastrophic backtracking, like (a+)+$, hangs as it does in JavaScript. Regex has Eq and Show, shown as /pattern/.

types
  Regex = Regex(String)
Function Description
compile(pattern: String) -> Result<String, Regex> Ok for a valid pattern, or Err with the engine's message
pattern(regex: Regex) -> String The string given to compile
is_match(regex: Regex, text: String) -> Bool Whether the pattern matches anywhere in text
find(regex: Regex, text: String) -> Option<String> The first match's text, or None. An empty match is Some("")
find_all(regex: Regex, text: String) -> List<String> Every non-overlapping match, in order
replace(regex: Regex, text: String, with: String) -> String Replaces every match with with verbatim; $1 is not expanded
matches(pattern: String, text: String) -> Bool is_match of compile(pattern), and false for a bad pattern

Time

An instant in UTC, held as milliseconds since the Unix epoch, and the Clock effect that reads it. Clock works on both Node and Browser. A program can bind it itself, to pin the time in a test. Time has Eq, Show and Json: its JSON form is an ISO-8601 string like "2026-10-02T09:30:00.123Z".

types
  Time = Time(Int)

effects
  Clock {
    now() -> Time
  }
Function Description
from_millis(ms: Int) -> Time Builds a Time from milliseconds since the epoch
millis(time: Time) -> Int The milliseconds since the epoch
to_iso(time: Time) -> String UTC with milliseconds: 2026-10-02T09:30:00.123Z
from_iso(text: String) -> Option<Time> Accepts YYYY-MM-DDTHH:MM:SS[.fff](Z|±HH:MM) and SQLite's YYYY-MM-DD HH:MM:SS[.fff] (read as UTC). Anything else, like 2026-02-30, is None
compact(time: Time) -> String YYYYMMDD_HHMMSS in UTC: 20261002_093000
compare(a: Time, b: Time) -> Int -1, 0 or 1
before(a: Time, b: Time) -> Bool Whether a is earlier than b

Crypto

Hashing, signing and random tokens over node:crypto. Crypto works on Node only, so a Browser build can't use Random. Randomness is the Random effect, so a function that makes a token says so in its type, and a program can bind Random to a fixed value in a test. There is no Bytes type: random bytes are a hex string.

effects
  Random {
    bytes(count: Int) -> String
  }
Function Description
sha256(text: String) -> String Lowercase hex SHA-256 of the string's UTF-8 bytes
hmac_sha256(key: String, text: String) -> String Lowercase hex HMAC-SHA-256 of text under key
equal(a: String, b: String) -> Bool Constant-time comparison; false for different lengths. Use it to check a signature
base64url(text: String) -> String URL-safe base64 of the UTF-8 bytes, no padding
from_base64url(text: String) -> Option<String> Decodes base64url output; None for invalid text
Random.bytes(count: Int) -> String count random bytes as 2 × count lowercase hex characters

Assert

Failed = Failed(String)
Function Description
assert(check: Bool) -> {} / {Throws<Failed>} Throws Failed when check is false
equal(actual: a, expected: a) -> {} / {Throws<Failed>} where Eq<a>, Show<a> Throws Failed("expected <expected>, got <actual>") when the values differ
not_equal(actual: a, other: a) -> {} / {Throws<Failed>} where Eq<a>, Show<a> Throws Failed("expected a value other than <other>") when the values are equal
fail(message: String) -> a / {Throws<Failed>} Always throws Failed(message). It has any type, so it fits any match arm

Math

Constant Description
pi: Float 3.141592653589793

Prelude

Always in scope, without a uses.

traits
  Eq<a> {
    eq(x: a, y: a) -> Bool
  }

  Show<a> {
    show(value: a) -> String
  }

Int, Float, String and Bool implement both. == uses Eq, and #{…} uses Show.