~ / track G / neighboring paradigms

Array programming (APL / J)

Advanced

Array programming treats whole arrays as primary values. Operations are "lifted" to apply element-wise (or rank-wise) without writing loops. APL, J, K, and Q are the canonical languages; NumPy and Clojure's core.matrix borrow the same ideas. Once you internalize it, a loop-heavy algorithm collapses into a one-liner that reads as a transformation of shape.

The core insight

In a normal language: "for each element, do X." In an array language: "X applies to arrays." Adding two vectors isn't a loop; it's just +.

NumPy in Python is the most familiar example:

import numpy as np
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
a + b           # array([11, 22, 33])
a * 2           # array([2, 4, 6])
(a + b).sum()   # 66

In Clojure with core.matrix (or just plain seqs for 1D):

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

The pattern: broadcast operations over arrays of compatible shapes, reduce along an axis when you want a scalar.

APL: notation as a tool of thought

APL pushes this further with single-character operators:

+/ 1 2 3 4 5       ⍝ sum:    15
×/ 1 2 3 4 5       ⍝ product: 120
1 2 3 + 10 20 30   ⍝ elementwise: 11 22 33
⍳5                 ⍝ iota: 1 2 3 4 5
+/⍳100             ⍝ sum 1..100: 5050

The famous APL one-liner for the average:

avg ← {(+/⍵) ÷ ≢⍵}

(+/⍵) sums the argument, ÷ divides by ≢⍵ (the count). The notation is the algorithm — there's no loop, no temporary variable.

Rank and broadcasting

The deep idea in array languages is rank — the number of dimensions of a value. Operators have an expected rank; values are automatically lifted to match. + between a scalar and a vector adds the scalar to each element. Between a vector and a matrix, between two matrices of compatible shape — the rules generalize. NumPy calls this broadcasting; APL/J call it rank polymorphism.

Why think this way

  • No loop bugs. No off-by-ones, no accumulator typos. The shape changes describe the algorithm.
  • Performance. Whole-array primitives are dispatched to optimized C/Fortran code (BLAS, SIMD, GPU kernels).
  • Composability. A pipeline of array operations is itself an array operation; you can transform an algorithm by transforming its shape, not rewriting its body.
  • Data-parallel by nature. "Apply f to every element" is the most paralleliseable shape; array languages were designed for this before GPUs existed.

Where Clojure folks meet array thinking

  • core.matrix, dtype-next, and neanderthal give Clojure NumPy-like arrays with fast kernels.
  • ML and data work in Clojure (Cortex, MXNet bindings) live in this style.
  • Even plain mapv/reduce over a homogeneous vector is array-shaped thinking.

Check yourself

? quiz

What's the practical advantage of writing `a + b` (whole-array add) instead of `for i: c[i] = a[i] + b[i]`?

Exercise

Without writing any explicit loops, compute the dot product of two vectors a and b of equal length: Σᵢ aᵢ · bᵢ.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate
 status: new