It is indeed doing a counting sort. Here's a slightly rewritten version that I find easier to understand:
import Data.Array
countingSort :: (Ix n) => [n] -> n -> n -> [n]
countingSort l lo hi = concat [replicate times n | (n, times) <- counts]
where counts = assocs (accumArray (+) 0 (lo, hi) [(i, 1) | i <- l])
Let's break it down step by step. We'll use the list [5, 3, 1, 2, 3, 4, 5]
.
*Main> [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]
[(5,1),(3,1),(1,1),(2,1),(3,1),(4,1),(5,1)]
We're just taking every element of the list and turning it into a tuple with 1. This is the basis for our counts. Now we need a way to sum up those counts per element. This is where accumArray
comes into play.
*Main> accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]
array (1,5) [(1,1),(2,1),(3,2),(4,1),(5,2)]
The first parameter to accumArray
is the operation to apply during accumulation (just simple addition for us). The second parameter is the starting value, and the third parameter is the bounds. So we end up with an array mapping numbers to their counts in the input.
Next we use assocs
to get key/value tuples from the map:
*Main> assocs $ accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]
[(1,1),(2,1),(3,2),(4,1),(5,2)]
And then replicate
to repeat each number based on its count:
*Main> [replicate times n | (n, times) <- assocs $ accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]]
[[1],[2],[3,3],[4],[5,5]]
Finally, we use concat to turn this list of lists into a single list:
*Main> concat [replicate times n | (n, times) <- assocs $ accumArray (+) 0 (1, 5) [(i, 1) | i <- [5, 3, 1, 2, 3, 4, 5]]]
[1,2,3,3,4,5,5]
In the actual function I wrote above, I used where
to break up this one-liner.
I find list comprehension to be easier to deal with than map
and concatMap
, so those are the main changes I made in my version of the function.
(uncurry $ flip replicate)
is a nice trick... flip replicate
gives you a version of replicate
that takes its arguments in the opposite order. uncurry
takes that curried function and turns it into a function that takes a tuple as an argument instead. Together, those produce the same result as my list comprehension, which destructured the tuple and then passed its parameters in reverse order. I'm not familiar enough with Haskell to know if this is a common idiom, but for me, the list comprehension was easier to follow.