Proposal: priority queues in containers

Louis Wasserman wasserman.louis at gmail.com
Thu Mar 18 11:43:43 EDT 2010


Oh, god, so much to respond to...heh.

 Submit this package for canonicalization as part of the Haskell Platform. I
> would for one would support its inclusion.


This is an option I seriously hadn't considered.  To be fair, that's because
I've never used the Platform myself, preferring rather to have the most
up-to-date version of GHC at all times, heh.  That said, while I'd be okay
with this option, I'd prefer putting it into containers, because I feel like
a canonical, reliable priority queue implementation is the sort of thing a
self-respecting language ought to have built in.

As does Python. In Python, though, the PQ implementation is not a built-in
> class in the default namespace (as are dict and set).  Rather, it is one of
> the "batteries included" libraries that come with Python. I think that's the
> right place for it in Haskell, too.


I don't know Python, but according to Wikipedia, dict and set are built into
the language.  I don't think it's a fair comparison: set and dict in Python
seem to have a role almost as ubiquitous as [] in Haskell, much more
ubiquitous than e.g. Data.Set or Data.Map.  I'm also not entirely sure that
"batteries included" doesn't describe containers, given all the other
packages that come with GHC.

 >   * There is no distinction between keys and priority values.  A utility
> type

 >     Prio p a with the instance Ord p => Ord (Prio p a) is exported to
> allow

 >     usage of distinct keys and priority values.

I disagree with this one.  It requires an Ord instance that isn't really an
> ordering, and makes a Functor instance impossible.  I would prefer an
> interface separating keys and values like that of Data.Map (which would also
> increase consistency within the package).


I'd be okay with separating out a priority/value version.  However, I'm
still clueless as to what you're talking about with respect to Functor --
what's wrong with the following?
data Prio p a = Prio p a
instance Ord p => Ord (Prio p a) where ...
instance Functor (Prio p) where fmap f (Prio p a) = Prio p (f a)

I can understand if you're uncomfortable with (==) and (\ x y -> compare x y
== EQ) being inequivalent, but neither the H98 Report nor the Prelude make
any such claim, as far as I can tell.

 The Foldable instance breaks the abstraction.  I think it should
> process elements in order.


I think that wanting to iterate over the priority queue in the middle of the
computation, without caring about order, is a perfectly legitimate desire
for a programmer!  Moreover, the only way to make a Foldable instance
process elements in order would be something like
data Ord a => PQueue a = ....
which I think is an awfully dirty approach.  I'm not at all a fan of adding
restrictions like that, not least because it adds lots of awkward overhead.
Would you be okay with not exporting a Foldable instance at all, but still
exporting a toList method which doesn't guarantee any ordering on the return
list?

My advice would be not to stress over whether priority queues go into
> containers. It's not some pristine thing of beauty that deserves treatment
> with velvet gloves.


I'm...not sure how to respond to this claim.  At least part of me wants to
say, I genuinely do think the containers package is a piece of art...and
then another part pipes up, "except for the inconsistencies between the
various minView/maxView versions, and the little differences between IntMap
and Map, and..."  That said, I wouldn't be a fan of scrapping the style
which the containers package has at least tried to create.  I could be
convinced that rewriting the rest of containers would be a good thing to do,
though...and I might just want to do that myself.  Hah.

 How does this implementation compare with using Map/Set as a
> priority queue?


Continuing the discussion of the benchmarks: first, Jim, it appears that I'm
the one who made a n00b benchmarking error.  TT_____TT  That said, as you
found, my implementation is still slightly faster when the benchmark is
corrected.  Some comments:

   - QuickBinom needs to have O(1) findMin for a reasonable comparison.  I
   added that in the benchmark below, and here.
   - I can't think of any more optimizations for the sparse binomial heap --
   I genuinely think it's not going to get better.
   - There is a way to optimize my implementation still further, but it
   makes my code much less readable.  (Specifically, I start the BinomForest at
   Succ Zero, and unpack the first child of every node still in the forest.
    Modifying the whole implementation that way, though, makes it unreadably
   ugly...and I think QuickBinom is possibly already at that point.  I started
   implementing it, and realized just how ugly it was, and I stopped, but I can
   finish it if I have to.)

Sorting 500,000 elements, compiled with -O2, run with +RTS -H128m -K64m,
with another few implementations thrown in for good measure:
Times (ms)
               min      mean      +/-sd    median      max
Pairing:    1440.090  1482.893    31.501  1482.093  1532.095
Binomial:   1356.084  1389.687    26.881  1388.087  1436.090
SBinomial:  1376.086  1422.489    48.453  1400.088  1520.095
Data.Set:   1712.107  1800.912    74.880  1782.111  1976.123
Skew:       1584.099  1644.503    85.702  1602.101  1848.116

Some other benchmarks<http://hackage.haskell.org/trac/ghc/attachment/ticket/3909/plot_2.png>were
done by Milan Straka earlier, when we hadn't decided what heap
implementation to use at all.  His comments:

> I think the pairing heaps are out of the question now. I would choose
> between Binomial and Leftist. The Binomial have O(1) amortized inserts, but
> beware, that this does not work in persistent setting -- the insert is O(log
> N) when the heaps are used persistently (there are Skew Binomial Heaps with
> O(1) insert in the persistent setting too).

  I vote for current (v5) Binomial implementation, even if the O(1)
> amortized inserts works only non-persistently (ie. under heavy persistent
> usage, leftist heaps might be a _little_ better).


Conclusions: There aren't any differences in asymptotics, and as it stands,
the existing implementation is just as fast.  It's also a) done, and b) full
of Haskellish type goodness.

After about five hours' work (!!!) I *finally* managed to install Criterion
yesterday, so I'll send out those tests ASAP.

Louis Wasserman
wasserman.louis at gmail.com
http://profiles.google.com/wasserman.louis
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://www.haskell.org/pipermail/glasgow-haskell-users/attachments/20100318/84893357/attachment.html


More information about the Glasgow-haskell-users mailing list