Learn · Chapter 2 of 12

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:

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.