Introducing: Elm-Static-Array
Use Arrays with fixed length! All checks are done in compile time. Is this possible? Yes it is! The magic word is Phantom types.
The package contains three types:
-
StaticArray n a
- A wrapper type over(a,Array a)
representing a non-empty array. The length is known in compile time. -
Index n
- A wrapper type overInt
. Allows values between0
andn
. It is essentially aRange
type. Checks are done in run time but the max-value is known in compile time. -
Length n
- A wrapper type overInt
. Values can not be changed in run time. The value is known in compile type, thus making the magic happen.
Pros
- The length is checked in compile time by using phantom types.
- Wrapper Types ensure that no performance is lost.
- Convert Custom Types into
Int
and back without needing a dead branch in the case distinction.
Cons
- Construction is a bit slower (should be neglectable for most cases).
- If you don’t know why this package is useful, you should not be using it.
Examples
Construction
StaticArray.fromList (Length.five |> Length.plus1) 0 [1,2,3,4,5]
Appending Elements
six =
Length.five |> Length.plus1
twelve =
Length.ten |> Length.plus2
array1 =
StaticArray.fromList six 0 [1,2,3,4,5]
|> StaticArray.toRecord
array2 =
StaticArray.fromList six 0 [1,2,3,4,5]
|> StaticArray.toRecord
StaticArray.fromRecord
{ length = twelve
, head = array1.head
, tail = Array.append (array1.tail |> Array.push array2.head) array2.tail
}
Notice that we can NOT do addition in compile time, therefore we need to construct 2*6 manually.
Removing Elements
six =
Length.five |> Length.plus1
array =
StaticArray.fromList six [0,1,2,3,4,5]
array
|> StaticArray.resize (six |> Length.minus1)
--> StaticArray.fromList Legnth.five [0,1,2,3,4]
Avoiding index out of bounds errors
array =
StaticArray.fromList Length.five [0,1,2,3,4]
fifthIndex =
array |> Index.last (Array |> StaticArray.legnth)
array
|> StaticArray.resize Lenght.one
|> StaticArray.get fifthIndex --COMPILER ERROR
Converting into/from Int
type Food =
Apples
| Oranges
asArray : StaticArray Two Food
asArray =
StaticArray.fromList Length.two Apples [Oranges]
toInt : Food -> Int
toInt food =
case food of
Apples -> 0
Oranges -> 1
fromInt : Int -> Food
fromInt int =
asArray
|> StaticArray.get (int |> Index.fromModBy Length.two)
No dead branch needed.
I know what your saying: “This is sorcery”. No it’s not, so let me show you how its done:
First we create types representing different natural numbers.
type One
= One
type OnePlus a
= OnePlus a
type alias Two =
OnePlus One
type alias Three =
OnePlus Two
--and so on.
For creating different values of the Length
type we just hard code the values:
type Length n
= C Int
one : Length One
one =
C 1
plus1 : Length n -> Length (OnePlus n)
plus1 (C n) =
C (n + 1)
minus1 : Length (OnePlus n) -> Length n
minus1 (C n) =
C (n - 1)
--and so on
We now can construct all indexes by using a length
type Index n
= I Int
range : Length n -> List (Index n)
range (C const) =
List.range 0 const
|> List.map
When using get
we can ensure in compile time that the index is in bound
get : Index n -> StaticArray n a -> a
get (I n) (A ( head, tail )) =
if n == 0 then
head
else
tail
|> Array.get (n - 1)
|> Maybe.withDefault head --dead branch