Zirric
No magic.
Zirric is built on composability, simplicity and transparency. Just data types, unions and attributes. No classes, inheritance, generics, interfaces, optional conformances or ambiguity. No exceptions.
Get StartedRead the Specification
import math
attr Area {
of(shape: @Area) -> Float
}
union Shape {
@Area(fn(c) { math.pi * c.radius * c.radius })
data Circle { radius: Float }
@Area(fn(r) { r.width * r.height })
data Rect { width: Float, height: Float }
}
const shapes = [Circle(1.0), Rect(4.0, 5.0), Circle(3.0)]
// `for` is the map and the filter.
const bigAreas = for shape <- shapes {
const area = Area(shape).of(shape)
if area > 10.0 {
area
} else {
continue
}
}
Five things to learn
Zirric lacks many features and has only a few powerful ones, and those that exist are curated to work well together. data types hold value, unions give structure, attributes provide context, functions do stuff, and modules prevent you from losing your mind.
data holds valueRecords with named fields, compared field by field, constructed by calling the type. Data types →
union gives structureA closed set of member types. A value belongs by being one of them — no wrapping step. Union types →
attr provides contextCapabilities and metadata, declared on the type and read back by anything that asks. Attributes →
fn does stuffFirst-class functions and closures, with one syntax for both. Closures →
mod keeps you saneEvery file names its module in full, so a name always says where it came from. Modules →
Existing features, put to work
Rather than adding a feature, Zirric tends to reach for one that is already there. Take for loops: used as an expression, a loop collects what its body produces, and continue drops a value. That is map and filter — literally the same loop you would write for its side effects.
const evenSquares = for n <- Range(0, 10) {
if n % 2 == 0 {
n * n
} else {
continue
}
}
// [0, 4, 16, 36, 64]
The same goes for the rest. for element <- value is a call into the Iterable attribute, so anything carrying it can be looped over. len is a call into Countable. The Cavefile is Zirric source, not a config format — a data declaration with attributes on it, like any other.
A failure returns a Result, and !., !! and T! work through one without an unwrapping step. panic is for bugs, and nothing catches it. Guarded access →
Write a type hint and analysis holds you to it. Leave it off and the value flows as it always did. Type hints →
The filesystem, the clock and randomness are values you pass, not globals you reach for. A test hands over a fake; nothing else changes. Standard library →
zirric runs, formats, tests, serves LSP and manages packages. The CLI →
About the standard library
It is basic, but it tries to make the right choices easier than the ones that bite you. Everything is built with testing in mind, and you notice bad habits right at the import: os is the only module that talks to the machine, so a function that reaches for it directly cannot be tested without one. Take a fs.FileSystem, a clock.SystemClock or a random.Source as a parameter instead, and a test can hand over fs.memory(), clock.fixed(…) or random.seeded(…).
Zirric is evolving quickly. Expect incomplete features and shifting syntax. The Specification describes the language as it is implemented; the proposals record how it got there.