Array slicing is already a very rich feature in F#, and it's been slightly improved in F# 5. In this post we're going to look at slicing in some detail. If you only want to know what's new for F# 5, skip straight to the end.
What is array slicing?
Array slicing is an F# feature stolen from many programming languages before it. It's a way to get a subset of items from an array.
// Given the array [| 0; 1; 2; ...; 99 |]
let array = [| 0 .. 99 |]
// Get me the items from index 5 to 10 inclusive
array.[5 .. 10] // [|5; 6; 7; 8; 9; 10|]
// From index 10 to 10
array.[10 .. 10] // [|10|]
// If the slice start is later than the slice end then you get an empty array.
array.[10 .. 9] // [||]
You can also slice F# lists and strings in the same way.
[ 0 .. 9 ].[6 .. 8] // [6; 7; 8]
let text = "Time flies like an arrow; fruit flies like a banana"
text.[16 .. 23] // "an arrow"
Unbounded slices
You can omit the boundary at the beginning or end of any slice. This will extend the slice to start or the end of the collection.
let array = [| 0 .. 99 |]
array.[97 ..] // [|97; 98; 99|]
Custom slicing
If you define your own collection type for any reason in F#, you can implement the GetSlice
member to allow slicing directly on your type.
type MyCoolCollection<'a> =
| MyCoolCollection of 'a array
member this.GetSlice(start, finish) =
match this with
| MyCoolCollection arr ->
let start = start |> Option.defaultValue 0
let finish = finish |> Option.defaultValue (arr.Length - 1)
MyCoolCollection arr.[start .. finish]
let collection = MyCoolCollection [| 0 .. 9 |]
collection.[1 .. 2] // MyCoolCollection [|1; 2|]
collection.[8 .. ] // MyCoolCollection [|8; 9|]
Multi-dimensional slicing
If you use multi-dimensional arrays, you can also use slices in each dimension.
let grid =
[| [| 1; 2; 3 |]
[| 4; 5; 6 |]
[| 7; 8; 9 |] |]
|> array2D
// Normal index access of individual items.
// On the first row, get the second item.
grid.[0, 1]
// 2
// Using a slice on one dimension.
// On the first row, get the first two items.
grid.[0, 0 .. 1]
// int [] = [|1; 2|]
// Using slices in both dimensions.
// Get items that are in the first two rows and first two columns.
grid.[0 .. 1, 0 .. 1]
// [[1; 2]
// [4; 5]]
// Using a partially unbounded slice in one dimension.
// Get items that are in the first two rows and skip the first column.
grid.[0 .. 1, 1 ..]
// [[2; 3]
// [5; 6]]
// Using a completely unbounded in one dimension.
// Get the first row, for all columns.
grid.[0, *]
// [|1; 2; 3|]
// Get the first column for all rows.
grid.[*, 0]
// [|1; 4; 7|]
Note that whenever a fixed index is accessed for only one of the two dimensions, the result will be an array instead of a 2D array.
How is slicing better in F# 5?
Consistency and runtime safety: In F# 5, slicing is slightly more consistent between lists, arrays and strings in a way that is more permissive. For all collection types, if you specify a range that is out of range, you will now get an empty collection instead of a runtime exception. This is useful considering the slice range is often calculated rather than written as a numeric literal, as in my examples. We can now be confident our code won't throw exceptions and the empty collection is usually a more useful result.
[0 .. 3].[5 .. 6] // []
[|0 .. 3|].[5 .. 6] // [||]
"0123".[5 .. 6] // ""
Mixing fixed index and slices for 3D and 4D arrays: Before F# 5, you couldn't use a mix of fixed indexes and slices between the different dimensions of 3D and 4D arrays (this is something that I have already demonstrated for 2D arrays above). But now you can.
Warning: This is still marked as an experimental feature for F# 5 and requires you to be using the preview version.
let cube = Array3D.init 2 2 2 (fun i j k -> (i, j, k))
cube.[0, *, 0 .. 1]
// [[(0, 0, 0); (0, 0, 1)]
// [(0, 1, 0); (0, 1, 1)]]
Happy slicing! 🍕🔪🍰🔪🥧🔪🍣