Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variables and Scopes

In Andy C++, you declare new variables with let, like you do in Rust. Use = to reassign an existing variable.

let x = 1;
print(x); // 1

Shadowing

Andy C++ follows Rust-style shadowing. If you declare a variable with an existing name, the new binding hides the old one in that scope.

let x = 1;
let x = 2;
print(x); // 2

You can create a new scope with curly braces. A binding inside that scope can shadow an outer binding. When the scope ends, the outer binding stays available.

let x = 1;
{
  let x = 2;
  print(x); // 2
}
print(x); // 1

Scopes

In Andy C++, every block is an expression. End the block with an expression, not a semicolon, and that expression becomes the block’s value.

let x = {
  let a = 1;
  let b = 2;
  a + b
};

print(x); // 3

Reassignment

The = operator can be used to reassign a value to an existing variable. When you reassign a variable to a value of a different type, the variable’s type is widened to the least upper bound (LUB) of the old and new types.

let x = 1;     // type is Int
x = 2;         // type is still Int
x = 3.14;      // type widens to Number (LUB of Int and Float)
let pos = ();        // type is ()
pos = (1, 2);        // type widens to Sequence<Any>
pos = ("a", "b");    // type is still Sequence<Any>

Tip: For the best type inference, initialize variables with a value that matches the intended type. For example, use let pos = (0, 0); instead of let pos = (); if you intend to store a 2-tuple of numbers.

Type annotations

You can pin a variable’s type by adding : Type after the name. The initialiser still has to fit, the analyser just checks it for you up front.

let count: Int = 0;
let name: String = "world";
let xs: List<Int> = [1, 2, 3];

A subtype is fine — Int fits where Number is asked for, and so on:

let n: Number = 3;        // OK: Int is a Number
let x: Any = "anything";  // OK: everything is Any

A mismatch is rejected with a mismatched types error:

let x: Int = "hello";   // ERROR: mismatched types: found String but expected Int

Once a binding has an annotation, it stays locked to that type. Reassignment and augmented assignment can’t widen it the way they widen an inferred binding:

let x: Int = 5;
x = "test";   // ERROR: mismatched types
x /= 2;       // ERROR: division can produce a Rational, which doesn't fit in Int

If you want a binding that widens freely, just leave the annotation off. Annotations are opt-in.

The same syntax shows up on function parameters and return types — see the Function page.

See Types for the full list of names you can use, including generics like List<T>, Map<K, V>, and tuple shorthand (Int, String).

Destructuring

Destructuring works more like Python than Rust. Commas matter more than the delimiters, so [] and () both work.

The statements below are all equivalent:

let a, b = 3, 4;
let [a, b] = 3, 4;
let (a, b) = [3, 4];
let [a, b] = (3, 4);

You can also destructure nested patterns:

let [a, (b, c)] = (1, [2, 3]);