Current section
Files
Jump to
Current section
Files
src/glistix/nix/array.gleam
//// Contains types and functions related to Nix's built-in lists (consisting of arrays).
import gleam/int
import gleam/iterator.{type Iterator, Done, Next}
import gleam/order.{type Order}
/// A Nix list. This is not a linked list, but rather a contiguous array.
/// The fastest way to access values in this array is by index.
/// Recursion over this type tends to be slower, as a consequence (would be `O(N^2)`).
pub type Array(element)
/// Creates a new, empty array.
@external(nix, "../../nix_ffi.nix", "array_new")
pub fn new() -> Array(a)
/// Creates an array with one element.
///
/// ## Examples
///
/// ```gleam
/// single(5)
/// // -> from_list([5])
/// ```
@external(nix, "../../nix_ffi.nix", "array_single")
pub fn single(item: a) -> Array(a)
/// Reduces a list of elements into a single value by calling a given function
/// on each element, going from start to end.
///
/// Runs in linear time, and is strict (uses the `foldl'` built-in).
@external(nix, "../../nix_ffi.nix", "array_fold")
pub fn fold(
over array: Array(item),
from init: acc,
with operator: fn(acc, item) -> acc,
) -> acc
/// Reduces a list of elements into a single value by calling a given function
/// on each element, going from end to start.
///
/// Runs in linear time, and is lazy and recursive, so large arrays can cause a stack overflow.
@external(nix, "../../nix_ffi.nix", "array_fold_right")
pub fn fold_right(
over array: Array(item),
from init: acc,
with operator: fn(acc, item) -> acc,
) -> acc
/// Get the element at the given index.
///
/// ## Examples
///
/// ```gleam
/// get(from_list([1, 2, 3]), 0)
/// // -> Ok(1)
///
/// get(from_list([1, 2, 3]), 2)
/// // -> Ok(3)
///
/// get(from_list([1, 2, 3]), 3)
/// // -> Error(Nil)
/// ```
pub fn get(array: Array(a), at index: Int) -> Result(a, Nil) {
case index >= 0 && index < size(array) {
True -> Ok(do_unsafe_get(array, index))
False -> Error(Nil)
}
}
/// Gets the element at the given index without checking.
@external(nix, "../../nix_ffi.nix", "array_get")
fn do_unsafe_get(array: Array(a), index: Int) -> a
/// Returns a new array containing only the elements of the first array after
/// the function has been applied to each one.
///
/// Runs in linear time.
@external(nix, "../../nix_ffi.nix", "array_map")
pub fn map(over array: Array(a), with operator: fn(a) -> b) -> Array(b)
/// Similar to `fold`, but the function receives each element's
/// index alongside the accumulator and the element.
///
/// Runs in linear time and is strict.
pub fn index_fold(
over array: Array(item),
from initial: acc,
with operator: fn(acc, item, Int) -> acc,
) -> acc {
let #(result, _) =
fold(over: array, from: #(initial, 0), with: fn(acc, item) {
let #(acc, index) = acc
#(operator(acc, item, index), index + 1)
})
result
}
/// Similar to `map`, but the function receives each element
/// as well as its index.
///
/// Runs in linear time.
pub fn index_map(
over array: Array(a),
with operator: fn(a, Int) -> b,
) -> Array(b) {
generate(size(array), with: fn(index) {
array
|> do_unsafe_get(index)
|> operator(index)
})
}
/// Similar to `map`, but flattens the resulting array of arrays after mapping.
///
/// This function is more efficient than a `map` followed by `flatten`, as it
/// uses the built-in `builtins.concatMap` function.
///
/// ## Examples
///
/// ```gleam
/// flat_map(from_list([8, 9, 10]), fn(x) { from_list([x, x - 1, x * 2]) })
/// // -> from_list([8, 7, 16, 9, 8, 18, 10, 9, 20])
/// ```
@external(nix, "../../nix_ffi.nix", "array_flat_map")
pub fn flat_map(
over array: Array(a),
with operator: fn(a) -> Array(b),
) -> Array(b)
/// Gets the amount of elements in the array.
///
/// Runs in constant time.
@external(nix, "../../nix_ffi.nix", "array_size")
pub fn size(array: Array(a)) -> Int
/// Checks if an array contains any element equal to the given value.
@external(nix, "../../nix_ffi.nix", "array_contains")
pub fn contains(array: Array(a), any elem: a) -> Bool
/// Returns the first element of the array, if it isn't empty.
///
/// ## Examples
///
/// ```gleam
/// first(from_list([]))
/// // -> Error(Nil)
///
/// first(from_list([1]))
/// // -> Ok(1)
///
/// first(from_list([2, 3, 4]))
/// // -> Ok(2)
/// ```
pub fn first(array: Array(a)) -> Result(a, Nil) {
case size(array) {
0 -> Error(Nil)
_ -> Ok(do_unsafe_first(array))
}
}
/// Returns the first element of the array without checking.
@external(nix, "../../nix_ffi.nix", "array_first")
fn do_unsafe_first(array: Array(a)) -> a
/// Returns the array minus its first element, or `Error(Nil)` if it is empty.
///
/// Note that this runs in linear time, so using `rest` with a recursive algorithm
/// will yield `O(n^2)` complexity. Consider using increasing indices to access the
/// array instead, if possible. Alternatively, use a `List` with such algorithms
/// instead, as the equivalent operation over `List` runs in constant time (while
/// indexing over a `List` runs in linear time).
///
/// ## Examples
///
/// ```gleam
/// rest(from_list([]))
/// // -> Error(Nil)
///
/// rest(from_list([1]))
/// // -> Ok(from_list([]))
///
/// rest(from_list([1, 2]))
/// // -> Ok(from_list([2]))
/// ```
pub fn rest(array: Array(a)) -> Result(Array(a), Nil) {
case size(array) {
0 -> Error(Nil)
_ -> Ok(do_unsafe_rest(array))
}
}
/// Returns the elements of the array after the first without checking.
@external(nix, "../../nix_ffi.nix", "array_rest")
fn do_unsafe_rest(array: Array(a)) -> Array(a)
/// Filters the array, returning a new array containing only the elements
/// for which the predicate function returned `True`.
///
/// ## Examples
///
/// ```gleam
/// filter(from_list([2, 3, 4, 5]), keeping: fn(x) { x > 3 })
/// // -> from_list([4, 5])
///
/// filter(from_list([2, 3, 4, 5]), keeping: fn(x) { x < 1 })
/// // -> from_list([])
/// ```
@external(nix, "../../nix_ffi.nix", "array_filter")
pub fn filter(array: Array(a), keeping predicate: fn(a) -> Bool) -> Array(a)
/// Filters the array, returning a new array containing only the elements
/// for which the given function returned `Ok(new_value)`, replacing each
/// with `new_value`.
///
/// ## Examples
///
/// ```gleam
/// filter_map(
/// from_list([#(1, True), #(2, False), #(3, False), #(4, True)]),
/// with: fn(x) {
/// case x {
/// #(value, True) -> Ok(value)
/// #(_, False) -> Error(Nil)
/// }
/// }
/// )
/// // -> from_list([1, 4])
///
/// filter_map(from_list([2, 3, 4, 5]), with: Error)
/// // -> from_list([])
///
/// filter_map(from_list([2, 3, 4, 5]), with: fn(x) { Ok(x + 1) })
/// // -> from_list([3, 4, 5, 6])
/// ```
pub fn filter_map(
array: Array(a),
with operator: fn(a) -> Result(b, c),
) -> Array(b) {
array
|> flat_map(with: fn(element) {
case operator(element) {
Ok(new_value) -> single(new_value)
Error(_) -> new()
}
})
}
/// Joins the second array to the end of the first using Nix's
/// built-in `++` operator.
///
/// ## Examples
///
/// ```gleam
/// append(from_list([1, 2]), from_list([7, 8]))
/// // -> from_list([1, 2, 7, 8])
/// ```
@external(nix, "../../nix_ffi.nix", "array_append")
pub fn append(first: Array(a), second: Array(a)) -> Array(a)
/// Concatenates an array of arrays into a single array.
/// Uses `builtins.concatLists` for this task.
///
/// ## Examples
///
/// ```gleam
/// let first = from_list([1, 2])
/// let second = from_list([3, 4])
/// let third = from_list([5])
/// concat(from_list([first, second, third]))
/// // -> from_list([1, 2, 3, 4, 5])
/// ```
@external(nix, "../../nix_ffi.nix", "array_concat")
pub fn concat(arrays: Array(Array(a))) -> Array(a)
/// This is the same as `concat`, which joins an array of arrays into
/// a single array.
///
/// ## Examples
///
/// ```gleam
/// let first = from_list([1, 2])
/// let second = from_list([3, 4])
/// let third = from_list([5])
/// flatten(from_list([first, second, third]))
/// // -> from_list([1, 2, 3, 4, 5])
/// ```
pub fn flatten(arrays: Array(Array(a))) -> Array(a) {
concat(arrays)
}
/// Finds the first element in the array for which the function returns `True`.
///
/// If no such element exists, returns `Error(Nil)`.
///
/// Note that, currently, this will always traverse the whole array.
///
/// ## Examples
///
/// ```gleam
/// find(from_list([1, 2, 3, 4, 5]), fn(x) { x > 3 })
/// // -> Ok(4)
///
/// find(from_list([10]), fn(x) { x == 5 })
/// // -> Error(Nil)
/// ```
pub fn find(
in array: Array(a),
one_that is_desired: fn(a) -> Bool,
) -> Result(a, Nil) {
// Folding will be the most efficient way for now while we don't have tail-call
// optimization. See also nixpkgs' `lib/lists.nix`
fold(over: array, from: Error(Nil), with: fn(found, item) {
case found {
Ok(_) -> found
Error(_) ->
case is_desired(item) {
True -> Ok(item)
False -> found
}
}
})
}
/// Finds the first element in the array for which the function returns `Ok(new_value)`,
/// and then returns `Ok(new_value)`.
///
/// If no such element exists (that is, the function returns `Error(_)` for all
/// elements in the array, or it is empty), returns `Error(Nil)`.
///
/// Note that, currently, this will always traverse the whole array.
///
/// ## Examples
///
/// ```gleam
/// find_map(
/// from_list([#(1, False), #(2, False), #(3, True), #(4, True)]), fn(x) {
/// case x {
/// #(value, True) -> Ok(value)
/// #(_, False) -> Error(Nil)
/// }
/// })
/// )
/// // -> Ok(3)
///
/// find_map(from_list([from_list([]), from_list([10]), from_list([12, 13])), first)
/// // -> Ok(10)
///
/// find_map(from_list([from_list([]), from_list([])]), first)
/// // -> Error(Nil)
///
/// find_map(from_list([]), first)
/// // -> Error(Nil)
/// ```
pub fn find_map(
in array: Array(a),
with operator: fn(a) -> Result(b, c),
) -> Result(b, Nil) {
fold(over: array, from: Error(Nil), with: fn(found, item) {
case found {
Ok(_) -> found
Error(_) ->
case operator(item) {
Ok(new_value) -> Ok(new_value)
Error(_) -> found
}
}
})
}
/// Similar to `fold`, but the initial value of the accumulator is the first
/// element in the array. Returns `Error(Nil)` if the array was empty.
///
/// ## Examples
///
/// ```gleam
/// reduce(over: from_list([1, 2, 3]), with: fn(a, b) { a + b })
/// // -> Ok(6)
///
/// reduce(over: from_list([]), with: fn(a, b) { a + b })
/// // -> Error(Nil)
/// ```
pub fn reduce(
over array: Array(a),
with operator: fn(a, a) -> a,
) -> Result(a, Nil) {
fold(over: array, from: Error(Nil), with: fn(acc, elem) {
// Ensure we skip the first element upon folding.
// Folding over `rest(array)` instead would be expensive.
case acc {
Ok(acc) -> Ok(operator(acc, elem))
Error(_) -> Ok(elem)
}
})
}
/// Reverses the array, returning a new array with its elements in the opposite
/// order as the given array.
///
/// Runs in linear time.
///
/// ## Examples
///
/// ```gleam
/// reverse(from_list([1, 2, 3, 4]))
/// // -> from_list([4, 3, 2, 1])
/// ```
pub fn reverse(array: Array(a)) -> Array(a) {
let len = size(array)
generate(len, fn(i) { do_unsafe_get(array, len - 1 - i) })
}
/// Sorts an array using the built-in `sort` function through
/// the given comparator. Sorts in ascending order by default,
/// but the order can be reversed through `order.reverse`
/// in the standard library.
///
/// This uses a stable sort algorithm, meaning elements which compare equal
/// preserve their relative order.
///
/// ## Examples
///
/// ```gleam
/// sort(from_list([3, 10, 4, 32]), by: int.compare)
/// // -> from_list([32, 10, 4, 3])
///
/// sort(from_list([3, 10, 4, 32]), by: order.reverse(int.compare))
/// // -> from_list([3, 4, 10, 32])
/// ```
pub fn sort(array: Array(a), by compare: fn(a, a) -> Order) -> Array(a) {
do_sort(array, fn(a, b) { compare(a, b) == order.Lt })
}
/// The compare function must return True if the first element is less than the
/// second.
@external(nix, "../../nix_ffi.nix", "array_sort")
fn do_sort(array: Array(a), compare: fn(a, a) -> Bool) -> Array(a)
/// Partitions an array's elements into a pair of arrays based on the output
/// of the given function. The first array returned includes elements for which
/// the function returned `True`, while the second array includes elements for
/// which the function returned `False`.
@external(nix, "../../nix_ffi.nix", "array_partition")
pub fn partition(
array: Array(a),
with categorise: fn(a) -> Bool,
) -> #(Array(a), Array(a))
/// Splits an array in two before the given index.
/// If the array isn't long enough to contain that index,
/// the first returned array will be equal to the full given
/// array, and the second returned array will be empty.
///
/// ## Examples
///
/// ```gleam
/// split(from_list([12, 34, 56]), at: 0)
/// // -> #(from_list([]), from_list([12, 34, 56]))
///
/// split(from_list([12, 34, 56]), at: 1)
/// // -> #(from_list([12]), from_list([34, 56]))
///
/// split(from_list([12, 34, 56]), at: 3)
/// // -> #(from_list([12, 34, 56]), from_list([]))
/// ```
pub fn split(array: Array(a), at index: Int) -> #(Array(a), Array(a)) {
let size = size(array)
case index < size {
True -> #(
generate(index, with: fn(i) { do_unsafe_get(array, i) }),
generate(size - index, with: fn(i) { do_unsafe_get(array, i + index) }),
)
False -> #(array, from_list([]))
}
}
/// Takes a specific portion of an array, slicing from the given
/// position and stopping after the given length, generating a new array.
/// Negative lengths can be used to take from the end of the array.
/// If the slicing would go out of bounds, returns an error.
///
/// ## Examples
///
/// ```gleam
/// slice(from: from_list([1, 2, 3, 4]), at: 1, take: 2)
/// // -> Ok(from_list([2, 3]))
///
/// slice(from: from_list([1, 2, 3, 4]), at: 4, take: -3)
/// // -> Ok(from_list([2, 3, 4]))
///
/// slice(from: from_list([]), at: 1, take: 2)
/// // -> Error(Nil)
/// ```
pub fn slice(
from array: Array(a),
at position: Int,
take length: Int,
) -> Result(Array(a), Nil) {
let start = int.min(position, position + length)
let end = int.max(position, position + length)
case start < 0 || end > size(array) {
True -> Error(Nil)
False ->
Ok(
generate(int.absolute_value(length), with: fn(i) {
do_unsafe_get(array, i + start)
}),
)
}
}
/// Splits an array's elements into chunks of fixed size.
/// If the chunk size doesn't evenly divide the array length,
/// the last chunk will be incomplete, containing only the
/// remaining elements.
///
/// Specifying a chunk size smaller than 1 is the same as
/// specifying 1.
///
/// ## Examples
///
/// ```gleam
/// from_list([1, 2, 3, 4, 5, 6]) |> sized_chunk(into: 2)
/// // -> from_list([from_list([1, 2]), from_list([3, 4]), from_list([5, 6])])
///
/// from_list([1, 2, 3, 4, 5, 6, 7, 8]) |> sized_chunk(into: 3)
/// // -> from_list([from_list([1, 2, 3]), from_list([4, 5, 6]), from_list([7, 8])])
/// ```
pub fn sized_chunk(in array: Array(a), into count: Int) -> Array(Array(a)) {
case count > 1 {
True -> {
let len = size(array)
let full_chunks = len / count
let incomplete_chunks = case len % count {
0 -> 0
_ -> 1
}
generate(full_chunks + incomplete_chunks, fn(i) {
let start = count * i
let size = case i == full_chunks {
True -> len - start
False -> count
}
generate(size, fn(i) { do_unsafe_get(array, start + i) })
})
}
False -> map(array, single)
}
}
/// Transpose rows and columns of the array of arrays.
///
/// Traverses the array once to determine the amount of columns,
/// and then traverses again for each column.
///
/// ## Examples
///
/// ```gleam
/// transpose(from_list([from_list([1, 2, 3]), from_list([4, 5, 6]), from_list([7, 8, 9])]))
/// // -> from_list([from_list([1, 4, 7]), from_list([2, 5, 8]), from_list(3, 6, 9)])
/// ```
pub fn transpose(rows: Array(Array(a))) -> Array(Array(a)) {
let columns =
fold(over: rows, from: 0, with: fn(acc, row) {
row
|> size
|> int.max(acc)
})
generate(columns, with: fn(i) {
rows
|> filter_map(with: fn(row) { get(row, i) })
})
}
/// Checks if the predicate is satisfied for all elements in the array,
/// returning `True` if the function returns `True` for all elements,
/// or `False` if it returned `False` for at least one element.
@external(nix, "../../nix_ffi.nix", "array_all")
pub fn all(in array: Array(a), satisfying predicate: fn(a) -> Bool) -> Bool
/// Checks if the predicate is satisfied for at least one element in the array,
/// returning `True` if the function returns `True` for one or more elements,
/// or `False` if it returned `False` for all elements.
@external(nix, "../../nix_ffi.nix", "array_any")
pub fn any(in array: Array(a), satisfying predicate: fn(a) -> Bool) -> Bool
/// Combines two arrays into an array of 2-element tuples, where the tuple at
/// position 'i' contains element 'i' from the first array and element 'i' from
/// the second array.
///
/// If one array is longer than the other, the returned array will have the
/// size of the shortest, with the longer array's extra items being ignored.
///
/// ## Examples
///
/// ```gleam
/// zip(from_list([1, 2]), from_list([3, 4]))
/// // -> from_list([#(1, 3), #(2, 4)])
///
/// zip(from_list([1, 2]), from_list([3]))
/// // -> from_list([#(1, 3)])
///
/// zip(from_list([1, 2]), from_list([]))
/// // -> from_list([])
/// ```
pub fn zip(first: Array(a), with second: Array(b)) -> Array(#(a, b)) {
let len = int.min(size(first), size(second))
generate(len, with: fn(i) {
#(do_unsafe_get(first, i), do_unsafe_get(second, i))
})
}
/// Takes an array of 2-element tuples and returns two arrays.
///
/// ## Examples
///
/// ```gleam
/// unzip(from_list([#(1, 2), #(3, 4)]))
/// // -> #(from_list([1, 3]), from_list([2, 4]))
///
/// unzip([])
/// // -> #(from_list([]), from_list([]))
/// ```
pub fn unzip(input: Array(#(a, b))) -> #(Array(a), Array(b)) {
#(map(input, fn(pair) { pair.0 }), map(input, fn(pair) { pair.1 }))
}
/// Creates an array of integers from start to finish, inclusive.
///
/// ## Examples
///
/// ```gleam
/// range(0, 0)
/// // -> from_list([0])
///
/// range(0, 5)
/// // -> from_list([0, 1, 2, 3, 4, 5])
///
/// range(1, -5)
/// // -> from_list([1, 0, -1, -2, -3, -4, -5])
/// ```
pub fn range(from start: Int, to stop: Int) -> Array(Int) {
let step = case start <= stop {
True -> 1
False -> -1
}
generate(1 + int.absolute_value(stop - start), fn(i) { start + step * i })
}
/// Converts a Gleam list to a Nix array.
///
/// Runs in linear time, and is recursive, so large lists can cause a stack overflow.
@external(nix, "../../nix_ffi.nix", "array_from_list")
pub fn from_list(list: List(a)) -> Array(a)
/// Converts a Nix array to a Gleam list.
///
/// Runs in linear time.
@external(nix, "../../nix_ffi.nix", "array_to_list")
pub fn to_list(array: Array(a)) -> List(a)
/// Converts a Gleam iterator to a Nix array.
///
/// Runs in linear time.
pub fn from_iterator(iterator: Iterator(a)) -> Array(a) {
iterator
|> iterator.fold(from_list([]), fn(acc, elem) {
acc
|> append(from_list([elem]))
})
}
/// Converts a Nix array to a Gleam iterator.
pub fn to_iterator(array: Array(a)) -> Iterator(a) {
let count = size(array)
iterator.unfold(from: 0, with: fn(i) {
case i == count {
True -> Done
False -> Next(do_unsafe_get(array, i), i + 1)
}
})
}
/// Generates an array with a specified length. Takes a function which specifies
/// a value for each index in the new array.
///
/// Runs in linear time, but is not recursive (uses the built-in `genList` function).
@external(nix, "../../nix_ffi.nix", "array_generate")
pub fn generate(
length: Int,
with generator: fn(Int) -> element,
) -> Array(element)