Skip to content

Commit 5aec998

Browse files
authored
Implement fromSetA for IntMap and Map (#1163)
* Implement `fromSetA`. * Add strictness property tests. * Verify evaluation order of applicative actions in tests. * Use `Fun` instead of lists of pairs for `fromSet` tests. * Add useful instances for `Bot` so it can be used elsewhere. * Add benchmarks for `fromSet` and `fromSetA`. * Add to gitignore. * Add benchmark script. * Bump bounds.
1 parent 6ea6d51 commit 5aec998

File tree

20 files changed

+389
-63
lines changed

20 files changed

+389
-63
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
**/dist-newstyle/*
1111
GNUmakefile
1212
dist-install
13+
dist-mcabal
1314
ghc.mk
1415
.stack-work
1516
.cabal-sandbox/
1617
cabal.sandbox.config
18+
cabal.project.local
1719
/benchmarks/bench-Map
1820
/benchmarks/bench-Set
1921
/benchmarks/bench-IntSet
@@ -22,3 +24,5 @@ cabal.sandbox.config
2224
/benchmarks/SetOperations/bench-*
2325
/result
2426
/TAGS
27+
28+
benchmark_tmp

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ To do so on Windows 10 or higher, follow these steps:
5959
2. Enable git symlinks: `git config --global core.symlinks true`.
6060
3. Clone the repository again once git is properly configured.
6161

62+
### Benchmarking script
63+
64+
To run the all benchmarks of your branch against master, you can run the script
65+
`./bench.sh` while on your feature branch.
66+
67+
This first builds and benchmarks against the master branch, and then builds and
68+
benchmarks on your branch, and compares between them.
69+
70+
You can also fiddle with the script as long as the changes are reflected in your
71+
local master branch and run less than the full suite of benchmarks.
72+
6273
## Sending Pull Requests
6374

6475
When you send a pull request, please:

bench.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#! /bin/bash
2+
3+
# convenience script to run all benchmarks for the master branch and for the
4+
# starting branch, and compare the before and after in the output file
5+
# bench-$CURR_BRANCH_NAME.out, in `benchmark_tmp/`
6+
7+
exitWith () {
8+
echo "$1"
9+
exit $(($2))
10+
}
11+
12+
if [ -n "$(git status --porcelain)" ]; then
13+
echo "there are changes, exiting benchmark script";
14+
exit 1
15+
fi
16+
17+
CURR=`git rev-parse --abbrev-ref HEAD`
18+
19+
if [ "$CURR" == "master" ]
20+
then
21+
exitWith "current branch is master, ending benchmarking" -1
22+
fi
23+
24+
BENCHMARKS=(
25+
intmap-benchmarks
26+
intset-benchmarks
27+
map-benchmarks
28+
tree-benchmarks
29+
sequence-benchmarks
30+
set-benchmarks
31+
graph-benchmarks
32+
set-operations-intmap
33+
set-operations-intset
34+
set-operations-map
35+
set-operations-set
36+
lookupge-intmap
37+
lookupge-map
38+
)
39+
40+
BENCHMARK_TMP="benchmark_tmp"
41+
42+
mkdir -p $BENCHMARK_TMP
43+
44+
git checkout master
45+
46+
cabal build all || exitWith "master build errored" 2
47+
48+
MASTER_BENCH_LOG="$BENCHMARK_TMP/bench-master.log"
49+
echo -n > $MASTER_BENCH_LOG
50+
51+
for BENCHMARK in "${BENCHMARKS[@]}"
52+
do
53+
echo "running $BENCHMARK on master"
54+
(cabal bench $BENCHMARK --benchmark-options="--csv $BENCHMARK.csv" >> $MASTER_BENCH_LOG 2>&1) ||
55+
exitWith "benchmark $BENCHMARK failed to run on master, exiting" 3
56+
done
57+
58+
git checkout $CURR
59+
60+
cabal build all || exitWith "$CURR build errored" 4
61+
62+
CURR_BENCH_LOG="$BENCHMARK_TMP/bench-$CURR.log"
63+
echo -n > $CURR_BENCH_LOG
64+
65+
for BENCHMARK in "${BENCHMARKS[@]}"
66+
do
67+
echo "running $BENCHMARK on $CURR"
68+
(cabal bench $BENCHMARK --benchmark-options="--csv $BENCHMARK-$CURR.csv --baseline $BENCHMARK.csv" >> $CURR_BENCH_LOG 2>&1) ||
69+
exitWith "benchmark $BENCHMARK failed to run on $CURR, exiting" 5
70+
done
71+
72+
mv containers-tests/*.csv $BENCHMARK_TMP/

containers-tests/benchmarks/IntMap.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import qualified Data.IntMap as M
99
import qualified Data.IntMap.Strict as MS
1010
import qualified Data.IntSet as S
1111
import Data.Maybe (fromMaybe)
12+
import Data.Tuple.Solo (Solo (MkSolo), getSolo)
1213
import Data.Word (Word8)
1314
import System.Random (StdGen, mkStdGen, random, randoms)
1415
import Prelude hiding (lookup)
@@ -80,6 +81,11 @@ main = do
8081
whnf (\n -> M.fromAscList (unitValues [1..n])) bound
8182
, bench "minView" $ whnf (maybe 0 (\((k,v), m) -> k+v+M.size m) . M.minViewWithKey)
8283
(M.fromList $ zip [1..10] [1..10])
84+
, bench "fromSet" $ whnf (M.fromSet pred) s_random2
85+
, bench "Lazy.fromSetA outer" $ whnf (M.fromSetA (MkSolo . pred)) s_random2
86+
, bench "Strict.fromSetA outer" $ whnf (MS.fromSetA (MkSolo . pred)) s_random2
87+
, bench "Lazy.fromSetA inner" $ whnf (getSolo . M.fromSetA (MkSolo . pred)) s_random2
88+
, bench "Strict.fromSetA inner" $ whnf (getSolo . MS.fromSetA (MkSolo . pred)) s_random2
8389
, bench "spanAntitone" $ whnf (M.spanAntitone (<key_mid)) m
8490
, bench "split" $ whnf (M.split key_mid) m
8591
, bench "splitLookup" $ whnf (M.splitLookup key_mid) m
@@ -123,7 +129,7 @@ main = do
123129
]
124130

125131
--------------------------------------------------------
126-
!bound = 2^12
132+
!bound = 2^14
127133
keys = [1..bound]
128134
keys' = fmap (+ 1000000) keys
129135
keys'' = fmap (* 2) [1..bound]

containers-tests/benchmarks/Map.hs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import Data.Functor.Identity (Identity(..))
1010
import Data.List (foldl')
1111
import qualified Data.Map as M
1212
import qualified Data.Map.Strict as MS
13+
import qualified Data.Set as Set
1314
import Data.Map (alterF)
1415
import Data.Maybe (fromMaybe)
1516
import Data.Functor ((<$))
1617
import Data.Coerce
18+
import Data.Tuple.Solo (Solo (MkSolo), getSolo)
19+
import System.Random (StdGen, mkStdGen, random, randoms)
1720
import Prelude hiding (lookup)
1821

1922
import Utils.Fold (foldBenchmarks, foldWithKeyBenchmarks)
@@ -23,9 +26,12 @@ main = do
2326
let m = M.fromList elems :: M.Map Int Int
2427
m_even = M.fromList elems_even :: M.Map Int Int
2528
m_odd = M.fromList elems_odd :: M.Map Int Int
29+
s_random = Set.fromList keys_random :: Set.Set Int
2630
evaluate $ rnf [m, m_even, m_odd]
31+
evaluate $ rnf [s_random]
2732
evaluate $ rnf
2833
[elems_distinct_asc, elems_distinct_desc, elems_asc, elems_desc]
34+
evaluate $ rnf [keys_random]
2935
defaultMain
3036
[ bench "lookup absent" $ whnf (lookup evens) m_odd
3137
, bench "lookup present" $ whnf (lookup evens) m_even
@@ -124,6 +130,11 @@ main = do
124130
, bench "fromDistinctAscList:fusion" $ whnf (\n -> M.fromDistinctAscList [(i,i) | i <- [1..n]]) bound
125131
, bench "fromDistinctDescList" $ whnf M.fromDistinctDescList elems_distinct_desc
126132
, bench "fromDistinctDescList:fusion" $ whnf (\n -> M.fromDistinctDescList [(i,i) | i <- [n,n-1..1]]) bound
133+
, bench "fromSet" $ whnf (M.fromSet pred) s_random
134+
, bench "Lazy.fromSetA outer" $ whnf (M.fromSetA (MkSolo . pred)) s_random
135+
, bench "Strict.fromSetA outer" $ whnf (MS.fromSetA (MkSolo . pred)) s_random
136+
, bench "Lazy.fromSetA inner" $ whnf (getSolo . M.fromSetA (MkSolo . pred)) s_random
137+
, bench "Strict.fromSetA inner" $ whnf (getSolo . MS.fromSetA (MkSolo . pred)) s_random
127138
, bench "minView" $ whnf (\m' -> case M.minViewWithKey m' of {Nothing -> 0; Just ((k,v),m'') -> k+v+M.size m''}) (M.fromAscList $ zip [1..10::Int] [100..110::Int])
128139
, bench "eq" $ whnf (\m' -> m' == m') m -- worst case, compares everything
129140
, bench "compare" $ whnf (\m' -> compare m' m') m -- worst case, compares everything
@@ -136,7 +147,7 @@ main = do
136147
, bench "mapKeysWith:desc" $ whnf (M.mapKeysWith (+) (negate . (`div` 2))) m
137148
]
138149
where
139-
bound = 2^12
150+
bound = 2^14
140151
elems = shuffle elems_distinct_asc
141152
elems_even = zip evens evens
142153
elems_odd = zip odds odds
@@ -152,6 +163,7 @@ main = do
152163
values = [1..bound]
153164
sumkv k v1 v2 = k + v1 + v2
154165
consPair k v xs = (k, v) : xs
166+
keys_random = take bound (randoms gen)
155167

156168
add3 :: Int -> Int -> Int -> Int
157169
add3 x y z = x + y + z
@@ -239,3 +251,6 @@ atAltNoRules f xs m = foldl' (\m k -> runIdent (alterF (Ident . f) k m)) m xs
239251
maybeDel :: Int -> Maybe Int
240252
maybeDel n | n `mod` 3 == 0 = Nothing
241253
| otherwise = Just n
254+
255+
gen :: StdGen
256+
gen = mkStdGen 90

containers-tests/containers-tests.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ common test-deps
4747
import: deps
4848
build-depends:
4949
containers-tests
50+
, OneTuple
5051
, QuickCheck >=2.7.1
5152
, tasty
5253
, tasty-hunit
@@ -58,6 +59,7 @@ common benchmark-deps
5859
build-depends:
5960
containers-tests
6061
, deepseq >=1.1.0.0 && <1.6
62+
, OneTuple
6163
, tasty-bench >=0.3.1 && <0.5
6264

6365
-- Flags recommended by tasty-bench

containers-tests/tests/Utils/Strictness.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ instance Arbitrary a => Arbitrary (Bot a) where
2727
, (4, Bot <$> arbitrary)
2828
]
2929

30+
instance CoArbitrary a => CoArbitrary (Bot a) where
31+
coarbitrary (Bot x) = coarbitrary x
32+
33+
instance Function a => Function (Bot a) where
34+
function = functionMap (\(Bot x) -> x) Bot
35+
3036
{--------------------------------------------------------------------
3137
Lazy functions
3238
--------------------------------------------------------------------}

containers-tests/tests/intmap-properties.hs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE TupleSections #-}
23

34
#ifdef STRICT
45
import Data.IntMap.Strict as Data.IntMap
@@ -14,7 +15,9 @@ import Data.IntSet.Internal.IntTreeCommons (Prefix(..), nomatch)
1415
import IntMapValidity (hasPrefix, hasPrefixSimple, valid)
1516

1617
import Control.Applicative (Applicative(..))
18+
import Control.Arrow ((&&&))
1719
import Control.Monad ((<=<))
20+
import Control.Monad.Trans.Writer.Lazy
1821
import qualified Data.Either as Either
1922
import qualified Data.Foldable as Foldable
2023
import Data.Monoid
@@ -23,6 +26,7 @@ import qualified Data.Maybe as Maybe (mapMaybe)
2326
import Data.Ord
2427
import Data.Foldable (foldMap)
2528
import Data.Function
29+
import Data.Functor
2630
import Data.Traversable (Traversable(traverse), foldMapDefault)
2731
import Prelude hiding (lookup, null, map, filter, foldr, foldl, foldl')
2832
import qualified Prelude (map, filter)
@@ -212,6 +216,7 @@ main = defaultMain $ testGroup "intmap-properties"
212216
prop_FoldableTraversableCompat
213217
, testProperty "keysSet" prop_keysSet
214218
, testProperty "fromSet" prop_fromSet
219+
, testProperty "fromSetA eval order" prop_fromSetA_action_order
215220
, testProperty "restrictKeys" prop_restrictKeys
216221
, testProperty "withoutKeys" prop_withoutKeys
217222
, testProperty "traverseWithKey identity" prop_traverseWithKey_identity
@@ -1689,14 +1694,23 @@ prop_FoldableTraversableCompat :: Fun A [B] -> IntMap A -> Property
16891694
prop_FoldableTraversableCompat fun m = foldMap f m === foldMapDefault f m
16901695
where f = apply fun
16911696

1692-
prop_keysSet :: [(Int, Int)] -> Bool
1693-
prop_keysSet xs =
1694-
keysSet (fromList xs) == IntSet.fromList (List.map fst xs)
1695-
1696-
prop_fromSet :: [(Int, Int)] -> Bool
1697-
prop_fromSet ys =
1698-
let xs = List.nubBy ((==) `on` fst) ys
1699-
in fromSet (\k -> fromJust $ List.lookup k xs) (IntSet.fromList $ List.map fst xs) == fromList xs
1697+
prop_keysSet :: [Int] -> Property
1698+
prop_keysSet keys =
1699+
keysSet (fromList (fmap (, ()) keys)) === IntSet.fromList keys
1700+
1701+
prop_fromSet :: [Int] -> Fun Int A -> Property
1702+
prop_fromSet keys funF =
1703+
let f = apply funF
1704+
in fromSet f (IntSet.fromList keys) === fromList (fmap (id &&& f) keys)
1705+
1706+
prop_fromSetA_action_order :: [Int] -> Fun Int A -> Property
1707+
prop_fromSetA_action_order keys funF =
1708+
let iSet = IntSet.fromList keys
1709+
f = apply funF
1710+
action = \k ->
1711+
let v = f k
1712+
in tell [v] $> v
1713+
in execWriter (fromSetA action iSet) === List.map f (IntSet.toList iSet)
17001714

17011715
newtype Identity a = Identity a
17021716
deriving (Eq, Show)

containers-tests/tests/intmap-strictness.hs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ import Data.Bifunctor (bimap)
88
import Data.Coerce (coerce)
99
import Data.Either (partitionEithers)
1010
import qualified Data.Foldable as F
11-
import Data.Functor.Identity (Identity(..))
1211
import Data.Function (on)
12+
import Data.Functor.Compose
13+
import Data.Functor.Identity (Identity(..))
1314
import qualified Data.List as List
1415
import qualified Data.List.NonEmpty as NE
1516
import Data.Maybe (catMaybes, mapMaybe)
1617
import Data.Ord (comparing)
1718
import Test.ChasingBottoms.IsBottom
1819
import Test.Tasty (TestTree, defaultMain, testGroup)
1920
import Test.Tasty.QuickCheck (testProperty)
21+
import Data.Tuple.Solo (Solo (MkSolo), getSolo)
2022
import Test.QuickCheck
2123
import Test.QuickCheck.Poly (A, B, C)
2224
import Test.QuickCheck.Function (apply)
@@ -68,12 +70,35 @@ prop_strictFromSet :: Func Key (Bot A) -> IntSet -> Property
6870
prop_strictFromSet fun set =
6971
isBottom (M.fromSet f set) === any (isBottom . f) (IntSet.toList set)
7072
where
71-
f = coerce (applyFunc fun) :: Key -> A
73+
f = applyFunc fun
74+
75+
prop_strictFromSetA :: Func Key (Bot A) -> IntSet -> Property
76+
prop_strictFromSetA fun set =
77+
isBottom (getSolo (M.fromSetA (MkSolo . f) set)) === any (isBottom . f) (IntSet.toList set)
78+
where
79+
f = applyFunc fun
7280

7381
prop_lazyFromSet :: Func Key (Bot A) -> IntSet -> Property
7482
prop_lazyFromSet fun set = isNotBottomProp (L.fromSet f set)
7583
where
76-
f = coerce (applyFunc fun) :: Key -> A
84+
f = applyFunc fun
85+
86+
prop_lazyFromSetA :: Func Key (Bot A) -> IntSet -> Property
87+
prop_lazyFromSetA fun set = isNotBottomProp (getSolo (L.fromSetA f set))
88+
where
89+
f = MkSolo . applyFunc fun
90+
91+
prop_fromSetA_equiv_strictness :: Func Int (Bot A) -> IntSet -> Property
92+
prop_fromSetA_equiv_strictness fun set =
93+
-- strict fromSetA is the same as lazy and then forcing
94+
bottomOn (M.fromSetA f set) (fmap forceValues (L.fromSetA f set)) .&&.
95+
-- strict fromSetA is the same as lazy fromSetA composed with strictly applied
96+
-- wrapper
97+
bottomOn (M.fromSetA f set) (fmap getSolo . getCompose $ L.fromSetA (Compose . fmap (MkSolo $!) . f) set)
98+
where
99+
forceValues xs = foldr (\ !_ r -> r) () xs `seq` xs
100+
bottomOn = (===) `on` isBottom . getSolo
101+
f = MkSolo . applyFunc fun
77102

78103
prop_strictFromList :: [(Key, Bot A)] -> Property
79104
prop_strictFromList kvs =
@@ -1015,6 +1040,8 @@ tests =
10151040
, testGroup "Construction"
10161041
[ testPropStrictLazy "singleton" prop_strictSingleton prop_lazySingleton
10171042
, testPropStrictLazy "fromSet" prop_strictFromSet prop_lazyFromSet
1043+
, testPropStrictLazy "fromSetA" prop_strictFromSetA prop_lazyFromSetA
1044+
, testProperty "fromSetA equivalences" prop_fromSetA_equiv_strictness
10181045
, testPropStrictLazy "fromList" prop_strictFromList prop_lazyFromList
10191046
, testPropStrictLazy "fromListWith" prop_strictFromListWith prop_lazyFromListWith
10201047
, testPropStrictLazy "fromListWithKey" prop_strictFromListWithKey prop_lazyFromListWithKey

0 commit comments

Comments
 (0)