Skip to content

Standard library

This page lists everything built into Tessel that isn’t a view or a modifier: functions like print and readFile, conversions, math, random numbers, files and folders, JSON and binary encoding, dates, the system, networking, timers and background work, and the properties and methods of Int, Float, String, Data, lists ([T]) and dictionaries ([K: V]). The outputs shown in comments come from running the examples.

Signatures use the same notation as the Views page: arguments are passed with their labels, in order, except those marked _. For example, writeFile(path: String, text: String) -> Bool is called as writeFile(path: "notes.txt", text: "Hello").

When a function or method has exactly one parameter, you may leave out its label: readFile("notes.txt") means readFile(path: "notes.txt"), and list.remove(0) means list.remove(at: 0).

A last parameter of function type can be written as a block after the call:

after(seconds: 1) {
print("a second later")
}
print(_ value: Int, Float, Bool, String, Range or an enum, terminator: String = "\n")

Writes a value to standard output, followed by terminator (a new line, unless you say otherwise).

print("Hello") // Hello
print(42) // 42
print(2.0) // 2.0
print(true) // true
print(Mode.grid) // grid
print(0..3) // 0..3
print("Name: ", terminator: "") // no line break: the answer goes on the same line
  • A Float always shows a decimal point (2.0), so it reads differently from an Int.
  • An enum value prints as its case name, and a range as start..end.
  • Lists, dictionaries, sets and tuples print as they’re written in code: [1, 2], ["tea": 3], (1, "one"), with text inside them in quotes and nil for a missing value. Their items must be printable this way (numbers, text, Bools, enums, and lists, dictionaries, sets and tuples of those).
  • Structs and optionals can’t be printed directly. Print a field instead, or give an optional a default with ??.
  • In an app, the output goes to the terminal the app was started from (or to the console of the Tessel IDE).
printError(_ value: …, terminator: String = "\n")

Like print, but writes to standard error. Use it for error messages and warnings from a command-line program, so they don’t mix with its real output when that’s piped into a file or another program:

if let text = readFile(path) {
print(text.uppercase())
} else {
printError("can't read {path}")
exit(1)
}
readLine() -> String?

Reads the next line from standard input: what the user types, up to Enter, or the next line of input piped into the program. The line break isn’t included. At the end of the input, it returns nil.

fn main() {
print("What's your name? ", terminator: "")
let name = readLine() ?? "stranger"
print("Hello, {name}!")
}

Read every line until the input ends with while let:

fn main() {
var total = 0
while let line = readLine() {
total += Int(line.trim()) ?? 0
}
print("total: {total}")
}
Terminal window
printf '1\n2\n3\n' | tessel run sum.tsl # total: 6
readInput() -> String

Reads all of standard input at once, until it ends; empty if there’s none. It suits programs that process piped text, like cat notes.txt | tessel run count.tsl:

fn main() {
let text = readInput()
print("{text.lines().count} lines, {text.words().count} words")
}

In an app (which has a window, not a terminal) there’s usually no input: readLine() returns nil and readInput() an empty string.

Int(_ value: Int or Float) -> Int
Int(_ value: String) -> Int?

Converts a number or text to an Int.

print(Int(3.99)) // 3
print(Int(-3.99)) // -3
print(Int("42") ?? 0) // 42
print(Int(" 42 ") ?? 0) // 42
print(Int("4.2") ?? -1) // -1
  • From a Float, the fraction is dropped (rounding toward zero). Values too big for an Int become the largest (or smallest) Int.
  • From a String, the result is optional: nil if the text isn’t a whole number. Spaces around the number are ignored; a leading + or - is allowed.
Float(_ value: Int or Float) -> Float
Float(_ value: String) -> Float?

Converts a number or text to a Float.

print(Float(3)) // 3.0
print(Float("4.5") ?? 0.0) // 4.5
print(Float("1e3") ?? 0.0) // 1000.0
print(Float("abc") ?? 0.0) // 0.0

From a String, the result is nil if the text isn’t a number. Spaces around it are ignored, and exponents like 1e3 are understood.

String(_ value: Int, Float, Bool, String or an enum) -> String

Turns a value into text, exactly as print would show it.

let label = String(3.0)
print(label.count) // 3

Usually string interpolation is simpler: "{value}" gives the same text.

print(sqrt(16.0)) // 4.0
print(pow(2.0, 10.0)) // 1024.0
print(sin(pi / 2.0)) // 1.0
print(max(0, 2.5)) // 2.5
print(abs(-4)) // 4

Math functions take their arguments without labels, in the order shown.

FunctionReturns
sqrt(x)The square root of x.
pow(x, power)x raised to power: pow(2.0, 3.0) is 8.0.
exp(x)e raised to x.
log(x)The natural logarithm (base e) of x.
log10(x)The base-10 logarithm of x.
sin(x), cos(x), tan(x)The sine, cosine and tangent of the angle x.
asin(x), acos(x), atan(x)The angle whose sine, cosine or tangent is x.
atan2(y, x)The angle from the positive x axis to the point (x, y), from -pi to pi. Note that y comes first.
floor(x)x rounded down: floor(-2.7) is -3.0.
ceil(x)x rounded up: ceil(2.1) is 3.0.
round(x)x rounded to the nearest whole number; halves round away from zero, so round(2.5) is 3.0 and round(-2.5) is -3.0.
abs(x)x without its sign.
min(a, b)The smaller of a and b.
max(a, b)The larger of a and b.
piThe constant π, 3.141592653589793.
  • All of these take and return a Float, except abs, min and max: they work on Ints or Floats, and return the same type. Both values of min and max must have the same type. A whole-number literal next to a Float is read as a Float, so max(0, x) works for a Float x. To mix an Int variable with a Float, convert it with Float().
  • Whole-number literals are read as Floats where a Float is expected, so sqrt(16) works too.
  • Angles are in radians. To convert from degrees, multiply by pi / 180.0: sin(30.0 * pi / 180.0).
  • floor, ceil and round return a Float. Wrap them in Int() to get an Int: Int(round(2.6)) is 3.
  • pi is a value, not a function: write pi, not pi().
  • The results follow the usual floating-point rules and never stop the program: sqrt(-1.0) is NaN (“not a number”) and log(0.0) is -inf.
random(_ range: Range) -> Int

A random Int from the range’s start up to, but not including, its end.

let die = random(1..7) // 1, 2, 3, 4, 5 or 6
let index = random(0..names.count)

The range must have at least one number: an empty range (like 3..3, or n..n when n is the same) stops the program with a runtime error.

randomFloat() -> Float

A random Float from 0.0 up to, but not including, 1.0.

if randomFloat() < 0.1 {
print("a one-in-ten chance")
}
let angle = randomFloat() * 2.0 * pi

To pick or mix up list items, use randomElement and shuffled.

Paths are ordinary strings. A relative path like "notes.txt" is relative to the program’s current folder (see currentFolder). Use / between folder names.

readFile(path: String) -> String?

The whole file as text, or nil if it doesn’t exist, can’t be read, or isn’t valid UTF-8 text.

if let text = readFile("notes.txt") {
print(text.lines().count)
} else {
print("no notes yet")
}
writeFile(path: String, text: String) -> Bool

Writes text to a file, replacing the file if it exists. Returns whether it worked. The folder must already exist (see createFolder).

let ok = writeFile(path: "notes.txt", text: "Buy milk\n")
print(ok) // true
listFolder(path: String) -> [String]?

The names of the files and folders in a folder, sorted, or nil if the folder can’t be read.

for name in listFolder(".") ?? [] {
print(name)
}
  • The result has names only, not full paths: join them yourself, like "{folder}/{name}".
  • Hidden files (names starting with .) are included.
  • The names are sorted by character code, so "B" comes before "a".
fileExists(path: String) -> Bool

Whether a file or folder exists at path.

print(fileExists("notes.txt"))
isFolder(path: String) -> Bool

Whether path is an existing folder.

print(isFolder(".")) // true
deleteFile(path: String) -> Bool

Deletes a file. Returns whether it worked. It doesn’t delete folders (it returns false for a folder).

if deleteFile("notes.txt") {
print("deleted")
}
createFolder(path: String) -> Bool

Creates a folder, and any missing folders above it. Returns whether it worked; a folder that already exists counts as success.

print(createFolder("backups/2026")) // true
renameFile(from: String, to: String) -> Bool

Renames or moves a file or folder. Returns whether it worked. An existing file at to may be replaced.

let moved = renameFile(from: "notes.txt", to: "backups/notes.txt")
currentFolder() -> String

The full path of the program’s current folder: the folder it was started from. Relative paths are relative to this folder.

print(currentFolder()) // for example /Users/ada/Projects/notes
appendFile(path: String, text: String) -> Bool

Adds text to the end of a file, creating the file if it doesn’t exist. Returns whether it worked. Handy for logs.

writeFile(path: "log.txt", text: "started\n")
appendFile(path: "log.txt", text: "saved\n")
print((readFile("log.txt") ?? "").lines().count) // 2
copyFile(from: String, to: String) -> Bool

Copies a file. Returns whether it worked. An existing file at to is replaced. It doesn’t copy folders.

print(copyFile(from: "log.txt", to: "log-backup.txt")) // true
fileSize(_ path: String) -> Int?

The size of a file in bytes, or nil if it doesn’t exist.

print(fileSize("log.txt") ?? 0) // 14
print(fileSize("missing.txt") ?? -1) // -1
modifiedTime(_ path: String) -> Float?

When the file last changed, as a time (seconds since 1970), or nil if it doesn’t exist.

if let time = modifiedTime("log.txt") {
print("Last changed {formatDate(time)}")
}
readData(_ path: String) -> Data?

The whole file as raw bytes (Data), or nil if it can’t be read. Use it for files that aren’t text, such as images or files written with toBinary.

if let data = readData("header.bin") {
print(data.count)
}
writeData(path: String, data: Data) -> Bool

Writes bytes to a file, replacing the file if it exists. Returns whether it worked.

let bytes = Data(bytes: [137, 80, 78, 71])
print(writeData(path: "header.bin", data: bytes)) // true

These functions give you the usual places to keep files, and build paths that work on every system. To take a path apart, use the String properties fileName, fileExtension and folder.

FunctionReturns
homeFolder()The user’s home folder, like /Users/ada (or C:\Users\ada on Windows).
documentsFolder()The user’s Documents folder.
tempFolder()A folder for temporary files.
appDataFolder(_ name: String)A folder for your app’s own files (settings, caches), created if needed.
joinPath(_ folder: String, _ name: String)folder and name joined with the system’s separator.
appDataFolder(_ name: String) -> String

The folder where an app keeps its own files, like settings, with name added. It’s created if it doesn’t exist yet.

  • On macOS: ~/Library/Application Support/<name>.
  • On Windows: %APPDATA%\<name>.
let settings = joinPath(appDataFolder("Notes"), "settings.json")

Use your app’s name for name, so apps don’t mix up their files.

joinPath(_ folder: String, _ name: String) -> String

Joins a folder and a name with / (\ on Windows). Extra separators where they meet are removed. If folder is "", the result is just name.

print(joinPath("/Users/ada/", "notes.txt")) // /Users/ada/notes.txt
let notes = joinPath(documentsFolder(), "Notes")

openFile and saveFile show the system’s standard dialog for choosing a file, and return the path the user picked. They’re meant for apps: call them from a button’s action, a shortcut or another block that runs when the user does something. See Documents and custom file types for a complete example.

openFile(title: String = "", types: [String] = []) -> String?

Asks the user to pick an existing file. Returns its full path, or nil if the user cancelled.

Button("Open…") {
if let path = openFile(title: "Open a note", types: ["note"]) {
text = readFile(path) ?? ""
}
}
  • title is shown in the dialog’s title bar (not on every system).
  • types limits the choice to files with these extensions. Write them without the dot: ["note"], or ["txt", "md"] for several. An empty list allows any file.
saveFile(title: String = "", name: String = "", types: [String] = []) -> String?

Asks the user where to save a file. Returns the chosen path, or nil if the user cancelled. It doesn’t write anything: pass the path to writeFile.

Button("Save…") {
if let path = saveFile(title: "Save the note", name: "Untitled.note", types: ["note"]) {
writeFile(path: path, text: text)
}
}
  • name is the file name the dialog suggests.
  • types works as for openFile. If the user types a name without an extension, the first extension in types is added, so the example above always gives a path ending in .note.
  • The system dialog asks the user before replacing an existing file.

These four functions turn your values into text or bytes, to save in a file or send over the network, and read them back. They work with structs, enums, lists, dictionaries, optionals, ranges, Data and the basic types, nested as deeply as you like. See Saving data as JSON for a complete example.

enum Priority { low, high }
struct Task {
title: String
done: Bool = false
priority: Priority = Priority.low
}
fn main() {
let task = Task(title: "Write docs", priority: Priority.high)
let text = toJson(task)
print(text) // {"title":"Write docs","done":false,"priority":"high"}
let back: Task? = fromJson(text)
print(back == task) // true
}
toJson(_ value, pretty: Bool = false) -> String

The value as JSON text. With pretty: true, the text is spread over several lines and indented, which is easier to read:

print(toJson(task, pretty: true))
{
"title": "Write docs",
"done": false,
"priority": "high"
}
fromJson(_ text: String) -> T?

Reads a value from JSON text. It gives nil if the text isn’t valid JSON or doesn’t fit the type.

fromJson finds out which type to read from where its result goes, so give the result a type:

let loaded: Task? = fromJson(text)
let ages: [String: Int] = fromJson(text) ?? [:]
if let numbers: [Int] = fromJson("[4, 8, 15]") {
print(numbers.sum()) // 27
}

Without a type, tessel check asks for one:

error: can't tell what type `fromJson` should read
--> main.tsl:2:16
|
2 | let data = fromJson(text)
| ^^^^^^^^^^^^^^
|
= help: give the result a type, like `let project: Project? = fromJson(…)`
toBinary(_ value) -> Data

The value as compact bytes (MessagePack), smaller and faster to read than JSON, but not readable by people. Save it with writeData.

let data = toBinary(task)
print(data.count) // 38
fromBinary(_ data: Data) -> T?

Reads a value back from bytes made by toBinary, or nil if they don’t fit the type. Like fromJson, it needs to know the type:

if let data = readData("task.bin") {
let task: Task? = fromBinary(data)
}
ValueJSONExample
Int, Float, Bool, StringThe same value42, 1.5, true, "hi"
A structAn object with a key for each field{"x":1.0,"y":2.0}
An enum case without valuesIts name, as a string"high"
An enum case with valuesAn object with the case’s name as key{"custom":{"name":"feet","factor":0.3048}}
A listAn array[1,2,3]
A dictionaryAn object; Int and Bool keys are written as strings{"ada":36}
An optionalThe value, or null for nilnull
A rangeAn array with its start and end[0,5]
DataA base64 string"aGk="

toBinary writes the same structure in MessagePack, which can hold a few things JSON can’t, so it keeps them as they are: Data as raw bytes (not base64), Int and Bool dictionary keys as numbers and booleans, and NaN and infinity as floats. That also makes binary files smaller.

When reading:

  • Fields that are missing take their default value (or nil for an optional field); a missing field without a default makes the result nil. This lets you add fields with defaults to a struct and still read files saved before.
  • Keys that the struct doesn’t have are ignored.
  • A value of the wrong kind (like a number where a string belongs), an unknown enum case, or text that isn’t valid JSON makes the whole result nil.
  • An Int also accepts a whole number written with a decimal point, like 2.0.
  • Float values that aren’t numbers (NaN, infinity) have no JSON form; they’re written as null. Reading null into a Float gives NaN, and into a Float? gives nil. (Binary keeps them exactly.)
  • A whole document that is just null reads as nil, whatever the type: let x: Float? = fromJson("null") is nil.

Functions and views can’t be saved. tessel check points out the field that is the problem:

struct Counter {
count: Int
onChange: fn(Int)
}
error: `Counter` can't be saved with `toJson`
--> main.tsl:8:23
|
8 | let text = toJson(c)
| ^
|
= help: field `onChange`: functions can't be saved

Dictionary keys must be String, Int, Bool or an enum.

Data holds raw bytes: the contents of a file that isn’t text, the result of toBinary, or a download.

let empty = Data()
let bytes = Data(bytes: [72, 105, 33])
let text = Data(text: "héllo")
print(bytes.text ?? "not text") // Hi!
print(text.count) // 6
print(text[1]) // 195
print(bytes == Data(text: "Hi!")) // true
Creating
Data()No bytes.
Data(bytes: [Int])The given bytes. Each Int should be from 0 to 255; other values are wrapped into that range.
Data(text: String)The text’s bytes in UTF-8.
MemberReturns
countIntNumber of bytes.
isEmptyBoolWhether there are no bytes.
bytes[Int]The bytes, each from 0 to 255.
textString?The bytes read as UTF-8 text, or nil if they aren’t valid UTF-8.
base64StringThe bytes as base64 text.
  • data[i] is the byte at index i (from 0), as an Int. An index outside the data stops the program with an error. Data can’t be changed in place; make a new one instead.
  • == and != compare the bytes.
  • Read and write files of bytes with readData and writeData.

Base64 turns bytes into plain text, for places that only take text (like a JSON field or a URL).

data.base64 -> String
decodeBase64(_ text: String) -> Data?
print(Data(text: "Hi!").base64) // SGkh
if let decoded = decodeBase64("SGkh") {
print(decoded.text ?? "") // Hi!
}

decodeBase64 returns nil if the text isn’t base64. Spaces and line breaks around it are ignored.

A point in time is a Float: the number of seconds since 1 January 1970 (UTC). That makes times easy to store and compare, and to do arithmetic with: add 60.0 for a minute later, or 86400.0 for a day.

let start = now()
let deadline = date(year: 2026, month: 12, day: 31, hour: 17)
print(formatDate(deadline, format: "EEEE, d MMMM yyyy")) // Thursday, 31 December 2026
let days = Int((deadline - start) / 86400.0)

Dates are shown and read in the computer’s time zone, unless you name another one (see Time zones).

now() -> Float

The current time. Use it to time something, too:

let started = now()
doWork()
print("took {(now() - started).formatted(decimals: 2)} seconds")
date(year: Int, month: Int, day: Int,
hour: Int = 0, minute: Int = 0, second: Int = 0, timeZone: String = "") -> Float

The time of a calendar date, in the computer’s time zone or the one named. Months and days count from 1. A date that doesn’t exist, like 30 February, gives NaN (see isNaN).

let t = date(year: 2026, month: 3, day: 7, hour: 9, minute: 5)
print(formatDate(t)) // 2026-03-07 09:05
let launch = date(year: 2026, month: 3, day: 7, hour: 9, timeZone: "Asia/Tokyo")

A time the clocks skip (when they’re set forward for summer time) gives the moment an hour later; a time they show twice (when they’re set back) gives the first of the two.

formatDate(_ time: Float, format: String = "yyyy-MM-dd HH:mm", timeZone: String = "") -> String

A time as text, in the computer’s time zone or the one named. format is a pattern where these letters are replaced; anything else is copied as it is:

PatternMeansExample
yyyyYear2026
yyYear, two digits26
MMMMMonth nameMarch
MMMShort month nameMar
MMMonth, two digits03
MMonth3
ddDay of the month, two digits07
dDay of the month7
EEEEWeekdaySaturday
EEEShort weekdaySat
HHHour (0–23), two digits09
HHour (0–23)9
hhHour (1–12), two digits09
hHour (1–12)9
mmMinute, two digits05
mMinute5
ssSecond, two digits30
sSecond30
aAM or PMAM
SSSThousandths of a second250
ZHow far the zone is from UTC+0100
ZZZZZThe same, with a colon+01:00
'text'The text as it isat
''A single quote'
let t = date(year: 2026, month: 3, day: 7, hour: 9, minute: 5, second: 30)
print(formatDate(t, format: "yyyy-MM-dd HH:mm:ss")) // 2026-03-07 09:05:30
print(formatDate(t, format: "d MMM yy")) // 7 Mar 26
print(formatDate(t, format: "EEE, MMMM d")) // Sat, March 7
print(formatDate(t, format: "h:mm a")) // 9:05 AM
print(formatDate(t, format: "dd/MM/yyyy")) // 07/03/2026
print(formatDate(t, format: "d MMM 'at' HH:mm")) // 7 Mar at 09:05

Month and weekday names are in English. Put words in single quotes, like 'at', so their letters aren’t replaced.

The format "iso" gives the form used on the internet and in JSON (ISO 8601): the date, a T, the time, and how far the zone is from UTC.

print(formatDate(t, format: "iso", timeZone: "Europe/Paris")) // 2026-03-07T09:05:30+01:00
print(formatDate(t, format: "iso", timeZone: "UTC")) // 2026-03-07T08:05:30Z
parseDate(_ text: String, format: String = "yyyy-MM-dd", timeZone: String = "") -> Float?

Reads a time from text written in format (the same patterns as formatDate), or nil if the text doesn’t match. Without an hour, the time is midnight. The text is taken to be in the computer’s time zone, or the one named, unless it says itself how far from UTC it is (with Z or ZZZZZ in the format).

let day = parseDate("2026-03-07")
let meeting = parseDate("07/03/2026 09:05", format: "dd/MM/yyyy HH:mm")
print(parseDate("tomorrow") == nil) // true

The whole text must match: parseDate("2026-03-07 09:05") is nil, because the default format has no time.

The format "iso" reads the internet’s form, as servers and JSON usually send it: with a distance from UTC (+01:00, or Z for none), with or without fractions of a second. Without a distance, or with only a date, the timeZone: counts.

let sent = parseDate("2026-03-07T09:05:30+01:00", format: "iso")
let utc = parseDate("2026-03-07T08:05:30.250Z", format: "iso")
let noon = parseDate("2026-03-07 12:00", format: "yyyy-MM-dd HH:mm", timeZone: "Asia/Tokyo")
dateParts(_ time: Float, timeZone: String = "") -> DateParts

What the calendar and the clock say at a moment: a DateParts struct.

FieldType
yearInt
monthInt1 for January to 12.
dayIntThe day of the month, from 1.
hourInt0 to 23.
minuteInt
secondInt
weekdayInt1 for Monday to 7 for Sunday.
dayOfYearInt1 for 1 January.
let today = dateParts(now())
if today.weekday >= 6 {
print("It's the weekend")
}
print("Day {today.dayOfYear} of {today.year}")
addToDate(_ time: Float, years: Int = 0, months: Int = 0, days: Int = 0,
hours: Int = 0, minutes: Int = 0, seconds: Int = 0, timeZone: String = "") -> Float

A time that’s some years, months or days later on the calendar (or earlier, with negative numbers), and then some hours, minutes and seconds later.

let due = addToDate(now(), days: 14)
let renewal = addToDate(start, years: 1)
let reminder = addToDate(meeting, minutes: -15)

Adding 86400.0 seconds isn’t always the next day at the same time, and a month isn’t a fixed number of days; addToDate follows the calendar:

  • A day later is the same time of day, even when the clocks change in between (that day is 23 or 25 hours long).
  • A month after 31 January is the last day of February: the day is brought back into a shorter month.
  • Hours, minutes and seconds are simply that much time later.
daysBetween(_ from: Float, _ to: Float, timeZone: String = "") -> Int

How many days later to is than from on the calendar: how many times the date changes between them. It’s negative if to is earlier. Two minutes that cross midnight are a day apart; 23 hours within one date are not.

let left = daysBetween(now(), deadline)
print(if left == 0 { "Due today" } else { "{left} days left" })

Every date function has a timeZone: argument. Left out, it’s the computer’s time zone. Otherwise it’s one of:

Time zoneExample
A name"Europe/Paris", "America/New_York", "Asia/Tokyo"A region and a city, from the time zone database that computers share. It knows that region’s summer time, now and in the past.
"UTC"The time the others are counted from.
A distance from UTC"+05:30", "-08:00", "+2"Always that far, with no summer time.
let t = now()
print(formatDate(t, format: "HH:mm", timeZone: "Asia/Tokyo"))
print(formatDate(t, format: "HH:mm", timeZone: "America/New_York"))

A time itself has no time zone: it’s one moment everywhere. The zone only decides what the calendar and the clock say at that moment.

A time zone that doesn’t exist is treated like a date that doesn’t: formatDate gives "", parseDate gives nil, date and addToDate give NaN, and dateParts, daysBetween and timeZoneOffset give zeros. Check a name that comes from outside the program with timeZones.

timeZones() -> [String]

The names of all time zones, in alphabetical order: about 600 of them. Use it to offer a choice, or to check a name:

if timeZones().contains(name) {
zone = name
}
localTimeZone() -> String

The name of the computer’s time zone, like "Europe/Paris", or "" if it can’t be found out.

timeZoneOffset(_ time: Float, timeZone: String = "") -> Int

How many seconds the zone’s clocks are ahead of UTC at that moment (negative if they’re behind). It depends on the moment, because of summer time.

let hours = timeZoneOffset(now(), timeZone: "America/New_York") / 3600 // -5, or -4 in summer
environment(name: String) -> String?

The value of an environment variable, or nil if it isn’t set.

let home = environment("HOME") ?? "unknown"
arguments() -> [String]

The arguments the program was started with, without the program’s own name.

fn main() {
let args = arguments()
if args.isEmpty {
print("usage: greet <name>")
exit(1)
}
print("Hello, {args[0]}!")
}

Run with tessel run greet.tsl Ada (or ./greet Ada once built), this prints Hello, Ada!.

exit(_ code: Int = 0)

Ends the program right away, with an exit code: 0 means success, anything else a failure. Timers, downloads and other work still waiting are dropped.

platform() -> String

The system the program is running on: "macos" or "windows".

let shortcut = if platform() == "macos" { "Cmd+S" } else { "Ctrl+S" }
isDarkMode() -> Bool

Whether the app is showing in its dark appearance: true when the system is dark (or the app says appearance: .dark). The UI is built again when the appearance changes, so a view can simply ask:

let card: Color = if isDarkMode() { .rgb(44, 44, 46) } else { .rgb(250, 246, 240) }

In a program without an app, it’s false. See Light and dark.

openURL(_ url: String) -> Bool

Opens a web page in the browser, or a file or folder with its usual app. Returns whether it worked.

Button("Help") { openURL("https://tessel-lang.netlify.app") }
Button("Show folder") { openURL(appDataFolder("Notes")) }

In a headless test run, nothing opens: it prints (open …) instead.

copyToClipboard(_ text: String) -> Bool

Puts text on the system clipboard, as if the user had copied it. Returns whether it worked.

Button("Copy link") { copyToClipboard(link) }
clipboardText() -> String?

The text on the clipboard, or nil if it has none (for example, when an image was copied).

Button("Paste") { text += clipboardText() ?? "" }

A headless test run uses a clipboard of its own, shared with its text fields’ copy and paste, so tests never touch yours.

runCommand(path: String, args: [String] = [],
onOutput: fn(String), onExit: fn(Int)) -> Int

Starts another program and returns right away with an id for stopCommand. The program runs in the background:

  • onOutput is called with each line the program prints, from both its standard output and standard error, without the line ending.
  • onExit is called once, after all the output, with the exit code. The code is -1 if the program couldn’t be started or was stopped by a signal. If it couldn’t be started, onOutput first receives a line starting with error: can't start.
app Runner {
state output: [String] = []
Button("List files") {
output = []
runCommand(path: "ls", args: ["-l"], onOutput: { line in
output.append(line)
}) { code in
output.append("finished with code {code}")
}
}
for line in output {
Text(line)
}
}
  • path is the program to run: a path like /bin/ls, or a name looked up in the folders of the PATH environment variable. It isn’t run through a shell, so each argument goes in args separately, and shell features like * or | don’t apply.
  • The program gets no input.
  • Both blocks run on the UI thread, so they can change state directly.
stopCommand(id: Int)

Stops a program started with runCommand. Its onExit block still runs. Stopping a program that has already finished does nothing.

app Watcher {
state running = -1
Button("Start") {
running = runCommand(path: "sleep", args: ["60"], onOutput: { line in print(line) }) { code in
running = -1
}
}
Button("Stop") { stopCommand(running) }
}

The functions below finish their work later: the network, timers and background work. They take a block (a callback) that Tessel runs when the work is done. Callbacks run on the UI thread in an app, so they can change state directly. They work in a program with only fn main() too: after main returns, the program keeps running until no timers, requests, WebSockets or background work are left, then exits. See Files, commands and timers.

fetch(url: String, done: fn(String?))

Loads a URL in the background, then calls done with the text, or nil if loading failed.

app Weather {
state report = "Loading…"
Text(report).onAppear {
fetch(url: "https://example.com/weather.txt") { text in
report = text ?? "Could not load the weather"
}
}
}
  • http:// and https:// URLs are loaded with a GET request. An error status (like 404) counts as failure.
  • file:// URLs read a local file: file:///Users/ada/notes.txt.
  • The response is read as text.

For other methods (like POST), headers, or the status code, use request.

request(url: String, method: String = "GET", headers: [String: String] = [:],
body: String = "", done: fn(HttpResponse)) -> Int

Sends an HTTP request in the background, then calls done with the response. It returns an id, for cancelRequest; most programs don’t need it.

struct Todo {
title: String
done: Bool = false
}
app Todos {
state status = ""
Button("Add") {
request(url: "https://example.com/api/todos", method: "POST",
headers: ["Content-Type": "application/json"],
body: toJson(Todo(title: "Buy milk"))) { response in
if response.ok {
status = "Added"
} else if response.status == 0 {
status = "Couldn't connect: {response.error}"
} else {
status = "The server said {response.status}"
}
}
}
Text(status)
}
  • method can be any HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE", and so on.
  • headers are sent with the request. body is sent as it is; use toJson to send a struct as JSON.
  • A response with an error status (like 404 or 500) is still a response: done gets it, with its status and body. Check ok or status.
  • file:// URLs read a local file, with status 200.

What request gives its block. It’s a built-in struct with these fields:

FieldType
statusIntThe HTTP status code, like 200 or 404. 0 if there was no response at all.
okBoolWhether the status is from 200 to 299: the request worked.
textStringThe body as text.
dataDataThe body as bytes, for images and other files that aren’t text.
headers[String: String]The response’s headers. The names are in lower case: response.headers["content-type"].
errorStringWhy there was no response (the server couldn’t be reached, the URL is invalid, …), or "".

To read a JSON answer, pass text to fromJson:

request(url: "https://example.com/api/todos") { response in
if let todos: [Todo] = fromJson(response.text) {
items = todos
}
}
download(url: String, to: String, progress: fn(Float)? = nil, done: fn(Bool)) -> Int

Downloads a URL straight into a file, which suits large files. progress, if given, is called now and then with how much has arrived, from 0.0 to 1.0. done is called at the end with whether it worked. It returns an id, for cancelRequest.

app Downloader {
state progress = 0.0
state status = ""
Button("Download") {
let file = joinPath(tempFolder(), "big.zip")
download(url: "https://example.com/big.zip", to: file, progress: { fraction in
progress = fraction
}) { ok in
status = if ok { "Done" } else { "Download failed" }
}
}
Text("{Int(progress * 100.0)}% {status}")
}
  • An error status (like 404) counts as failure.
  • While downloading, the data goes to a file with .part added to the name; it’s renamed to to only once everything has arrived. So a failed download leaves no half-written file behind. An existing file at to is replaced.
  • progress gets its fractions only if the size is known (the server says how big the file is, or it’s a local file). Either way, it’s called with 1.0 exactly once, when everything has arrived.
  • http://, https:// and file:// URLs can be downloaded; file:// copies a local file.
cancelRequest(_ id: Int)

Gives up a request or a download that’s under way. Its done block is never called (nor progress), and a cancelled download leaves no file behind, not even a partial one.

app Downloader {
state loading: Int? = nil
state status = ""
if let id = loading {
Button("Cancel") {
cancelRequest(id)
loading = nil
status = "Cancelled"
}
} else {
Button("Download") {
loading = download(url: "https://example.com/big.zip", to: "big.zip") { ok in
loading = nil
status = if ok { "Done" } else { "Download failed" }
}
}
}
Text(status)
}
  • Since done isn’t called, do your own tidying up where you cancel, as the example does.
  • Cancelling something that has finished already, or an id that was never given out, does nothing.
  • A request that’s still waiting for the server to answer is dropped by your program at once, though the connection itself is only closed when the server answers or gives up.
openWebSocket(url: String, onMessage: fn(String), onData: fn(Data)? = nil,
onClose: fn(String)) -> Int

Connects to a WebSocket server (ws:// or wss://) and returns an id for sendWebSocket and closeWebSocket. The connection stays open, and messages can go both ways:

  • onMessage is called with each text message the server sends.
  • onData, if given, is called with each binary message, as Data. Without it, binary messages go to onMessage, as text.
  • onClose is called once, when the connection ends, with the reason: "closed" if you closed it, the server’s reason (or "closed by the server"), or what went wrong, like "couldn't connect: …".
app Chat {
state socket = -1
state messages: [String] = []
state draft = ""
VStack {
for message in messages {
Text(message)
}
TextField("Message", text: draft).onSubmit {
sendWebSocket(socket, text: draft)
draft = ""
}
}
.onAppear {
socket = openWebSocket(url: "wss://example.com/chat", onMessage: { text in
messages.append(text)
}) { reason in
messages.append("Disconnected: {reason}")
socket = -1
}
}
}

A server that sends binary messages (bytes rather than text) needs onData:

socket = openWebSocket(url: "wss://example.com/feed", onMessage: { text in
log.append(text)
}, onData: { data in
received += data.count
}) { reason in
socket = -1
}
sendWebSocket(_ id: Int, text: String) -> Bool

Sends a text message. Returns false if the WebSocket isn’t open (it was closed, or id is wrong). Messages sent before the connection is ready are sent once it is.

sendWebSocketData(_ id: Int, data: Data) -> Bool

Sends a binary message: the bytes of a Data. Like sendWebSocket, it returns false if the WebSocket isn’t open.

sendWebSocketData(socket, data: toBinary(move))
sendWebSocketData(socket, data: Data(bytes: [1, 2, 255]))
closeWebSocket(_ id: Int)

Closes a WebSocket. Its onClose block still runs, with "closed". Closing one that is already closed does nothing.

after(seconds: Float, action: fn())

Runs action once, after a delay, on the UI thread.

app Reminder {
state showHint = false
Text("Welcome!").onAppear {
after(seconds: 2) { showHint = true }
}
Text("Tip: press Cmd+S to save").hidden(!showHint)
}

A delay of 0 (or less) runs action as soon as possible, after the current code finishes. An after timer can’t be cancelled; to repeat, use every.

every(seconds: Float, action: fn()) -> Int

Runs action again and again, every seconds, until it’s stopped with stopTimer. The first run is one interval from now. Returns an id for stopTimer.

app Clock {
state time = ""
Text(time).font(size: 32).onAppear {
every(seconds: 1) {
time = formatDate(now(), format: "HH:mm:ss")
}
}
}
stopTimer(_ id: Int)

Stops a timer started with every. The timer can stop itself:

fn main() {
var ticks = 0
var id = 0
id = every(seconds: 0.5) {
ticks += 1
print("tick {ticks}")
if ticks == 3 {
stopTimer(id)
}
}
}

This prints tick 1, tick 2 and tick 3, then the program ends, since nothing is left to wait for. Stopping a timer that has already stopped does nothing.

background(work: fn() -> T, done: fn(T))

Runs work on another thread, so a long computation doesn’t freeze the app, then calls done with its result on the UI thread. It’s usually written with done as a trailing block:

let n = limit
background(work: { primesBelow(limit: n) }) { primes in
status = "{primes.count} primes"
}

The two threads share nothing: work gets its own copies of the values it uses, and done gets a copy of the result. That’s what makes it safe, and it leads to a few rules, which tessel check enforces:

  • work can use local values, and call top-level functions (fns outside any app, view or struct) and methods of the values it has.
  • It can’t use state, bindings, self or its fields, or the functions of the app or view: they belong to the UI thread. Copy what it needs into a let first, as with n above.
  • The values it uses and its result must be values toBinary can save: not functions or views.
  • work must end with a value. return works inside it too.

Values are copied when background is called, so changes made afterwards don’t reach work. See Heavy work in the background for a complete app.

A runtime error in work (like an index out of range) stops the program, as it would anywhere else.

An Int is a whole number. The arithmetic operators work on it, and abs, min and max take Ints too.

MemberReturns
isEvenBoolWhether the number is even.
isOddBoolWhether the number is odd.
clamped(min: Int, max: Int)IntThe number, moved into the range from min to max.
print((4).isEven) // true
print((-3).isOdd) // true
print((15).clamped(min: 0, max: 10)) // 10
let row = 7
let shade = if row.isEven { Color.lightGray } else { Color.white }

Write a number literal in parentheses to use a member on it: (4).isEven.

A Float is a decimal number. The arithmetic operators work on it, and the math functions take and return Floats.

MemberReturns
formatted(decimals: Int)StringThe number as text, with exactly decimals digits after the point.
clamped(min: Float, max: Float)FloatThe number, moved into the range from min to max.
isNaNBoolWhether the value is NaN, “not a number” (like 0.0 / 0.0 or sqrt(-1.0)).
isFiniteBoolWhether the value is an ordinary number: not infinite and not NaN.
let volume = 1.4
print(volume.clamped(min: 0.0, max: 1.0)) // 1.0
print(sqrt(-1.0).isNaN) // true
print((1.0 / 0.0).isFinite) // false

NaN is never equal to anything, not even itself, so x == x is false for a NaN: use isNaN to check for it.

let third = 1.0 / 3.0
print(third) // 0.3333333333333333
print(third.formatted(decimals: 2)) // 0.33
print((2.0).formatted(decimals: 3)) // 2.000
print((0.666).formatted(decimals: 0)) // 1
let area = pi * 0.5 * 0.5
print("Area: {area.formatted(decimals: 4)} m²") // Area: 0.7854 m²
  • The number is rounded to decimals places. An exact half goes to the even digit, so (2.5).formatted(decimals: 0) is "2" and (3.5).formatted(decimals: 0) is "4". Because most decimal fractions can’t be stored exactly, a value like 1.005 may round down.
  • decimals can be from 0 to 15; values outside that range are treated as 0 or 15. With 0, there’s no decimal point.
  • A result that would be a negative zero, like -0.00, is shown as 0.00.
  • formatted is a Float method only. For an Int, convert first: Float(count).formatted(decimals: 1).

A String is text. Strings can’t be changed in place; methods return new strings. Join strings with + or interpolation ("{a}{b}"); += appends to a String variable. ==, !=, <, <=, > and >= compare strings; < compares character codes, so "B" < "a".

“Character” below means a Unicode character (code point): "é".count is 1.

MemberReturns
countIntNumber of characters.
isEmptyBoolWhether the text is "".
contains(_ text: String)BoolWhether text appears in it.
hasPrefix(_ text: String)BoolWhether it starts with text.
hasSuffix(_ text: String)BoolWhether it ends with text.
split(_ separator: String)[String]The parts between separators.
lines()[String]The lines, without line endings.
trim()StringWithout spaces and line breaks at both ends.
uppercase()StringIn capital letters.
lowercase()StringIn small letters.
replace(_ text: String, with: String)StringWith every text replaced.
substring(from: Int, to: Int)StringThe characters from index from up to to.
find(_ text: String)Int?The index where text first appears.
findLast(_ text: String)Int?The index where text last appears.
character(at: Int)String?The character at an index, or nil.
characters[String]Each character, as a list.
words()[String]The words: the parts between spaces, tabs and line breaks.
occurrences(of: String)IntHow many times a text appears.
trimStart()StringWithout spaces and line breaks at the start.
trimEnd()StringWithout spaces and line breaks at the end.
capitalized()StringWith the first letter of each word in capitals.
reversed()StringThe characters in reverse order.
repeated(_ times: Int)StringThe text repeated times times.
padStart(_ length: Int, with: String = " ")StringFilled at the start up to length characters.
padEnd(_ length: Int, with: String = " ")StringFilled at the end up to length characters.
urlEncodedStringMade safe to put in a URL.
urlDecodedString?A urlEncoded text turned back.
fileNameStringFor a path: the last part.
fileExtensionStringFor a path: the file’s extension, without the dot.
folderStringFor a path: the folder it’s in.

Strings can’t be indexed with […]; use character(at:), substring, split or find.

let word = "héllo"
print(word.count) // 5
print(word.isEmpty) // false
print("".isEmpty) // true
let file = "main.tsl"
print(file.contains("in")) // true
print(file.hasPrefix("main")) // true
print(file.hasSuffix(".tsl")) // true
print(file.contains("Main")) // false

All three are case-sensitive. Every string contains, starts and ends with "".

let parts = "a,,b,".split(",")
print(parts.count) // 4
print(parts.joined(separator: "|")) // a||b|
print("abc".split("").joined(separator: " ")) // a b c
  • Empty parts are kept: two separators in a row give an empty string between them, and a separator at the start or end gives an empty first or last part. Splitting "" gives [""].
  • An empty separator splits the text into single characters.
let text = "one\ntwo\r\nthree\n"
print(text.lines().count) // 3
print(text.lines()[1]) // two

Splits at \n and at \r\n. A line break at the very end doesn’t start another line, and "".lines() is empty.

print("[" + " hi \n".trim() + "]") // [hi]

Removes spaces, tabs and line breaks from both ends, not from the middle.

print("Hello".uppercase()) // HELLO
print("Hello".lowercase()) // hello
print("straße".uppercase()) // STRASSE

They follow Unicode rules, so a result can have a different length.

print("a-b-a".replace("a", with: "x")) // x-b-x

Replaces every occurrence, left to right. Replacing "" returns the text unchanged.

let word = "héllo"
print(word.substring(from: 1, to: 3)) // él
print(word.substring(from: 2, to: 100)) // llo
  • Indexes count characters from 0. from is included, to is not.
  • Out-of-range indexes are clamped to the text, so substring never stops the program. If to isn’t after from, the result is "".
let line = "error: missing value"
print(line.find(":") ?? -1) // 5
print(line.find("warning") ?? -1) // -1
if let i = line.find(": ") {
print(line.substring(from: i + 2, to: line.count)) // missing value
}

The character index (from 0) of the first place text appears, or nil if it doesn’t. The index works with substring.

let path = "docs/guide/intro.md"
print(path.findLast("/") ?? -1) // 10
print("a-b-a".findLast("a") ?? -1) // 4

Like find, but the index of the last place text appears.

let word = "héllo"
print(word.character(at: 1) ?? "?") // é
print(word.character(at: 9) ?? "none") // none
print(word.characters.count) // 5
for letter in "abc".characters {
print(letter)
}

character(at:) is nil for an index outside the text. characters is a property (no parentheses): a list with each character as a one-character string.

let words = " one two\tthree\n".words()
print(words.count) // 3
print(words.joined(separator: ",")) // one,two,three

Splits at runs of spaces, tabs and line breaks, and leaves out empty parts, unlike split.

print("a,b,,c".occurrences(of: ",")) // 3
print("banana".occurrences(of: "ana")) // 1

Counts from left to right without overlapping, so "ana" is found once in "banana". Counting "" gives 0.

print("[" + " hi ".trimStart() + "]") // [hi ]
print("[" + " hi ".trimEnd() + "]") // [ hi]

Like trim, but at one end only.

print("hello big world".capitalized()) // Hello Big World

Makes the first letter of each word a capital. The other letters are left as they are.

print("héllo".reversed()) // olléh
print("ab".repeated(3)) // ababab
print("=".repeated(10)) // ==========

repeated with 0 or less gives "".

print("7".padStart(3, with: "0")) // 007
print("[" + "ab".padEnd(5) + "]") // [ab ]
print("12345".padStart(3)) // 12345

They add with (a space unless you say otherwise) until the text is length characters long, which lines up columns of text and numbers. Text that is already long enough is left as it is.

let query = "fish & chips"
let url = "https://example.com/search?q={query.urlEncoded}"
print(url) // https://example.com/search?q=fish%20%26%20chips
print("fish%20%26%20chips".urlDecoded ?? "") // fish & chips
print("%zz".urlDecoded ?? "invalid") // invalid
  • urlEncoded keeps letters, digits and - _ . ~, and writes every other character as % and a code. Use it for a single part of a URL, like a query value or a path segment, not a whole URL.
  • urlDecoded also reads + as a space. It’s nil if the text has a % that isn’t followed by a valid code.

Three properties take a file path apart:

let path = "/Users/ada/notes/todo.txt"
print(path.fileName) // todo.txt
print(path.fileExtension) // txt
print(path.folder) // /Users/ada/notes
  • fileName is the part after the last / (or \), the whole text if there’s none.
  • fileExtension is the part of the file name after its last dot: "archive.tar.gz" gives "gz". It’s "" if there’s no dot, and for names that start with a dot, like ".profile".
  • folder is the part before the last / (or \), or "" if there’s none.

To put paths together, use joinPath.

A list [T] holds values of one type in order. Write one as [1, 2, 3]; an empty list needs its type from context: var names: [String] = [].

MemberReturns
countIntNumber of items.
isEmptyBoolWhether there are no items.
firstT?The first item, or nil if empty.
lastT?The last item, or nil if empty.
append(_ item: T)Adds item at the end. Changes the list.
insert(_ item: T, at: Int)Inserts item before index at. Changes the list.
remove(at: Int)TRemoves and returns the item at at. Changes the list.
removeAll(where: fn(T) -> Bool)Removes every item for which the block is true. Changes the list.
filter(_ keep: fn(T) -> Bool)[T]The items for which the block is true.
first(where: fn(T) -> Bool)T?The first item for which the block is true.
last(where: fn(T) -> Bool)T?The last item for which the block is true.
contains(_ item: T)BoolWhether an item equals item.
reversed()[T]The items in reverse order.
map(_ transform: fn(T) -> U)[U]Each item transformed by the block.
sorted(by: fn(T, T) -> Bool = …)[T]The items in order.
joined(separator: String = "")StringOnly for [String]: the items joined into one string.
indicesRangeThe valid indexes, 0..count.
index(of: T)Int?The index of the first item equal to the value.
lastIndex(of: T)Int?The index of the last item equal to the value.
firstIndex(where: fn(T) -> Bool)Int?The index of the first item for which the block is true.
count(where: fn(T) -> Bool)IntHow many items the block is true for.
any(where: fn(T) -> Bool)BoolWhether the block is true for at least one item.
all(where: fn(T) -> Bool)BoolWhether the block is true for every item.
sum()TOnly for [Int] and [Float]: the items added up.
min()T?Only for [Int], [Float] and [String]: the smallest item.
max()T?Only for [Int], [Float] and [String]: the largest item.
prefix(_ count: Int)[T]The first count items.
suffix(_ count: Int)[T]The last count items.
dropFirst(_ count: Int = 1)[T]Without the first count items.
dropLast(_ count: Int = 1)[T]Without the last count items.
slice(from: Int, to: Int)[T]The items from index from up to to.
unique()[T]Without repeated items.
shuffled()[T]The items in random order.
randomElement()T?A random item, or nil if empty.
enumerated()[(Int, T)]Each item with its index: for (i, x) in list.enumerated().
appendAll(_ items: [T])Adds all of items at the end. Changes the list.
removeFirst()TRemoves and returns the first item. Changes the list.
removeLast()TRemoves and returns the last item. Changes the list.

Other list operations:

  • list[i] reads the item at index i (from 0), and list[i] = value replaces it. An index outside the list stops the program with an error.
  • a + b is a new list with the items of a then b; list += [x] appends.
  • == and != compare two lists item by item.
  • for item in list { … } loops over the items. See Collections.

Methods that change the list (append, appendAll, insert, remove, removeFirst, removeLast, removeAll) need a list you can change: a var, a state, or a bind parameter, or a field of one. The others return a new list and leave the original alone.

let scores = [40, 75, 90]
print(scores.first ?? 0) // 40
print(scores.last ?? 0) // 90
print(scores.first(where: { s in s > 50 }) ?? 0) // 75
print(scores.last(where: { s in s < 50 }) ?? 0) // 40

Without parentheses, first and last are properties; with a where: block, they search.

var items = ["b", "c"]
items.append("d")
items.insert("a", at: 0)
print(items.joined(separator: ",")) // a,b,c,d
let removed = items.remove(at: 1)
print(removed) // b
print(items.joined(separator: ",")) // a,c,d

insert(at:) accepts indexes from 0 to count (the end). remove(at:) needs an index of an existing item. Other indexes stop the program with an error.

var numbers = [1, 5, 2, 8, 3]
numbers.removeAll(where: { n in n > 2 })
print(numbers.count) // 2
let numbers = [1, 5, 2, 8, 3]
let big = numbers.filter { n in n > 2 }
print(big.count) // 3
let tags = ["red", "green"]
print(tags.contains("green")) // true
print(tags.contains("blue")) // false
let letters = ["a", "b", "c"]
print(letters.reversed().joined()) // cba
let numbers = [1, 2, 3]
let labels = numbers.map { n in "#{n}" }
print(labels.joined(separator: " ")) // #1 #2 #3

The block’s result type decides the new list’s type; the block must return a value.

let names = ["Linus", "ada", "Grace"]
print(names.sorted().joined(separator: ",")) // Grace,Linus,ada
let byLength = names.sorted(by: { a, b in a.count < b.count })
print(byLength.joined(separator: ",")) // ada,Linus,Grace
  • Without by:, lists of Int, Float and String are sorted smallest first. Strings are compared by character code, so capital letters come before small ones.
  • Other lists (like a list of structs) need by:: a block that returns true when its first value should come before its second.
  • The sort is stable: items that are equal keep their order.
let words = ["one", "two", "three"]
print(words.joined(separator: ", ")) // one, two, three
print(words.joined()) // onetwothree

Only lists of String have joined. To join other values, map them to strings first.

let names = ["Ada", "Grace", "Linus"]
print(names.indices) // 0..3
for i in names.indices {
print("{i + 1}. {names[i]}")
}

A range of every valid index, for when you need the index as well as the item.

let nums = [4, 8, 15, 8]
print(nums.index(of: 8) ?? -1) // 1
print(nums.lastIndex(of: 8) ?? -1) // 3
print(nums.index(of: 99) ?? -1) // -1

nil if no item is equal to the value. They work for any list whose items can be compared with ==, structs included.

let nums = [4, 8, 15, 16]
print(nums.firstIndex(where: { n in n > 10 }) ?? -1) // 2

count(where:), any(where:) and all(where:)

Section titled “count(where:), any(where:) and all(where:)”
let nums = [4, 8, 15, 16, 23]
print(nums.count(where: { n in n.isEven })) // 3
print(nums.any(where: { n in n > 20 })) // true
print(nums.all(where: { n in n > 5 })) // false

For an empty list, any is false and all is true.

let nums = [4, 8, 15]
print(nums.sum()) // 27
print(nums.min() ?? 0) // 4
print(nums.max() ?? 0) // 15
print(["pear", "apple"].min() ?? "") // apple
  • sum is for lists of Int or Float; it’s 0 for an empty list.
  • min and max are for lists of Int, Float or String, and are nil for an empty list. Strings are compared as with <.
  • For other lists, map to numbers first, or use sorted(by:).
let nums = [1, 2, 3, 4, 5]
print(nums.prefix(2).sum()) // 3 (1 + 2)
print(nums.suffix(2).sum()) // 9 (4 + 5)
print(nums.dropFirst().count) // 4
print(nums.dropLast(2).sum()) // 6 (1 + 2 + 3)
print(nums.prefix(100).count) // 5

A count bigger than the list is fine: you get the whole list (or an empty one). None of them stop the program.

let letters = ["a", "b", "c", "d", "e"]
print(letters.slice(from: 1, to: 3).joined()) // bc
print(letters.slice(from: 3, to: 99).joined()) // de

The items from index from (included) up to to (not included), like substring for strings. Indexes outside the list are clamped to it, and if to isn’t after from, the result is empty.

let tags = ["red", "blue", "red", "green", "blue"]
print(tags.unique().joined(separator: ",")) // red,blue,green

Keeps the first of each group of equal items, in their order.

let names = ["Ada", "Grace", "Linus"]
let winner = names.randomElement() ?? "nobody"
let order = names.shuffled()

randomElement is nil for an empty list. See also Random numbers.

var queue = ["a", "b"]
queue.appendAll(["c", "d"])
print(queue.removeFirst()) // a
print(queue.removeLast()) // d
print(queue.joined(separator: ",")) // b,c

queue.appendAll(other) does the same as queue += other. removeFirst and removeLast need a list with at least one item; on an empty list they stop the program with an error, like remove(at:).

A dictionary [K: V] maps keys to values. Write one as ["a": 1, "b": 2]; an empty one needs its type from context: var ages: [String: Int] = [:]. Keys can be Int, String, Bool, an enum whose cases have no values, or a tuple or struct made of those.

MemberReturns
countIntNumber of entries.
isEmptyBoolWhether there are no entries.
keys[K]The keys.
values[V]The values.
contains(key: K)BoolWhether there is an entry for the key.
removeValue(forKey: K)V?Removes the entry and returns its value, or nil if there was none. Changes the dictionary.

Reading and writing entries:

  • dict[key] is the value for key, as an optional (V?): nil if there’s no entry.
  • dict[key] = value adds or replaces an entry; dict[key] = nil removes it.
  • == and != compare two dictionaries’ entries, in any order.
var ages = ["Ada": 36, "Linus": 21]
ages["Grace"] = 85
ages["Linus"] = 22
print(ages["Linus"] ?? 0) // 22
print(ages["Bob"] ?? 0) // 0
print(ages.contains(key: "Ada")) // true
print(ages.removeValue(forKey: "Ada") ?? 0) // 36
ages["Grace"] = nil
print(ages.keys.joined(separator: ",")) // Linus

keys and values are in the order the entries were first added. Replacing a value keeps its place; removing an entry and adding it again moves it to the end.

A for loop over a dictionary gives each entry as a (key, value) tuple, in order:

let stock = ["apples": 3, "pears": 0]
for (fruit, count) in stock {
print("{fruit}: {count}")
}
zip(_ a: [A], _ b: [B]) -> [(A, B)]

Pairs up the items at the same positions of two lists, as long as the shorter list:

let names = ["Ada", "Alan"]
let scores = [90, 85, 70]
for (name, score) in zip(names, scores) {
print("{name}: {score}") // Ada: 90, then Alan: 85
}

A set Set<T> holds values without order and without repeats. Write one as a list where a set is expected (let seen: Set<Int> = [1, 2]), make one from a list with Set(list), or an empty one with Set<Int>(). Items can be the same types as dictionary keys. for x in set goes through the items in the order they were first added. Two sets are == when they have the same items. JSON writes a set as an array. See Collections.

MemberReturns
countIntNumber of items.
isEmptyBoolWhether there are no items.
values[T]The items, in the order they were first added.
contains(_ item: T)BoolWhether the item is in the set.
insert(_ item: T)Adds the item (nothing happens if it’s there). Changes the set.
remove(_ item: T)Removes the item (nothing happens if it isn’t there). Changes the set.
union(_ other: Set<T>)Set<T>The items in either set.
intersection(_ other: Set<T>)Set<T>The items in both sets.
subtracting(_ other: Set<T>)Set<T>The items that aren’t in other.
isSubset(of: Set<T>)BoolWhether every item is also in the other set.
isSuperset(of: Set<T>)BoolWhether every item of the other set is in this one.
isDisjoint(with: Set<T>)BoolWhether no item is in both.
filter(_ keep: fn(T) -> Bool)Set<T>The items for which keep is true.
sorted(by: fn(T, T) -> Bool = …)[T]The items as a sorted list; without by:, only for Int, Float and String items.

A tuple groups 2 to 8 values: its type is written (Int, String), a value (1, "one"), and its items are .0, .1 and so on. let (a, b) = pair takes one apart. Tuples compare with ==, can be dictionary keys and set items when their items can, and JSON writes them as arrays. See Collections.

The functions in this section return a Result or an Error? instead of an optional, so a failure comes with a message that says why. They’re built in; you don’t import anything.

struct Error { message: String }
enum Result<T> {
ok(value: T)
failure(error: Error)
}

Error("reason") makes an error; the label is optional. Result has these helpers:

MethodReturns
value()T?: the value, or nil on failure
error()Error?: the error, or nil on success
isOk()Bool
valueOr(_ fallback: T)the value, or fallback on failure

try unwraps a Result (or checks an Error?) and passes a failure on to the caller. See Errors.

readText(_ path: String) -> Result<String>

Like readFile, but a failure says why, for example can't read notes.txt: there's no such file or folder.

writeText(path: String, text: String) -> Error?

Like writeFile; nil means it worked.

appendText(path: String, text: String) -> Error?

Like appendFile; nil means it worked.

readBytes(_ path: String) -> Result<Data>

Like readData, with a reason on failure.

writeBytes(path: String, data: Data) -> Error?

Like writeData; nil means it worked.

parseInt(_ text: String) -> Result<Int>

Parses a whole number, ignoring spaces around it. On failure the message is "abc" isn't a whole number.

parseFloat(_ text: String) -> Result<Float>

Parses a number. On failure the message is "abc" isn't a number.

decodeJson<T>(_ text: String) -> Result<T>

Like fromJson, with the type given by the result you assign to. The reason is either this isn't valid JSON: … or the JSON doesn't have the shape of this type.

struct Point { x: Int, y: Int }
let r: Result<Point> = decodeJson("\{\"x\": 1}")
if let e = r.error() {
print(e.message) // the JSON doesn't have the shape of this type
}
decodeBinary<T>(_ data: Data) -> Result<T>

Like fromBinary, with a reason on failure.

A few mistakes can only be found while the program runs. They stop the program with a message and the source location:

error: index 5 is out of range for a list of 3 items
--> main.tsl:4:11
ErrorCaused by
index … is out of rangelist[i], remove(at:) or insert(at:) with an index outside the list, removeFirst() or removeLast() on an empty list, or data[i] outside the data.
integer overflowInt arithmetic whose result is too big for an Int.
division by zeroInt division or remainder (/, %) by 0.

Float arithmetic never stops the program: dividing by 0.0 gives an infinite value (printed as inf).