Skip to content

Ix

The last type class I want to mention here is Ix. We encountered it before when talking about arrays. It is the class of all types that can be used as array indices. This includes integers, characters, Booleans and more esoteric types such as ((Int, Bool), Char).

GHCi
:info Ix
type Ix :: * -> Constraint
class Ord a => Ix a where
  range :: (a, a) -> [a]
  index :: (a, a) -> a -> Int
  GHC.Ix.unsafeIndex :: (a, a) -> a -> Int
  inRange :: (a, a) -> a -> Bool
  rangeSize :: (a, a) -> Int
  GHC.Ix.unsafeRangeSize :: (a, a) -> Int
  {-# MINIMAL range, (index | unsafeIndex), inRange #-}
    -- Defined in ‘GHC.Ix’

Ix is a subclass of Ord, so types in the Ix type class must be orderable. Clearly, this makes sense because the entries in an array are arranged in a linear order, and our mapping from indices to array slots reflects this ordering.

A type that is an instance of Ix needs to support the following operations, all of which are used under the hood of the implementations of the various array functions:

The range function converts a range of array bounds represented as a pair consisting of the minimum array index and the maximum array index into the list of array indices. The indices function for arrays is really just a call to range.

GHCi
>>> range ((1,1),(3,3))
[(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)]

Even though we can use rather fancy types as array indices, Haskell programs still run on real computers, so Haskell arrays are still only chunks of memory and accessing an array element requires to determine its position in this chunk. Under the hood, Haskell arrays are 0-based arrays as in C, Rust, Java, and every other programming language. To translate an index of type a into a 0-based integer index, we use the index function. Given the range of array indices, of type (a, a), and a particular index value a, it converts the index into the corresponding integer index.

GHCi
>>> index ((1,1),(3,3)) (1,1)
0
>>> index ((1,1),(3,3)) (1,3)
2

There's also unsafeIndex. index calls inRange to check that the given index is in the given range. Otherwise, bad things will happen if we try to translate this index into an array position. unsafeIndex skips this check. This is clearly more efficient, but you should never use unsafeIndex. It is used under the hood of low-level array code exactly to make array operations faster.

inRange rng ix checks whether ix is part of the range rng:

GHCi
>>> inRange ((1,1),(3,3)) (2,3)
True
>>> inRange ((1,1),(3,3)) (1,4)
False
>>> inRange ((1,1),(3,3)) (2,0)
False

Finally, rangeSize and its unsafe cousin unsafeRangeSize return the number of indices in a given range. It's the length of the list returned by range, but it is implemented using arithmetic only, without generating this list. When creating an array, the various array functions use rangeSize to determine the amount of memory to be allocated for an array, based on the provided range of array indices.

GHCi
>>> rangeSize ((1,1),(3,3))
9
>>> length $ range ((1,1),(3,3))
9