N-Dimensional Arrays

Eucalypt provides a native n-dimensional array type (also called tensors) backed by a flat contiguous store with shape metadata. Arrays enable efficient grid and matrix operations, in particular for Advent of Code-style grid simulations where list-based approaches are too slow.

Arrays are an immutable, purely functional data structure. All mutation operations return new arrays.

Construction

FunctionDescription
arr.zeros(shape)Create an array of zeros with the given shape list
arr.fill(shape, val)Create an array filled with val with the given shape list
arr.from-flat(shape, vals)Create an array from a flat list of numbers with the given shape

The shape argument is always a list of integers, e.g. [3] for a 1D array of 3 elements or [2, 3] for a 2×3 matrix.

Access and Query

FunctionDescription
arr.get(a, coords)Get element at coordinate list coords in array a
arr.set(a, coords, val)Return new array with element at coords set to val
arr.shape(a)Return shape of array a as a list of integers
arr.rank(a)Return number of dimensions of array a
arr.length(a)Return total number of elements in array a
arr.to-list(a)Return flat list of elements in row-major order
arr.array?(x)true if x is an n-dimensional array
is-array?(x)true if x is an n-dimensional array (alias)

The coords argument to arr.get and arr.set is a list of integers, one per dimension, e.g. [row, col] for a 2D array.

The !! Operator

The indexing operator !! is overloaded for arrays. When the left operand is an array, !! delegates to arr.get:

my-2d-array !! [row, col]   # same as arr.get(my-2d-array, [row, col])
my-1d-array !! [idx]        # same as arr.get(my-1d-array, [idx])

For plain lists, !! retains its original list-index behaviour:

[10, 20, 30] !! 1   # => 20

Transformations

FunctionDescription
arr.transpose(a)Reverse all axes of array a
arr.reshape(a, shape)Reshape array a to new shape (total elements must match)
arr.slice(a, axis, idx)Take a slice along axis at idx, reducing rank by 1

Arithmetic

Array-specific arithmetic (explicit namespace):

FunctionDescription
arr.add(a, b)Element-wise addition; b may be a scalar
arr.sub(a, b)Element-wise subtraction; b may be a scalar
arr.mul(a, b)Element-wise multiplication; b may be a scalar
arr.div(a, b)Element-wise division; b may be a scalar

The standard arithmetic operators +, -, *, / are polymorphic: when either operand is an array, the operation is applied element-wise.

a: arr.from-flat([3], [1, 2, 3])
b: arr.from-flat([3], [10, 20, 30])
c: a + b          # element-wise: [11, 22, 33]
d: a * 2          # scalar broadcast: [2, 4, 6]

Note: / on arrays performs element-wise float division (not floor division as it does for plain integers).

Higher-order Operations

FunctionDescription
arr.indices(a)Return list of coordinate lists for every element, in row-major order
arr.map(f, a)Apply f to each element; return new array of same shape
arr.map-indexed(f, a)Apply f(coords, val) to each element; return new array of same shape
arr.fold(f, init, a)Left-fold f over all elements in row-major order, starting from init
arr.neighbours(a, coords, offsets)Return list of values at valid in-bounds neighbours of coords, given a list of offset vectors

arr.indices returns coordinates as lists; for a 2D array of shape [rows, cols], each entry is [row, col].

arr.neighbours silently skips any out-of-bounds coordinates, so it is safe to call on border elements without special-casing.

# Double every element of a 1D array
a: arr.from-flat([3], [1, 2, 3])
b: arr.map((_ * 2), a)   # => [2, 4, 6] (same shape)

# Sum all elements
total: arr.fold((+), 0, a)   # => 6

# List all coordinates of a 2×2 array
coords: arr.from-flat([2, 2], [0, 0, 0, 0]) arr.indices
# => [[0, 0], [0, 1], [1, 0], [1, 1]]

# Neighbours of centre cell in a 3×3 grid (4-connected)
grid: arr.from-flat([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9])
ns: arr.neighbours(grid, [1, 1], [[-1, 0], [1, 0], [0, -1], [0, 1]])
# => [2, 8, 4, 6]

Example

# 3×3 grid initialised to zero
grid: arr.zeros([3, 3])

# Set a value at row 1, col 2
grid2: grid arr.set([1, 2], 42)

# Read back the value
val: grid2 arr.get([1, 2])    # => 42
val2: grid2 !! [1, 2]         # same thing via !! operator

# Compute element-wise sum of two grids
a: arr.from-flat([2, 2], [1, 2, 3, 4])
b: arr.from-flat([2, 2], [5, 6, 7, 8])
c: a + b   # => [[6, 8], [10, 12]] (as flat list: [6, 8, 10, 12])