Arrays

Flix supports mutable scoped arrays. An array is a fixed-length mutable sequence of elements that share the same type. Arrays are laid out consecutively in memory. Arrays are mutable; hence their elements can change over time. However, once created, the length of an array cannot be changed.

In Flix, the type of an array is Array[t, r] where t is the type of its elements and r is its region. Like all mutable memory in Flix, every array must belong to some region. Reading from and writing to arrays are effectful operations. For example, reading an element from an array of type Array[t, r] has the effect r. Likewise, creating an array in a region is also an effectful operation.

Arrays are always unboxed. For example, an array of type Array[Int32, r] is represented as a sequence of primitive 32-bit integers, i.e., in JVM terminology, the array is represented as int[]. Flix will never box primitive integers as java.lang.Integer objects but still permits primitives in generic collections and functions. The same is true for other types of primitives and arrays of primitives.

Arrays are low-level data structures typically used to implement higher-level data structures. Therefore, unless implementing such data structures, we recommend that arrays are used sparingly. Instead, we recommend using the MutList, MutDeque, MutSet, and MutMap data structures.

Hint: Use MutList if you need a growable mutable sequence of elements.

Array Literals

The syntax of an array literal is of the form Array#{e1, e2, e3, ...} @ r where e1, e2, and so forth are element expressions, and r is the region expression. For example:

region rc {
    let fruits = Array#{"Apple", "Pear", "Mango"} @ rc;
    println(Array.toString(fruits))
}

Here we introduce a region named rc. Inside the region, we create an array of fruits that contain the three strings "Apple", "Pear", and "Mango". The type of fruits is Array[String, rc]. For more information about regions, we refer to the chapter on Regions.

Running the program prints Array#{"Apple", "Pear", "Mango"}.

Allocating Arrays

We can allocate an array of size n filled with the same element using the Array.repeat function. For example:

region rc {
    let arr = Array.repeat(rc, 1_000, 42);
    println(Array.toString(arr))
}

Here we create an array arr of length 1_000 where each array element has the value 42. Note that we must pass the region rc as an argument to Array.repeat because the function must know to which region the returned array should belong.

We can also create an array filled with all integers from zero to ninety-nine:

region rc {
    let arr = Array.range(rc, 0, 100);
    println(Array.toString(arr))
}

Moreover, we can convert most data structures to arrays. For example:

region rc {
    let fruitList = List#{"Apple", "Pear", "Mango"};
    let fruitArray = List.toArray(rc, fruitList);
}

Note that we must pass the region rc as an argument to List.toArray since the function must know to which region the returned array should belong.

Allocating Arrays with Uninitialized Elements

We can use the Array.new function to create an array of a given length where the content of the array is uninitialized. For example:

region rc {
    let arr: Array[String, rc] = Array.empty(rc, 100);
    // ... initialize `arr` here ...
}

Here we create an array of length 100 of type Array[String, rc]. We use an explicit type annotation : Array[String, rc] to inform Flix of the expected type of the array.

Warning: It is dangerous to use arrays that have uninitialized elements.

What are the elements of an uninitialized array? Flix follows Java (and the JVM) which defines a default value for every primitive-- and reference type. So, for example, the default values for Bool and Int32 are false and 0, respectively. The default value for reference types are null. So be careful! Flix does not have a null value, but one can be indirectly introduced by reading from improperly initialized arrays which can lead to NullPointerExceptions.

Reading from and Writing to Arrays

We can retrieve or update the element at a specific position in an array using Array.get and Array.put, respectively. For example:

region rc {
    let strings = Array.empty(rc, 2);
    Array.put("Hello", 0, strings);
    Array.put("World", 1, strings);
    let s1 = Array.get(0, strings);
    let s2 = Array.get(1, strings);
    println("${s1} ${s2}")
}

Here we create an empty array of length of two. We then store the string "Hello" at position zero and the string "World" at position one. Next, we retrieve the two strings, and print them. Thus the program, when compiled and run, prints Hello World.

We can also write part of the program in a more fluent-style using the !> pipeline operator:

let strings =
    Array.empty(rc, 2) !>
    Array.put("Hello", 0) !>
    Array.put("World", 1);

Slicing Arrays

We can slice arrays using Array.slice. A slice of an array is a new (shallow) copy of a sub-range of the original array. For example

region rc {
    let fruits = Array#{"Apple", "Pear", "Mango"} @ rc;
    let result = Array.slice(rc, start = 1, end = 2, fruits);
    println(Array.toString(result))
}

which prints Array#{"Pear"} when run.

Taking the Length of an Array

We can compute the length of an array using the Array.length function. For example

region rc {
    let fruits = Array#{"Apple", "Pear", "Mango"} @ rc;
    println(Array.length(fruits))
}

which prints 3 when run.

Note: We advise against indexed-based iteration through arrays. Instead, we recommend to use functions such as Array.count, Array.forEach, and Array.transform!.

Additional Array Operations

The Array module offers an extensive collection of functions for working with arrays. For example, Array.append, Array.copyOfRange, Array.findLeft, Array.findRight, Array.sortWith!, and Array.sortBy! to name a few. In total, the module offers more than 100 functions ready for use.