## Abstract

Various Second-Order Array Combinators that are operationally parallel in a way that can be exploited by the compiler.

The functions here are all recognised specially by the compiler (or built on those that are). The asymptotic work and span is provided for each function, but note that this easily hides very substantial constant factors. For example, `scan` is much slower than `reduce`, although they have the same asymptotic complexity.

Reminder on terminology: A function `op` is said to be associative if

``(x `op` y) `op` z == x `op` (y `op` z)``

for all `x`, `y`, `z`. Similarly, it is commutative if

``x `op` y == y `op` x``

The value `o` is a neutral element if

``x `op` o == o `op` x == x``

for any `x`.

## Synopsis

 val map 'a [n] 'x : (f: a -> x) -> (as: [n]a) -> *[n]x val map1 'a [n] 'x : (f: a -> x) -> (as: [n]a) -> *[n]x val map2 'a 'b [n] 'x : (f: a -> b -> x) -> (as: [n]a) -> (bs: [n]b) -> *[n]x val map3 'a 'b 'c [n] 'x : (f: a -> b -> c -> x) -> (as: [n]a) -> (bs: [n]b) -> (cs: [n]c) -> *[n]x val map4 'a 'b 'c 'd [n] 'x : (f: a -> b -> c -> d -> x) -> (as: [n]a) -> (bs: [n]b) -> (cs: [n]c) -> (ds: [n]d) -> *[n]x val map5 'a 'b 'c 'd 'e [n] 'x : (f: a -> b -> c -> d -> e -> x) -> (as: [n]a) -> (bs: [n]b) -> (cs: [n]c) -> (ds: [n]d) -> (es: [n]e) -> *[n]x val reduce 'a : (op: a -> a -> a) -> (ne: a) -> (as: []a) -> a val reduce_comm 'a : (op: a -> a -> a) -> (ne: a) -> (as: []a) -> a val reduce_by_index 'a [m] [n] : (dest: *[m]a) -> (f: a -> a -> a) -> (ne: a) -> (is: [n]i32) -> (as: [n]a) -> *[m]a val scan [n] 'a : (op: a -> a -> a) -> (ne: a) -> (as: [n]a) -> *[n]a val filter 'a : (p: a -> bool) -> (as: []a) -> *[]a val partition [n] 'a : (p: a -> bool) -> (as: [n]a) -> ([]a, []a) val partition2 [n] 'a : (p1: a -> bool) -> (p2: a -> bool) -> (as: [n]a) -> ([]a, []a, []a) val stream_red 'a 'b : (op: b -> b -> b) -> (f: []a -> b) -> (as: []a) -> b val stream_red_per 'a 'b : (op: b -> b -> b) -> (f: []a -> b) -> (as: []a) -> b val stream_map 'a 'b : (f: []a -> []b) -> (as: []a) -> *[]b val stream_map_per 'a 'b : (f: []a -> []b) -> (as: []a) -> *[]b val all 'a : (f: a -> bool) -> (as: []a) -> bool val any 'a : (f: a -> bool) -> (as: []a) -> bool val scatter 't [m] [n] : (dest: *[m]t) -> (is: [n]i32) -> (vs: [n]t) -> *[m]t

## Description

val map 'a [n] 'x: (f: a -> x) -> (as: [n]a) -> *[n]x

Apply the given function to each element of an array.

Work: O(n)

Span: O(1)

val map1 'a [n] 'x: (f: a -> x) -> (as: [n]a) -> *[n]x

Apply the given function to each element of a single array.

Work: O(n)

Span: O(1)

val map2 'a 'b [n] 'x: (f: a -> b -> x) -> (as: [n]a) -> (bs: [n]b) -> *[n]x

As `map1`, but with one more array.

Work: O(n)

Span: O(1)

val map3 'a 'b 'c [n] 'x: (f: a -> b -> c -> x) -> (as: [n]a) -> (bs: [n]b) -> (cs: [n]c) -> *[n]x

As `map2`, but with one more array.

Work: O(n)

Span: O(1)

val map4 'a 'b 'c 'd [n] 'x: (f: a -> b -> c -> d -> x) -> (as: [n]a) -> (bs: [n]b) -> (cs: [n]c) -> (ds: [n]d) -> *[n]x

As `map3`, but with one more array.

Work: O(n)

Span: O(1)

val map5 'a 'b 'c 'd 'e [n] 'x: (f: a -> b -> c -> d -> e -> x) -> (as: [n]a) -> (bs: [n]b) -> (cs: [n]c) -> (ds: [n]d) -> (es: [n]e) -> *[n]x

As `map4`, but with one more array.

Work: O(n)

Span: O(1)

val reduce 'a: (op: a -> a -> a) -> (ne: a) -> (as: []a) -> a

Reduce the array `as` with `op`, with `ne` as the neutral element for `op`. The function `op` must be associative. If it is not, the return value is unspecified. If the value returned by the operator is an array, it must have the exact same size as the neutral element, and that must again have the same size as the elements of the input array.

Work: O(n)

Span: O(log(n))

val reduce_comm 'a: (op: a -> a -> a) -> (ne: a) -> (as: []a) -> a

As `reduce`, but the operator must also be commutative. This is potentially faster than `reduce`. For simple built-in operators, like addition, the compiler already knows that the operator is associative.

Work: O(n)

Span: O(log(n))

val reduce_by_index 'a [m] [n]: (dest: *[m]a) -> (f: a -> a -> a) -> (ne: a) -> (is: [n]i32) -> (as: [n]a) -> *[m]a

`reduce_by_index dest f ne is as` returns `dest`, but with each element given by the indices of `is` updated by applying `f` to the current value in `dest` and the corresponding value in `as`. The `ne` value must be a neutral element for `op`. If `is` has duplicates, `f` may be applied multiple times, and hence must be associative and commutative. Out-of-bounds indices in `is` are ignored.

Work: O(n)

Span: O(n) in the worst case (all updates to same position), but O(1) in the best case.

In practice, the O(n) behaviour only occurs if m is also very large.

val scan [n] 'a: (op: a -> a -> a) -> (ne: a) -> (as: [n]a) -> *[n]a

Inclusive prefix scan. Has the same caveats with respect to associativity as `reduce`.

Work: O(n)

Span: O(log(n))

val filter 'a: (p: a -> bool) -> (as: []a) -> *[]a

Remove all those elements of `as` that do not satisfy the predicate `p`.

Work: O(n)

Span: O(log(n))

val partition [n] 'a: (p: a -> bool) -> (as: [n]a) -> ([]a, []a)

Split an array into those elements that satisfy the given predicate, and those that do not.

Work: O(n)

Span: O(log(n))

val partition2 [n] 'a: (p1: a -> bool) -> (p2: a -> bool) -> (as: [n]a) -> ([]a, []a, []a)

Split an array by two predicates, producing three arrays.

Work: O(n)

Span: O(log(n))

val stream_red 'a 'b: (op: b -> b -> b) -> (f: []a -> b) -> (as: []a) -> b

`stream_red op f as` splits `as` into chunks, applies `f` to each of these in parallel, and uses `op` (which must be associative) to combine the per-chunk results into a final result. This SOAC is useful when `f` can be given a particularly work-efficient sequential implementation. Operationally, we can imagine that `as` is divided among as many threads as necessary to saturate the machine, with each thread operating sequentially.

A chunk may be empty, `f []` must produce the neutral element for `op`.

Work: O(n)

Span: O(log(n))

val stream_red_per 'a 'b: (op: b -> b -> b) -> (f: []a -> b) -> (as: []a) -> b

As `stream_red`, but the chunks do not necessarily correspond to subsequences of the original array (they may be interleaved).

Work: O(n)

Span: O(log(n))

val stream_map 'a 'b: (f: []a -> []b) -> (as: []a) -> *[]b

Similar to `stream_red`, except that each chunk must produce an array of the same size. The per-chunk results are concatenated.

Work: O(n)

Span: O(1)

val stream_map_per 'a 'b: (f: []a -> []b) -> (as: []a) -> *[]b

Similar to `stream_map`, but the chunks do not necessarily correspond to subsequences of the original array (they may be interleaved).

Work: O(n)

Span: O(1)

val all 'a: (f: a -> bool) -> (as: []a) -> bool

Return `true` if the given function returns `true` for all elements in the array.

Work: O(n)

Span: O(log(n))

val any 'a: (f: a -> bool) -> (as: []a) -> bool

Return `true` if the given function returns `true` for any elements in the array.

Work: O(n)

Span: O(log(n))

val scatter 't [m] [n]: (dest: *[m]t) -> (is: [n]i32) -> (vs: [n]t) -> *[m]t

The `scatter as is vs` expression calculates the equivalent of this imperative code:

``````for index in 0..length is-1:
i = is[index]
v = vs[index]
as[i] = v``````

The `is` and `vs` arrays must have the same outer size. `scatter` acts in-place and consumes the `as` array, returning a new array that has the same type and elements as `as`, except for the indices in `is`. If `is` contains duplicates (i.e. several writes are performed to the same location), the result is unspecified. It is not guaranteed that one of the duplicate writes will complete atomically - they may be interleaved. See `reduce_by_index` for a function that can handle this case deterministically.

This is technically not a second-order operation, but it is defined here because it is closely related to the SOACs.

Work: O(n)

Span: O(1)