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
a arr.get(coords)Get element at coordinate list coords in array a
a arr.set(coords, val)Return new array with element at coords set to val
a arr.shapeReturn shape of array a as a list of integers
a arr.rankReturn number of dimensions of array a
a arr.lengthReturn total number of elements in array a
a arr.to-listReturn 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 my-2d-array arr.get([row, col])
my-1d-array !! [idx]        # same as my-1d-array arr.get([idx])

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

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

Transformations

FunctionDescription
a arr.transposeReverse all axes of array a
a arr.reshape(shape)Reshape array a to new shape (total elements must match)
a arr.slice(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
a arr.indicesReturn 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
a arr.neighbours(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: grid arr.neighbours([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])