Values and types
The data your programs operate on. Six primitive types, two ways to bind names, and a string syntax with built-in interpolation.
The six primitive types
Capa has six built-in primitives:
Int· whole numbers, signed, 64-bit at runtime:42,-7,0.Float· floating-point numbers:3.14,-0.5,1e10.Bool·trueorfalse.String· text, double-quoted:"Capa".Char· one character, single-quoted:'A'.Unit· the empty value, written(). The return type of any function that "returns nothing".
No silent conversions between them. 1 + 1.0 is a compile error; you write to_float(1) + 1.0 to make the intent explicit. The point is that nothing is ever surprising at run time: if it type-checks, the arithmetic is the arithmetic you wrote.
Binding names: let and var
Two keywords, two purposes:
let n = 42
var counter = 0
let is for values that never change after binding. Trying to reassign one is a compile error:
let n = 42
n = 7 // error: 'n' is bound with `let` and cannot be reassigned
var is for values that change. The type is fixed at binding time; only the value can change.
var counter = 0
counter = counter + 1 // fine
counter = "one" // error: expected Int, got String
Default to let. Reach for var only when the value genuinely changes; in practice this is mostly loop counters and accumulators that cannot be written as a fold. Capa's analyzer reads let as a strong hint to the reader, so prefer it.
Types are inferred, but you can be explicit
The examples above leave the type out: the compiler figures it out from the right-hand side. You can also write it explicitly:
let n: Int = 42
let pi: Float = 3.14
let ok: Bool = true
let name: String = "Capa"
Two reasons to be explicit: as documentation for the reader, and to constrain the inference when you really want a specific type (e.g. let xs: List<Int> = []; without the annotation the empty list cannot be inferred to anything in particular).
The convention in the standard library and the examples is: explicit types on function signatures (always), inferred on local let / var bindings (unless the type adds information).
Strings interpolate by default
To put a value inside a string, use ${expression}. The expression is evaluated and its result is converted to a string:
fun main(stdio: Stdio)
let name = "Capa"
let n = 42
stdio.println("Hello, ${name}! n is ${n}.")
$ capa --run hello.capa
Hello, Capa! n is 42.
The expression inside ${...} can be any expression that produces a value: "${1 + 1}" prints 2. The conversion to string is automatic for every primitive type and for any type that has a __str__ in the runtime (most user types do).
If you actually want the four characters ${ } in your output, double the dollar: "$${" prints ${.
Raw strings. Prefix a string with r to disable both escapes and interpolation: r"C:\path\to\file" contains the literal backslashes. Useful for regular expressions and Windows paths.
The two error modes
The first one we already saw above: reassigning a let binding is a compile error.
The second is type mismatch. Try to add an Int and a String:
fun main(stdio: Stdio)
let result = 42 + "oops"
stdio.println("${result}")
$ capa --run hello.capa
hello.capa:2:18: error: '+': expected Int + Int or Float + Float or String + String,
got Int + String
2 | let result = 42 + "oops"
^
Capa overloads + within each primitive type ("a" + "b" concatenates), but never across them. The error names the mismatch precisely so you know whether you wanted to convert the number to a string or convert the string to a number.
Where you are now
You can declare values, give them types, and produce strings that include them. Next: how to put a sequence of statements behind a name and call it. Functions.