Skip to content

Commit ac9f4f3

Browse files
committed
Unzip Map and IntMap more efficiently
The previous `Map` (same for `IntMap` throughout) instance would first map eagerly over tha `Map`, producing an entire `Map` full of thunks to apply the `unzipWith` function. Then it would build two more entire `Map`s full of thunks to select components of each pair. Depending on inlining and such, the resulting maps may or may not have contained selector thunks; if not, they could leak memory. Fix that. NOTE: This PR is an alternative to haskellari#163. This one preserves the precise laziness properties of the previous implementation.
1 parent 12d1472 commit ac9f4f3

File tree

2 files changed

+66
-6
lines changed

2 files changed

+66
-6
lines changed

semialign/semialign.cabal

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ library
7979
, tagged >=0.8.6 && <0.9
8080
, unordered-containers >=0.2.8.0 && <0.3
8181
, vector >=0.12.0.2 && <0.13
82-
83-
-- base shims
84-
if !impl(ghc >=8.2)
85-
build-depends: bifunctors >=5.5.4 && <5.6
82+
, bifunctors >=5.5.4 && <5.6
8683

8784
if !impl(ghc >=8.0)
8885
build-depends:

semialign/src/Data/Semialign/Internal.hs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE DeriveFunctor #-}
23
{-# LANGUAGE FlexibleInstances #-}
34
{-# LANGUAGE FunctionalDependencies #-}
45
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
@@ -19,6 +20,7 @@ import qualified Prelude as Prelude
1920

2021
import Control.Applicative (ZipList (..), pure, (<$>))
2122
import Data.Bifunctor (Bifunctor (..))
23+
import Data.Biapplicative (Biapplicative (..), traverseBia)
2224
import Data.Functor.Compose (Compose (..))
2325
import Data.Functor.Identity (Identity (..))
2426
import Data.Functor.Product (Product (..))
@@ -577,7 +579,47 @@ instance (Ord k) => Align (Map k) where
577579
instance Ord k => Unalign (Map k) where
578580
unalign xs = (Map.mapMaybe justHere xs, Map.mapMaybe justThere xs)
579581

580-
instance Ord k => Unzip (Map k) where unzip = unzipDefault
582+
-- A copy of (,) with a stricter bimap.
583+
newtype SBPair a b = SBPair { unSBPair :: (a, b) }
584+
585+
instance Bifunctor SBPair where
586+
bimap f g (SBPair (a, b)) = SBPair (f a, g b)
587+
588+
instance Biapplicative SBPair where
589+
bipure a b = SBPair (a, b)
590+
biliftA2 f g (SBPair (a, b)) (SBPair (c, d)) =
591+
SBPair (f a c, g b d)
592+
593+
instance Ord k => Unzip (Map k) where
594+
-- Map has a strict spine, so we have to build a whole one at
595+
-- once. The default instance would first build an entire
596+
-- Map filled with thunks, each of which will produce a pair,
597+
-- and then build two maps, each filled with thunks to extract
598+
-- a value from the pair. We instead build both maps at once,
599+
-- each of which will be filled with selector thunks, along
600+
-- with thunks (not in any Map) holding the applications of
601+
-- `f`.
602+
unzipWith f xs = (l, r)
603+
where
604+
~(l, r) = unSBPair . traverseBia (SBPair . blah) $ xs
605+
blah c = let
606+
{-# NOINLINE fc #-} -- make sure the result of f c is shared,
607+
-- and that nothing weird happens to
608+
-- keep us from getting selector thunks.
609+
{-# NOINLINE a #-} -- make sure we get selector thunks
610+
{-# NOINLINE b #-}
611+
fc = f c
612+
~(a, b) = fc
613+
in (a, b)
614+
615+
unzip xs = (l, r)
616+
where
617+
~(l, r) = unSBPair . traverseBia (SBPair . blah) $ xs
618+
blah ab = let
619+
{-# NOINLINE a #-} -- make sure we get selector thunks
620+
{-# NOINLINE b #-}
621+
~(a, b) = ab
622+
in (a, b)
581623

582624
instance Ord k => Zip (Map k) where
583625
zipWith = Map.intersectionWith
@@ -601,7 +643,28 @@ instance Align IntMap where
601643
instance Unalign IntMap where
602644
unalign xs = (IntMap.mapMaybe justHere xs, IntMap.mapMaybe justThere xs)
603645

604-
instance Unzip IntMap where unzip = unzipDefault
646+
instance Unzip IntMap where
647+
-- See notes at the Map instance
648+
unzipWith f xs = (l, r)
649+
where
650+
~(l, r) = unSBPair . traverseBia (SBPair . blah) $ xs
651+
blah c = let
652+
{-# NOINLINE fc #-} -- make sure the result of f c is shared,
653+
-- and that nothing weird happens to
654+
-- keep us from getting selector thunks.
655+
{-# NOINLINE a #-} -- make sure we get selector thunks
656+
{-# NOINLINE b #-}
657+
fc = f c
658+
~(a, b) = fc
659+
in (a, b)
660+
unzip xs = (l, r)
661+
where
662+
~(l, r) = unSBPair . traverseBia (SBPair . blah) $ xs
663+
blah ab = let
664+
{-# NOINLINE a #-} -- make sure we get selector thunks
665+
{-# NOINLINE b #-}
666+
~(a, b) = ab
667+
in (a, b)
605668

606669
instance Zip IntMap where
607670
zipWith = IntMap.intersectionWith

0 commit comments

Comments
 (0)