Sequences, Maps, and Flows

Four structures carry data through a J2 program: sequences for order, maps for names, pairs for twos, and flows for laziness.

Sequences

A sequence is an ordered, growable collection written in square brackets. Indexing is zero based, and elements may be reassigned in place:

xs := [10, 20, 30]
print(xs[0])       # 10
print(len(xs))     # 3

xs[1] = 99
print(xs)          # [10,99,30]

Indexes must be in range and non-negative; anything else raises IndexError. There is no from-the-end indexing; the last element is xs[len(xs) - 1].

Growing

push appends in place. + concatenates into a new sequence, and the compound form xs += [x] rebinds a mutable name to the enlarged sequence:

xs := [1, 2]
push(xs, 3)          # in place
print(xs)            # [1,2,3]

ys := []
for n in 1..4 { ys += [n * n] }
print(ys)            # [1,4,9,16]

print([1, 2] + [3])  # [1,2,3], a new seq

For numeric work, build the sequence at its final size with make_seq(n, init) and write by index; this is both the fastest shape and the one the parallelizer recognizes best:

n = 8
squares := make_seq(n, 0)
for i in 0..(n - 1) {
    squares[i] = i * i
}
print(squares)    # [0,1,4,9,16,25,36,49]

Reference semantics

A sequence is shared, not copied, when bound to another name or passed to a function. Mutation through one reference is visible through all of them:

a := [1, 2, 3]
b = a
push(b, 4)
print(a)    # [1,2,3,4]: a and b are the same seq

Concatenation with + is the escape hatch: it builds a fresh sequence, so copy = xs + [] makes an independent copy.

Slicing and reshaping

slice(s, i, j) returns elements from i up to but not including j. The standard library rounds out the family: sort, sort_by, reverse, unique, flatten, zip, enumerate, take, contains, join, and the reductions sum, min, and max. Each is specified in the Standard Library.

s = [3, 1, 4, 1, 5, 9, 2, 6]
print(slice(s, 1, 4))    # [1,4,1]
print(sort(s))           # [1,1,2,3,4,5,6,9]
print(unique(s))         # [3,1,4,5,9,2,6]
print(take(s, 3))        # [3,1,4]
print(sum(s))            # 31

Nesting

Sequences nest to form grids and tables. Chained indexing reads and writes the inner elements:

grid := [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(grid[1][2])    # 6
grid[1][1] = 50
print(grid[1])       # [4,50,6]

Maps

A map associates text keys with values. Literal keys are written bare, like field names, which keeps map literals close to what they usually are: lightweight records.

person := {name: "Ada", born: 1815}

print(person.name)          # dot access
print(person["born"])       # index access with a text key

person.role = "engineer"    # add or update a field
print(len(person))          # 3

Reading a key that is not present raises KeyError. Both access forms read the same data: use dot syntax when the key is a fixed name in your program and index syntax when the key arrives in a variable.

0.1.0

Adding or updating entries is done with dot syntax, as above. Assigning through index syntax (m["key"] = v) is not supported in this release, and map keys in literals must be bare identifiers.

Maps are references, like sequences, and they are records rather than iterable collections: a map cannot be looped over. When you need to iterate labeled data, use a sequence of maps:

people = [
    {name: "Ann", age: 30},
    {name: "Bob", age: 25}
]

for p in people {
    print(fmt("{} is {}", p.name, p.age))
}

ages = map(people, func(p) = p.age)
print(sum(ages))    # 55

Pairs

A pair groups exactly two values: (a, b). Pairs appear mostly as the output of zip and enumerate, and the way to take one apart is the two-variable for:

points = zip([1, 2, 3], [10, 20, 30])
for x, y in points {
    print(x * y)      # 10 40 90
}

Pairs are not indexable and have no field names; if a value needs structure beyond position, reach for a map or a class.

Flows

A flow is a lazy stream of values. Ranges are the flows you meet every day: 1..100 holds no hundred integers, it produces them on demand. Loops, reductions, and collect all consume flows:

print(sum(1..1000))        # 500500, nothing materialized
print(collect(3..6))       # [3,4,5,6], now it is a seq

An open-ended range a.. is an infinite flow. J2 refuses to consume one without a bound: collect(1..) raises InfiniteFlowError rather than hanging. The until clause of a loop is the stopping condition that makes an infinite flow usable:

seen := []
for n in 1.. until n * n > 20 {
    seen += [n]
}
print(seen)    # [1,2,3,4,5]