Examples
These 22 programs are both Singular's tutorial and its test suite. Each one must compile and print exactly the output shown under it. Read them in order: each builds on the ones before.
- 01 HelloThe smallest Singular program.
- 02 ValuesValues, types and conversions. Every name is written with its type.
- 03 FunctionsFunctions, single-expression bodies, and returning more than one value.
- 04 Control flowBranches and loops. Braces are always required.
- 05 StructsStructs hold data. Methods live in
implblocks. - 06 Enums and matchEnums list every form a value can take.
matchmust handle all of them. - 07 OptionThere is no null. A value that might be missing is an
Option. - 08 ListsLists grow and shrink. A list declared with
constcan't. - 09 MapsMaps store values by key and keep keys in the order they were added.
- 10 StringsStrings are UTF-8 text. Building one from pieces uses interpolation.
- 11 Access modesThe three ways a function can use what you pass it: view, edit and move.
- 12 Moves and copiesPlain values copy by themselves. Everything else is moved or copied on purpose.
- 13 SlicesOne function can read a whole list, a whole array, or just part of either.
- 14 ErrorsA function that can fail says so in its signature, and every call that can fail is marked.
- 15 ClosuresClosures use the same
fnsyntax as functions. Errors pass straight through them. - 16 Traits and genericsTraits describe what a type can do. Generics let one piece of code work for many types.
- 17 Any
any Traitholds values of different types in one collection. - 18 DropValues clean up after themselves when their owner goes away.
- 19 ArenaLinked data lives in an arena. Handles point into it safely.
- 20 Compile timeWork done while compiling: constants, sized arrays and compile-time switches.
- 21 Word countCounts the words in a piece of text and prints the most common ones.
- 22 ModulesA program split across two files.
01 Hello
//! The smallest Singular program.
fn main() {
print("Hello, Singular!")
}Hello, Singular!02 Values
//! Values, types and conversions. Every name is written with its type.
fn main() {
const String name = "Singular"
const Int year = 2026
const Float pi = 3.14159
const Bool ready = true
const Char letter = 'S'
print("{name} {year} {pi:.2} {ready} {letter}")
// Without `const`, a name can change. With `const`, it never does.
Int lives = 3
lives -= 1
print("lives: {lives}")
// There are no silent conversions: you call the type you want.
const Int8 small = 100
const Int bigger = Int(small) * 10
print(bigger)
const Float ratio = Float(7) / 2.0
print("{ratio:.1}")
print(Int(ratio))
const Int big_number = 1_000_000
const Int mask = 0xFF
print(big_number + mask)
}Singular 2026 3.14 true S
lives: 2
1000
3.5
3
100025503 Functions
//! Functions, single-expression bodies, and returning more than one value.
/// A body that is one expression can be written after `=>`.
fn square(Int x) -> Int => x * x
/// A block body always says `return`.
fn average(Float a, Float b) -> Float {
const Float total = a + b
return total / 2.0
}
/// Returns two values at once as a tuple.
fn min_max(List[Int] values) -> (Int, Int) {
Int low = values[0]
Int high = values[0]
for Int v in values {
if v < low {
low = v
}
if v > high {
high = v
}
}
return (low, high)
}
fn greet(String name) {
print("hi, {name}")
}
fn main() {
print(square(7))
print("{average(3.0, 4.0):.1}")
const (Int low, Int high) = min_max([5, 2, 9, 1])
print("low {low}, high {high}")
const (Int, Int) pair = min_max([10, 20])
print(pair)
print(pair.0)
greet("Ada")
}49
3.5
low 1, high 9
(10, 20)
10
hi, Ada04 Control flow
//! Branches and loops. Braces are always required.
fn classify(Int n) -> String {
if n < 0 {
return "negative"
} else if n == 0 {
return "zero"
}
return "positive"
}
fn main() {
print(classify(-5))
print(classify(0))
print(classify(7))
// `if` can produce a value. Each branch's last line is its value.
const Int temperature = 25
const String feeling = if temperature > 20 { "warm" } else { "cold" }
print(feeling)
Int total = 0
for Int i in 1..=10 {
if i % 2 == 0 {
continue
}
total += i
}
print("sum of odd numbers up to 10: {total}")
Int n = 1
while n < 100 {
n *= 3
}
print("first power of 3 over 100: {n}")
for Int i in 0..10 {
if i == 3 {
break
}
print(i)
}
// Mixing `and` with `or` needs parentheses, so the grouping is never a guess.
const Int x = 5
if (x > 0 and x < 10) or x == 100 {
print("in range")
}
}negative
zero
positive
warm
sum of odd numbers up to 10: 25
first power of 3 over 100: 243
0
1
2
in range05 Structs
//! Structs hold data. Methods live in `impl` blocks.
struct Counter {
String label
Int value
}
impl Counter {
/// A static function: there is no `self`. Called as `Counter.new(...)`.
fn new(move String label) -> Counter {
return Counter { label: move label, value: 0 }
}
/// `edit self` means this method changes the counter.
fn increment(edit self) {
self.value += 1
}
fn add(edit self, Int amount) {
self.value += amount
}
/// Plain `self` means this method only reads it.
fn report(self) -> String => "{self.label}: {self.value}"
}
fn main() {
Counter clicks = Counter.new("clicks")
clicks.increment()
clicks.increment()
clicks.add(5)
print(clicks.report())
print(clicks)
}clicks: 7
Counter { label: "clicks", value: 7 }06 Enums and match
//! Enums list every form a value can take. `match` must handle all of them.
enum Shape {
Circle(Float radius)
Rect(Float width, Float height)
Dot
}
fn describe(Shape shape) -> String {
return match shape {
Circle(Float r) if r > 10.0 => "a big circle"
Circle(_) => "a circle"
Rect(Float w, Float h) if w == h => "a square"
Rect(_, _) => "a rectangle"
Dot => "a dot"
}
}
enum Direction {
North
East
South
West
}
fn is_vertical(Direction direction) -> Bool {
return match direction {
North | South => true
East | West => false
}
}
fn main() {
const List[Shape] shapes = [
Shape.Circle(1.0),
Shape.Circle(12.0),
Shape.Rect(2.0, 2.0),
Shape.Rect(1.0, 3.0),
Shape.Dot,
]
for Shape shape in shapes {
print(describe(shape))
}
print(is_vertical(Direction.North))
print(is_vertical(Direction.East))
// `is` checks one pattern and can name its parts.
if shapes[2] is Rect(Float w, _) {
print("the third shape is {w:.1} wide")
}
// `match edit` lets an arm change the value in place.
Shape ball = Shape.Circle(1.0)
match edit ball {
Circle(Float r) => {
r *= 3.0
}
_ => {}
}
if ball is Circle(Float r) {
print("the ball's radius is now {r:.1}")
}
}a circle
a big circle
a square
a rectangle
a dot
true
false
the third shape is 2.0 wide
the ball's radius is now 3.007 Option
//! There is no null. A value that might be missing is an `Option`.
fn find_index(List[String] names, String target) -> Option[Int] {
for Int i in 0..names.len() {
if names[i] == target {
return Some(i)
}
}
return None
}
fn main() {
const List[String] names = ["ada", "grace", "linus"]
const Option[Int] found = find_index(names, "grace")
print(found)
print(find_index(names, "bob"))
if found is Some(Int i) {
print("grace is at position {i}")
}
// Option[Int] is plain, so calling a `move self` method just copies it.
const Option[Int] missing = find_index(names, "bob")
print(missing.unwrap_or(-1))
print("found something: {found.is_some()}")
// `pop` returns a new value each time, so `job` owns what it gets.
List[String] queue = ["first", "second", "third"]
while queue.pop() is Some(String job) {
print("doing {job}")
}
}Some(1)
None
grace is at position 1
-1
found something: true
doing third
doing second
doing first08 Lists
//! Lists grow and shrink. A list declared with `const` can't.
fn main() {
List[String] names = ["ada", "grace"]
names.push("linus")
print(names)
print("count: {names.len()}")
print("second: {names[1]}")
for String name in names {
print("hello, {name}")
}
List[Int] numbers = [3, 1, 2]
numbers.sort()
print(numbers)
// `edit` on the loop source lets each element be changed in place.
for Int n in edit numbers {
n *= 10
}
print(numbers)
const Option[Int] last = numbers.pop()
print("popped: {last}")
print(numbers)
print("has 10: {numbers.contains(10)}")
}["ada", "grace", "linus"]
count: 3
second: grace
hello, ada
hello, grace
hello, linus
[1, 2, 3]
[10, 20, 30]
popped: Some(30)
[10, 20]
has 10: true09 Maps
//! Maps store values by key and keep keys in the order they were added.
fn main() {
Map[String, Int] stock = ["apples": 3, "pears": 0]
stock.insert("plums", 7)
print(stock)
// Indexing works for keys that exist. New keys are added with `insert`.
stock["apples"] += 2
const Int apples = stock["apples"]
print("apples: {apples}")
const String key = "pears"
const Option[Int] removed = stock.remove(key)
print("removed {key}: {removed}")
print("still has {key}: {stock.contains(key)}")
for (String fruit, Int count) in stock {
print("{fruit}: {count}")
}
// With `edit`, values can change. Keys never can.
for (_, Int count) in edit stock {
count *= 2
}
print(stock)
print(stock.get("kiwis"))
}["apples": 3, "pears": 0, "plums": 7]
apples: 5
removed pears: Some(0)
still has pears: false
apples: 5
plums: 7
["apples": 10, "plums": 14]
None10 Strings
//! Strings are UTF-8 text. Building one from pieces uses interpolation.
fn main() {
const String greeting = "Hello, Singular"
print(greeting.len())
print(greeting.to_upper())
print(greeting.slice(7, 15))
print(greeting.contains("Sing"))
print(greeting.starts_with("Hell"))
Int vowels = 0
for Char c in greeting {
if c == 'a' or c == 'e' or c == 'i' or c == 'o' or c == 'u' {
vowels += 1
}
}
print("vowels: {vowels}")
const List[String] colors = "red,green,blue".split(",")
print(colors)
print(colors.join(" + "))
String shout = "hey"
shout.append("!!")
shout.push('?')
print(shout)
// Raw strings have no escapes and no interpolation.
const String path = r"C:\games\{save}.dat"
print(path)
print("braces: {{like this}}")
}15
HELLO, SINGULAR
Singular
true
true
vowels: 5
["red", "green", "blue"]
red + green + blue
hey!!?
C:\games\{save}.dat
braces: {like this}11 Access modes
//! The three ways a function can use what you pass it: view, edit and move.
struct Item {
String name
Float price
}
/// Only reads the items. The caller's list is untouched.
fn total(List[Item] items) -> Float {
Float sum = 0.0
for Item item in items {
sum += item.price
}
return sum
}
/// Changes the items in place. Callers must write `edit`.
fn discount(edit List[Item] items, Float percent) {
for Item item in edit items {
item.price *= 1.0 - percent / 100.0
}
}
/// Takes the list for good. Callers must write `move`.
fn archive(move List[Item] items) -> Int {
return items.len()
}
fn main() {
List[Item] cart = [
Item { name: "book", price: 20.0 },
Item { name: "pen", price: 5.0 },
]
print("total: {total(cart):.2}")
discount(edit cart, 10.0)
print("after discount: {total(cart):.2}")
// Names for parts of a value, with no copying.
// A `view` is read-only, so it's declared `const`.
const Item first = view cart[0]
print("first item: {first.name}")
Item second = edit cart[1]
second.price = 1.0
print("pen now costs {cart[1].price:.2}")
// After this line, `cart` is gone.
const Int count = archive(move cart)
print("archived {count} items")
}total: 25.00
after discount: 22.50
first item: book
pen now costs 1.00
archived 2 items12 Moves and copies
//! Plain values copy by themselves. Everything else is moved or copied on purpose.
struct Point {
Int x
Int y
}
struct Player {
String name
Point position
}
fn make_player(String name) -> Player {
const Player player = Player { name: name.copy(), position: Point { x: 0, y: 0 } }
// `return` hands the value to the caller. No `move` is needed.
return player
}
fn main() {
// Point holds only numbers, so it's plain: `Point b = a` copies it.
const Point a = Point { x: 1, y: 2 }
Point b = a
b.x = 10
print("a: {a}, b: {b}")
// Player holds a String, so it isn't plain. You say what you want.
const String name = "Ada"
const Player hero = Player { name: move name, position: a }
// Using `name` from here on would be error E0401: it was moved into `hero`.
Player twin = hero.copy()
twin.name = "Grace"
print("{hero.name} and {twin.name}")
// A `const` value can still be moved. Being moved isn't being changed.
List[Player] team = []
team.push(move hero)
team.push(move twin)
print("team size: {team.len()}")
const Player guest = make_player("Linus")
print("guest: {guest.name} at {guest.position}")
}a: Point { x: 1, y: 2 }, b: Point { x: 10, y: 2 }
Ada and Grace
team size: 2
guest: Linus at Point { x: 0, y: 0 }13 Slices
//! One function can read a whole list, a whole array, or just part of either.
fn sum(Slice[Int] values) -> Int {
Int total = 0
for Int v in values {
total += v
}
return total
}
/// An `edit` slice can change elements, but can't add or remove them.
fn zero_out(edit Slice[Int] values) {
for Int v in edit values {
v = 0
}
}
fn main() {
List[Int] scores = [4, 8, 15, 16, 23, 42]
print("all: {sum(scores)}")
print("first three: {sum(scores[0..3])}")
print("last two: {sum(scores[4..6])}")
const Array[Int, 3] fixed = [1, 1, 1]
print("array: {sum(fixed)}")
zero_out(edit scores[0..2])
print(scores)
}all: 108
first three: 27
last two: 65
array: 3
[0, 0, 15, 16, 23, 42]14 Errors
//! A function that can fail says so in its signature, and every call that can fail is marked.
enum ConfigError {
Missing(String)
BadNumber(ParseIntError)
}
/// Reads the port number from the settings.
///
/// # Errors
/// - `Missing` if there is no "port" entry.
/// - `BadNumber` if the entry isn't a whole number.
fn read_port(Map[String, String] settings) -> Int raises ConfigError {
const String key = "port"
if not settings.contains(key) {
raise ConfigError.Missing("port")
}
// `catch` handles the error here, converting it to this function's error type.
const Int port = Int.parse(settings[key]) catch ParseIntError e {
raise ConfigError.BadNumber(e)
}
return port
}
/// `try` passes any error straight up to whoever called this function.
fn describe(Map[String, String] settings) -> String raises ConfigError {
const Int port = try read_port(settings)
return "listening on port {port}"
}
fn main() {
const Map[String, String] good = ["host": "localhost", "port": "8080"]
const Map[String, String] typo = ["host": "localhost", "port": "80x0"]
const Map[String, String] empty = [:]
for Map[String, String] settings in [move good, move typo, move empty] {
const String message = describe(settings) catch ConfigError e {
"config problem: {e}"
}
print(message)
}
}listening on port 8080
config problem: BadNumber(InvalidDigit('x'))
config problem: Missing("port")15 Closures
//! Closures use the same `fn` syntax as functions. Errors pass straight through them.
fn apply_twice(fn(Int) -> Int f, Int x) -> Int => f(f(x))
fn repeat(Int times, fn() action) {
for _ in 0..times {
action()
}
}
/// `map` has no "fallible" twin. If the function you pass can fail, so does the call.
fn parse_all(List[String] texts) -> List[Int] raises ParseIntError {
return try texts.map(Int.parse)
}
fn main() {
const List[Int] numbers = [1, 2, 3, 4]
const List[Int] doubled = numbers.map(fn(Int n) => n * 2)
print(doubled)
// A closure can read variables around it.
const Int limit = 2
const List[Int] big = numbers.filter(fn(Int n) => n > limit)
print(big)
print(apply_twice(fn(Int n) => n + 10, 1))
// It can also change them while it runs. Here `count` is edited.
Int count = 0
repeat(3, fn() { count += 1 })
print("count: {count}")
const List[Int] good = parse_all(["4", "5", "6"]) catch _ { [] }
print(good)
const List[Int] bad = parse_all(["7", "x"]) catch ParseIntError e {
print("could not parse: {e}")
[]
}
print(bad)
}[2, 4, 6, 8]
[3, 4]
21
count: 3
[4, 5, 6]
could not parse: InvalidDigit('x')
[]16 Traits and generics
//! Traits describe what a type can do. Generics let one piece of code work for many types.
trait Describe {
fn label(self) -> String
/// A default body. Implementations get it for free and can replace it.
fn describe(self) -> String => "this is {self.label()}"
}
struct Dog {
String name
}
impl Describe for Dog {
fn label(self) -> String => self.name.copy()
}
/// Works for any type that can be ordered and copied.
fn largest[T: Ord + Copy](Slice[T] values) -> T {
T best = values[0].copy()
for T v in values {
if v > best {
best = v.copy()
}
}
return best
}
struct Stack[T] {
List[T] items
}
impl[T] Stack[T] {
fn new() -> Stack[T] => Stack { items: [] }
fn push(edit self, move T item) {
self.items.push(move item)
}
fn pop(edit self) -> Option[T] => self.items.pop()
fn size(self) -> Int => self.items.len()
}
fn main() {
const Dog rex = Dog { name: "Rex" }
print(rex.describe())
print(largest([3, 9, 4]))
const List[String] fruits = ["pear", "apple", "fig"]
print(largest(fruits))
Stack[String] stack = Stack.new()
stack.push("first")
stack.push("second")
print("size: {stack.size()}")
while stack.pop() is Some(String item) {
print("popped {item}")
}
}this is Rex
9
pear
size: 2
popped second
popped first17 Any
//! `any Trait` holds values of different types in one collection.
use std.math
trait Shape {
fn area(self) -> Float
fn label(self) -> String
}
struct Circle {
Float radius
}
impl Shape for Circle {
fn area(self) -> Float => math.pi * self.radius * self.radius
fn label(self) -> String => "circle"
}
struct Square {
Float side
}
impl Shape for Square {
fn area(self) -> Float => self.side * self.side
fn label(self) -> String => "square"
}
fn total_area(Slice[any Shape] shapes) -> Float {
Float total = 0.0
for any Shape s in shapes {
total += s.area()
}
return total
}
fn main() {
const List[any Shape] shapes = [
Circle { radius: 1.0 },
Square { side: 2.0 },
Circle { radius: 0.5 },
]
for any Shape s in shapes {
print("{s.label()}: {s.area():.2}")
}
print("total: {total_area(shapes):.2}")
}circle: 3.14
square: 4.00
circle: 0.79
total: 7.9318 Drop
//! Values clean up after themselves when their owner goes away.
struct Logger {
String name
}
impl Drop for Logger {
fn drop(edit self) {
print("closing {self.name}")
}
}
fn main() {
const Logger a = Logger { name: "a" }
const Logger b = Logger { name: "b" }
const Logger c = Logger { name: "c" }
print("working")
drop(move a)
print("a was closed early")
// At the end of the block, the rest drop in reverse order: c, then b.
}working
closing a
a was closed early
closing c
closing b19 Arena
//! Linked data lives in an arena. Handles point into it safely.
struct Monster {
String name
Int hp
}
fn main() {
Arena[Monster] monsters = Arena.new()
const Handle[Monster] slime = monsters.add(Monster { name: "slime", hp: 10 })
const Handle[Monster] bat = monsters.add(Monster { name: "bat", hp: 4 })
// A handle is plain, so it can be copied and stored anywhere.
const Handle[Monster] target = bat
monsters[target].hp -= 4
if monsters[target].hp <= 0 {
const Monster defeated = monsters.remove(target)
print("{defeated.name} was defeated")
}
// The bat's handle is now stale. Checking it is safe; using it would panic.
print("bat still here: {monsters.contains(bat)}")
monsters[slime].hp += 5
for (_, Monster monster) in monsters {
print("{monster.name} has {monster.hp} hp")
}
}bat was defeated
bat still here: false
slime has 15 hp20 Compile time
//! Work done while compiling: constants, sized arrays and compile-time switches.
/// Turn this on to print extra detail. Code behind a false switch isn't compiled.
comptime Bool verbose = false
/// The squares of 0 through 7, computed by the compiler. The program only reads them.
comptime Array[Int, 8] squares = make_squares()
fn make_squares() -> Array[Int, 8] {
Array[Int, 8] out = Array[Int, 8].filled(0)
for Int i in 0..8 {
out[i] = i * i
}
return out
}
/// Works for an array of any length. `N` is known at compile time.
fn sum[comptime Int N](Array[Int, N] values) -> Int {
Int total = 0
for Int v in values {
total += v
}
return total
}
/// A value parameter can be used like any other value.
fn length[comptime Int N](Array[Int, N] values) -> Int => N
fn main() {
print(squares)
print("squares holds {length(squares)} numbers")
print("sum of squares: {sum(squares)}")
const Array[Int, 3] small = [1, 2, 3]
print("sum of small: {sum(small)}")
comptime if verbose {
print("verbose mode is on")
}
}[0, 1, 4, 9, 16, 25, 36, 49]
squares holds 8 numbers
sum of squares: 140
sum of small: 621 Word count
//! Counts the words in a piece of text and prints the most common ones.
comptime String sample = "the cat and the hat and the bat"
/// One word and how many times it appeared.
pub struct WordCount {
pub String word
pub Int count
}
/// Splits `text` on spaces and counts how often each word appears.
///
/// Words are compared in lowercase, so "The" and "the" count together.
///
/// ```sing
/// const Map[String, Int] counts = count_words("a b A")
/// assert_eq(counts["a"], 2)
/// ```
pub fn count_words(String text) -> Map[String, Int] {
Map[String, Int] counts = [:]
for String word in text.split(" ") {
if word.is_empty() {
continue
}
const String key = word.to_lower()
if counts.contains(key) {
counts[key] += 1
} else {
counts.insert(move key, 1)
}
}
return counts
}
/// Returns the `limit` most common words, most frequent first.
/// Words with the same count keep the order they first appeared in.
pub fn top_words(Map[String, Int] counts, Int limit) -> List[WordCount] {
List[WordCount] ranked = []
for (String word, Int count) in counts {
ranked.push(WordCount { word: word.copy(), count: count })
}
ranked.sort_by(fn(WordCount a, WordCount b) => b.count.compare(a.count))
ranked.truncate(limit)
return ranked
}
fn main() {
const Map[String, Int] counts = count_words(sample)
print("{counts.len()} different words")
for WordCount entry in top_words(counts, 3) {
print("{entry.word}: {entry.count}")
}
}
test "empty text has no words" {
assert_eq(count_words("").len(), 0)
}
test "counting ignores case" {
const Map[String, Int] counts = count_words("Go go GO")
assert_eq(counts["go"], 3)
}5 different words
the: 3
and: 2
cat: 122 Modules
//! Shapes and measurements. Only items marked `pub` can be used from other files.
pub struct Rect {
pub Int width
pub Int height
}
pub fn area(Rect r) -> Int => multiply(r.width, r.height)
/// Private: only this file can call it.
fn multiply(Int a, Int b) -> Int => a * b//! A program split across two files.
// `use geometry` makes the module name available: geometry.area(...)
use geometry
// `use geometry.{Rect}` brings in one item by its bare name.
use geometry.{Rect}
fn main() {
const Rect r = Rect { width: 3, height: 4 }
print(geometry.area(r))
}12