Dijkstra's Algorithm in Haskell
Nice exercise!


  But the big decision I needed was how to store the 3 elements of state
  
  
  The results
  The Dijkstra scores of the points not yet turned into results; and
  The set of points already found (i.e. the keys of the results)
  


An important feature of the algorithm is that the scores are kept in a min-priority-queue. Keeping the scores in a vector requires O(n) cost at every step. Also note that having using a priority queue API would simplify the mainLoop, as the operations on a vector are somewhat low-level wrt the task (remove the minimal value from the queue, if it exists).

The explored nodes form a set, to it'd be indeed more appropriate to use IntSet or Set. Whenever you use elem with [], it's a sign to think of a better data structure (unless you know the list length is bounded by some small constant).

Regarding the results, we know we add each node only once, so [] seems to be OK. Adding is then O(1) and whatever manipulation we want to do at the end can be deferred until then.

I don't think there is anything wrong with your approach to keeping the state. While you could use the State monad (or perhaps RWS where the reader part would be the graph, the writer part the results and the state the scores), it'd force (as any monad) more imperative, sequential style. And it'd cloud which part of the code needs what information. And having the value that's being modified as the last argument (like ... -&gt; Results -&gt; Results) allows to use point-free style where appropriate.

You might want to check if your mainLoop is tail-recursive.

Perhaps one way how to make mainLoop have less imperative feel would be to check for the termination condition first (which isn't such a bad idea, if the input is an empty graph or so). Something like:

mainLoop gr = loop
  where
    loop exs ds res | ifQueueEmpty ds = res
                    | otherwise =
      let ...
      in loop exs' newDs res'


There are multiple reasons for separating the loop in an inner function. One (that doesn't really apply here) is that by having the top-level function non-recursive, it allows it to be inlined. The other is that this clearly expresses what changes during the loop and what not.



Some more remarks:

I like that you included types for all functions, including helpers, that's often very convenient. And that you gave descriptive names to the types.

From the types it's not that obvious how are graphs represented. If an edge is

type Edge = (NodeName, Dist)


what is the other node it connects? My guess is that the other node is determined by the index of the [Edge] list in the vector. In any case, this should be documented in comments for the types.

Also, a question comes to my mind, why the graph is a Vector, but the list of edges is a []?

Regarding comments, I'd suggest you to use the Haddock syntax, as it's then very easy to generate documentation from the code.

Also it's not clear what Nothing means in the vector. It could mean that an element hasn't been explored yet (infinite distance), which would work well with manipulating distances, but it seems to be used for elements that have been removed from the queue. I'd rather use a queue that allows proper removal of elements, and use Dist that deals with infinity (see below)

For representing distances it'd be useful to have a separate data type, something like

newtype Dist = Dist (Maybe Int)


with the appropriate Ord instance and supply the minimum monoid instance where mempty is the infinity and mappend takes the minimum of its arguments. For example process could be greatly simplified by this.

The comments are sometimes somewhat misleading, for example in Nothing is interpreted as 0 otherwise it's not clear what "otherwise" means.