1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
%**<title>A Gentle Introduction to Haskell: Arrays</title>
%**~header
\section{Arrays}
Ideally, arrays in a functional language would be regarded simply as
functions from indices to values, but pragmatically, in order to
assure efficient access to array elements, we need to
be sure we can take advantage of the special properties of the domains
of these functions, which are isomorphic to finite contiguous subsets
of the integers. Haskell, therefore, does not treat arrays as general
functions with an application operation, but as abstract data types
with a subscript operation.
Two main approaches to functional arrays may be discerned: {\em
incremental} and {\em monolithic} definition. In the incremental
case, we have a function that produces an empty array of a given size
and another that takes an array, an index, and a value, producing a
new array that differs from the old one only at the given index.
Obviously, a naive implementation of such an array semantics would be
intolerably inefficient, either requiring a new copy of an array for each
incremental redefinition, or taking linear time for array lookup; thus, serious attempts at using this
approach employ sophisticated static analysis and clever run-time
devices to avoid excessive copying. The monolithic approach, on the
other hand, constructs an array all at once, without reference to
intermediate array values. Although Haskell has an incremental array
update operator, the main thrust of the array facility is monolithic.
Arrays are not part of the Standard Prelude---the standard library
contains the array operators. Any module using
arrays must import the @Array@ module.
\subsection{Index types}
The @Ix@ library defines a type class of array indices:
\bprog
@
class (Ord a) => Ix a where
range :: (a,a) -> [a]
index :: (a,a) a -> Int
inRange :: (a,a) -> a -> Bool
@
\eprog
Instance declarations are provided for @Int@, @Integer@, @Char@,
@Bool@, and tuples of @Ix@ types up to length 5; in addition, instances may be
automatically derived for enumerated and tuple types. We regard the
primitive types as vector indices, and tuples as indices of
multidimensional rectangular arrays. Note that the first argument of
each of the operations of class @Ix@ is a pair of indices; these are
typically the {\em bounds} (first and last indices) of an array. For
example, the bounds of a 10-element, zero-origin vector with @Int@
indices would be @(0,9)@, while a 100 by 100 1-origin matrix might have
the bounds @((1,1),(100,100))@. (In many other languages, such bounds
would be written in a form like @1:100, 1:100@, but the present
form fits the type system better, since each bound is of the same
type as a general index.)
The @range@ operation takes a bounds pair and produces the list of
indices lying between those bounds, in index order. For example,
\[ @range (0,4)@\ \ \ \ \red\ \ \ \ @[0,1,2,3,4]@ \]
\[ @range ((0,0),(1,2))@\ \ \ \ \red\ \ \ \ %
@[(0,0), (0,1), (0,2), (1,0), (1,1), (1,2)]@ \]
The @inRange@ predicate determines whether an index lies between a given
pair of bounds. (For a tuple type, this test is performed
component-wise.) Finally, the @index@ operation allows
a particular element of an array to be addressed: given a bounds pair and an
in-range index, the operation yields the zero-origin ordinal of the
index within the range; for example:
\[ @index (1,9) 2@\ \ \ \ \red\ \ \ \ @1@ \]
\[ @index ((0,0),(1,2)) (1,1)@\ \ \ \ \red\ \ \ \ @4@ \]
\subsection{Array Creation}
Haskell's monolithic array creation function forms an array from a
pair of bounds and a list of index-value pairs (an {\em association
list}):
\bprog
@
array :: (Ix a) => (a,a) -> [(a,b)] -> Array a b
@
\eprog
Here, for example, is a definition of an array of the squares
of numbers from 1 to 100:
\bprog
@
squares = array (1,100) [(i, i*i) | i <- [1..100]]
@
\eprog
This array expression is typical in using a list comprehension for
the association list; in fact, this usage results in array expressions
much like the {\em array comprehensions} of the language Id~\cite{id-manual}.
Array subscripting is performed with the infix operator @!@, and the
bounds of an array can be extracted with the function @bounds@:
\[ @squares!7@\ \ \ \ \red\ \ \ \ @49@ \]
\[ @bounds squares@\ \ \ \ \red\ \ \ \ @(1,100)@ \]
We might generalize this example by parameterizing the bounds and the
function to be applied to each index:
\bprog
@
mkArray :: (Ix a) => (a -> b) -> (a,a) -> Array a b
mkArray f bnds = array bnds [(i, f i) | i <- range bnds]
@
\eprog
Thus, we could define @squares@ as @mkArray (\i -> i * i) (1,100)@.
Many arrays are defined recursively; that is, with the values of some
elements depending on the values of others. Here, for example, we
have a function returning an array of Fibonacci numbers:
\bprog
@
fibs :: Int -> Array Int Int
fibs n = a where a = array (0,n) ([(0, 1), (1, 1)] ++
[(i, a!(i-2) + a!(i-1)) | i <- [2..n]])
@
\eprog
Another example of such a recurrence is the "n" by "n" {\em wavefront}
matrix, in which elements of the first row and first column all have
the value "1" and other elements are sums of their neighbors to the
west, northwest, and north:
\bprog
@
wavefront :: Int -> Array (Int,Int) Int
wavefront n = a where
a = array ((1,1),(n,n))
([((1,j), 1) | j <- [1..n]] ++
[((i,1), 1) | i <- [2..n]] ++
[((i,j), a!(i,j-1) + a!(i-1,j-1) + a!(i-1,j))
| i <- [2..n], j <- [2..n]])
@
\eprog
The wavefront matrix is so called because in a parallel
implementation, the recurrence dictates that the computation can begin
with the first row and column in parallel and proceed as a
wedge-shaped wave, traveling from northwest to southeast. It is
important to note, however, that no order of computation is specified
by the association list.
In each of our examples so far, we have given a unique association for
each index of the array and only for the indices within the bounds
of the array, and indeed, we must do this in general for an array
be fully defined. An association with an out-of-bounds index results
in an error; if an index is missing or appears more than once, however,
there is no immediate error, but the value of the array at that index
is then undefined, so that subscripting the array with such an index
yields an error.
\subsection{Accumulation}
We can relax the restriction that an index appear at most once in the
association list by specifying how to combine multiple values
associated with a single index; the result is called an {\em accumulated
array}:
\bprog
@
accumArray :: (Ix a) -> (b -> c -> b) -> b -> (a,a) -> [Assoc a c] -> Array a b
@
\eprog
The first argument of @accumArray@ is the {\em accumulating function},
the second is an initial value (the same for each element of the array),
and the remaining arguments are bounds and an association list, as with
the @array@ function. Typically, the accumulating function is @(+)@, and
the initial value, zero; for example, this function takes a pair of
bounds and a list of values (of an index type) and yields a histogram;
that is, a table of the number of occurrences of each value within the
bounds:
\bprog
@
hist :: (Ix a, Integral b) => (a,a) -> [a] -> Array a b
hist bnds is = accumArray (+) 0 bnds [(i, 1) | i <- is, inRange bnds i]
@
\eprog
Suppose we have a collection of measurements on the interval "[a,b)", and
we want to divide the interval into decades and count the number of
measurements within each:
\bprog
@
decades :: (RealFrac a) => a -> a -> [a] -> Array Int Int
decades a b = hist (0,9) . map decade
where decade x = floor ((x - a) * s)
s = 10 / (b - a)
@
\eprog
\subsection{Incremental updates}
In addition to the monolithic array creation functions, Haskell also
has an incremental array update function, written as the infix
operator @//@; the simplest case, an array @a@ with element @i@
updated to @v@, is written @a // [(i, v)]@. The reason for the square
brackets is that the left argument of @(//)@ is an association list,
usually containing a proper subset of the indices of the array:
\bprog
@
(//) :: (Ix a) => Array a b -> [(a,b)] -> Array a b
@
\eprog
As with the @array@ function, the indices in the association list
must be unique for the values to be defined. For example, here
is a function to interchange two rows of a matrix:
\bprog
@
swapRows :: (Ix a, Ix b, Enum b) => a -> a -> Array (a,b) c -> Array (a,b) c
swapRows i i' a = a // ([((i ,j), a!(i',j)) | j <- [jLo..jHi]] ++
[((i',j), a!(i ,j)) | j <- [jLo..jHi]])
where ((iLo,jLo),(iHi,jHi)) = bounds a
@
\eprog
The concatenation here of two separate list comprehensions over the same
list of @j@ indices is, however, a slight inefficiency; it's like
writing two loops where one will do in an imperative language.
Never fear, we can perform the equivalent of a loop fusion optimization
in Haskell:
\bprog
@
swapRows i i' a = a // [assoc | j <- [jLo..jHi],
assoc <- [((i ,j), a!(i',j)),
((i',j), a!(i, j))] ]
where ((iLo,jLo),(iHi,jHi)) = bounds a
@
\eprog
\subsection{An example: Matrix Multiplication}
We complete our introduction to Haskell arrays with the familiar
example of matrix multiplication, taking advantage of overloading
to define a fairly general function. Since only multiplication and
addition on the element type of the matrices is involved, we get
a function that multiplies matrices of any numeric type unless we
try hard not to. Additionally, if we are careful to apply only
@(!)@ and the operations of @Ix@ to indices, we get genericity over
index types, and in fact, the four row and column index types need
not all be the same. For simplicity, however, we require that
the left column indices and right row indices be of the same type, and
moreover, that the bounds be equal:
\bprog
@
matMult :: (Ix a, Ix b, Ix c, Num d) =>
Array (a,b) d -> Array (b,c) d -> Array (a,c) d
matMult x y = array resultBounds
[((i,j), sum [x!(i,k) * y!(k,j) | k <- range (lj,uj)])
| i <- range (li,ui),
j <- range (lj',uj') ]
where ((li,lj),(ui,uj)) = bounds x
((li',lj'),(ui',uj')) = bounds y
resultBounds
| (lj,uj)==(li',ui') = ((li,lj'),(ui,uj'))
| otherwise = error "matMult: incompatible bounds"
@
\eprog
As an aside, we can also define @matMult@ using @accumArray@,
resulting in a presentation that more closely resembles the
usual formulation in an imperative language:
\bprog
@
matMult x y = accumArray (+) 0 resultBounds
[((i,j), x!(i,k) * y!(k,j))
| i <- range (li,ui),
j <- range (lj',uj')
k <- range (lj,uj) ]
where ((li,lj),(ui,uj)) = bounds x
((li',lj'),(ui',uj')) = bounds y
resultBounds
| (lj,uj)==(li',ui') = ((li,lj'),(ui,uj'))
| otherwise = error "matMult: incompatible bounds"
@
\eprog
We can generalize further by making the function higher-order,
simply replacing @sum@ and @(*)@ by functional parameters:
\bprog
@
genMatMult :: (Ix a, Ix b, Ix c) =>
([f] -> g) -> (d -> e -> f) ->
Array (a,b) d -> Array (b,c) e -> Array (a,c) g
genMatMult sum' star x y =
array resultBounds
[((i,j), sum' [x!(i,k) `star` y!(k,j) | k <- range (lj,uj)])
| i <- range (li,ui),
j <- range (lj',uj') ]
where ((li,lj),(ui,uj)) = bounds x
((li',lj'),(ui',uj')) = bounds y
resultBounds
| (lj,uj)==(li',ui') = ((li,lj'),(ui,uj'))
| otherwise = error "matMult: incompatible bounds"
@
\eprog
APL fans will recognize the usefulness of functions like the following:
\bprog
@
genMatMult maximum (-)
genMatMult and (==)
@
\eprog
With the first of these, the arguments are numeric matrices, and the
"(i,j)"-th element of the result is the maximum difference between
corresponding elements of the "i"-th row and "j"-th column of the
inputs. In the second case, the arguments are matrices of any equality
type, and the result is a Boolean matrix in which element "(i,j)"
is @True@ if and only if the "i"-th row of the first argument and
"j"-th column of the second are equal as vectors.
Notice that the element types of @genMatMult@ need not be the same,
but merely appropriate for the function parameter @star@. We could
generalize still further by dropping the requirement that the first
column index and second row index types be the same; clearly, two
matrices could be considered conformable as long as the lengths
of the columns of the first and the rows of the second are equal.
The reader may wish to derive this still more general version.
({\bf Hint:} Use the @index@ operation to determine the lengths.)
%**~footer
|