Whereas a list's representation is a linked list, which allows for a tail to be undefined, an array takes a contiguous chunk of memory, so its whole spine needs to be defined.
One way to see this distinction is as the following equations:
listArray (0,1) (a : b : t) = array (0, 1) [(0, a), (1, b)]
listArray (0,1) (a : ⊥) = ⊥
where array (0,1) [(0, a), (1, b)]
denotes the array containing the two elements a
and b
respectively at indices 0
and 1
, and ⊥
is the undefined value.
Note in particular that in the first equation, the array is still defined if a
, b
, or t
are ⊥
. (Consider array
as the constructor for the Array
type; it is a primitive which cannot be expressed as a user-defined data type.)
A Haskell program is a system of equations, whose unknown are all of the functions and values, and the meaning of the program is the least solution of that system with respect to the definedness ordering (it is guaranteed that a solution exists, and that there is a least one).
To show that a value (goodArray
) is indeed defined (i.e., more than ⊥
), it is sufficient to unfold its definition until we reach a constructor form (in this case, array
). This guarantees that goodArray = ⊥
is not a solution of the equation(s).
goodArray :: Array Int Int
goodArray = listArray (0, 1) (go 0)
where
go x = x : go ((goodArray ! x) + 1)
-- unfold go
goodArray = listArray (0, 1) (0 : go ((goodArray ! 0) + 1))
-- unfold go again
= listArray (0, 1) (0 : (goodArray ! 0) + 1 : go (...))
-- by the listArray equation above
= array (0,1) [(0, 0), (1, (goodArray ! 0) + 1)]
Hence goodArray
is defined: it is at least an array
, and that allows the indexing to succeed to further simplify that expression.
In the second version, badArray
, where go
is strict, the difference is that the second unfolding of go
is not allowed until we know that its argument is defined.
Indeed, the strictness annotation produces the following equations for go
, with a side condition in the second one:
go ⊥ = ⊥
go x = x : go ((badArray ! x) + 1) if x is not ⊥
Intuitively, before you can apply the definition of go
to unfold go ((badArray ! x) + 1)
, you have to evaluate its argument to an actual integer, and clearly you end up in an infinite loop.
That may be enough to convince yourself that badArray
is undefined, but "it goes on forever" is a tricky argument to get right. A more finitary proof technique is the following.
To prove that a value (badArray
) is undefined is to show that ⊥
is a solution of its defining equation(s), in other words, show that badArray = ⊥
indeed satisfies the following system:
badArray = listArray (0,1) (go 0)
go ⊥ = ⊥
go x = x : go ((badArray ! x) + 1) if x is not ⊥
To check that ⊥
is indeed a solution,
replace badArray
with ⊥
and simplify:
⊥ = listArray (0,1) (go 0)
= listArray (0,1) (0 : go ((⊥ ! 0) + 1)) -- (!) is strict
= listArray (0,1) (0 : go (⊥ + 1)) -- (+) is strict
= listArray (0,1) (0 : go ⊥) -- go is strict
= listArray (0,1) (0 : ⊥) -- listArray needs as many elements from the list as the range (0,1) requires
= ⊥