Skip to content

Commit 66907f2

Browse files
authored
Stack safe default folds (#149)
* adjust the foldrDefault and foldlDefault functions to be stack safe * performance tuning of folding the FreeMonoidTree * add stack safety tests, and some benchmarks * update the CHANGELOG
1 parent b3926f8 commit 66907f2

File tree

4 files changed

+88
-6
lines changed

4 files changed

+88
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ New features:
1111
Bugfixes:
1212

1313
Other improvements:
14+
- Make `foldrDefault` and `foldlDefault` stack safe (#148)
1415

1516
## [v6.0.0](https://github.com/purescript/purescript-foldable-traversable/releases/tag/v6.0.0) - 2022-04-27
1617

bower.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"purescript-assert": "^6.0.0",
3434
"purescript-console": "^6.0.0",
3535
"purescript-integers": "^6.0.0",
36-
"purescript-unsafe-coerce": "^6.0.0"
36+
"purescript-unsafe-coerce": "^6.0.0",
37+
"purescript-minibench": "^4.0.0"
3738
}
3839
}

src/Data/Foldable.purs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,54 @@ class Foldable f where
7575
foldl :: forall a b. (b -> a -> b) -> b -> f a -> b
7676
foldMap :: forall a m. Monoid m => (a -> m) -> f a -> m
7777

78+
79+
-- | This internal type is used just to implement a stack-safe and performant foldrDefault and foldlDefault.
80+
-- | It has O(1) append (because foldrDefault and foldlDefault are implemented in terms of foldMap), and
81+
-- | an amortized O(1) uncons/unsnoc. It behaves similarly to a CatList
82+
data FreeMonoidTree a = Empty | Node a | Append (FreeMonoidTree a) (FreeMonoidTree a)
83+
84+
instance Foldable FreeMonoidTree where
85+
-- these folding implementations could be written more plainly, but are optimized to minimize conditionals.
86+
foldl fn = (\a b -> go a b Empty)
87+
where
88+
go acc lhs rhs =
89+
case lhs of
90+
Node a -> go (fn acc a) rhs Empty
91+
Append xs ys ->
92+
case ys of
93+
Empty -> go acc xs rhs
94+
_ ->
95+
case rhs of
96+
Empty -> go acc xs ys
97+
_ -> go acc xs (Append ys rhs)
98+
Empty ->
99+
case rhs of
100+
Empty -> acc
101+
_ -> go acc rhs Empty
102+
103+
foldr fn = (\a b -> go a Empty b)
104+
where
105+
go acc lhs rhs =
106+
case rhs of
107+
Node a -> go (fn a acc) Empty lhs
108+
Append xs ys ->
109+
case xs of
110+
Empty -> go acc lhs ys
111+
_ ->
112+
case lhs of
113+
Empty -> go acc xs ys
114+
_ -> go acc (Append lhs xs) ys
115+
Empty ->
116+
case lhs of
117+
Empty -> acc
118+
_ -> go acc Empty lhs
119+
120+
121+
foldMap = foldMapDefaultR
122+
123+
instance Semigroup (FreeMonoidTree a) where append = Append
124+
instance Monoid (FreeMonoidTree a) where mempty = Empty
125+
78126
-- | A default implementation of `foldr` using `foldMap`.
79127
-- |
80128
-- | Note: when defining a `Foldable` instance, this function is unsafe to use
@@ -86,7 +134,7 @@ foldrDefault
86134
-> b
87135
-> f a
88136
-> b
89-
foldrDefault c u xs = unwrap (foldMap (Endo <<< c) xs) u
137+
foldrDefault c u xs = foldr c u $ foldMap Node xs
90138

91139
-- | A default implementation of `foldl` using `foldMap`.
92140
-- |
@@ -99,7 +147,7 @@ foldlDefault
99147
-> b
100148
-> f a
101149
-> b
102-
foldlDefault c u xs = unwrap (unwrap (foldMap (Dual <<< Endo <<< flip c) xs)) u
150+
foldlDefault c u xs = foldl c u $ foldMap Node xs
103151

104152
-- | A default implementation of `foldMap` using `foldr`.
105153
-- |

test/Main.purs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Data.Traversable (class Traversable, sequenceDefault, traverse, sequence,
2020
import Data.TraversableWithIndex (class TraversableWithIndex, traverseWithIndex)
2121
import Effect (Effect, foreachE)
2222
import Effect.Console (log)
23+
import Performance.Minibench (benchWith)
2324
import Test.Assert (assert, assert')
2425
import Unsafe.Coerce (unsafeCoerce)
2526

@@ -58,26 +59,38 @@ main = do
5859
assert $ foldMapDefaultL (\x -> [x]) [1, 2] == [1, 2]
5960

6061
log "Test foldableArray instance is stack safe"
61-
testFoldableArrayWith 20000
62+
testFoldableArrayWith 20_000
6263

6364
log "Test foldMapDefaultL"
6465
testFoldableFoldMapDefaultL 20
6566

67+
log "Test foldMapDefaultL is stack safe"
68+
testFoldableFoldMapDefaultL 20_000
69+
6670
log "Test foldMapDefaultR"
6771
testFoldableFoldMapDefaultR 20
6872

73+
log "Test foldMapDefaultR is stack safe"
74+
testFoldableFoldMapDefaultR 20_000
75+
6976
log "Test foldlDefault"
7077
testFoldableFoldlDefault 20
7178

79+
log "Test foldlDefault is stack safe"
80+
testFoldableFoldlDefault 20_000
81+
7282
log "Test foldrDefault"
7383
testFoldableFoldrDefault 20
7484

85+
log "Test foldrDefault is stack safe"
86+
testFoldableFoldrDefault 20_000
87+
7588
foreachE [1,2,3,4,5,10,20] \i -> do
7689
log $ "Test traversableArray instance with an array of size: " <> show i
7790
testTraversableArrayWith i
7891

7992
log "Test traversableArray instance is stack safe"
80-
testTraversableArrayWith 20000
93+
testTraversableArrayWith 20_000
8194

8295
log "Test traverseDefault"
8396
testTraverseDefault 20
@@ -89,7 +102,7 @@ main = do
89102
testFoldableWithIndexArrayWith 20
90103

91104
log "Test foldableWithIndexArray instance is stack safe"
92-
testFoldableWithIndexArrayWith 20000
105+
testFoldableWithIndexArrayWith 20_000
93106

94107
log "Test FoldableWithIndex laws for array instance"
95108
testFoldableWithIndexLawsOn
@@ -561,3 +574,22 @@ instance bitraversableBTD :: Bitraversable BitraverseDefault where
561574
instance bitraversableBSD :: Bitraversable BisequenceDefault where
562575
bitraverse f g (BSD m) = map BSD (bitraverse f g m)
563576
bisequence m = bisequenceDefault m
577+
578+
579+
benchmarkDefaultFolds :: Effect Unit
580+
benchmarkDefaultFolds = do
581+
let
582+
sm = arrayFrom1UpTo 1_000
583+
m = arrayFrom1UpTo 10_000
584+
lg = arrayFrom1UpTo 100_000
585+
xl = arrayFrom1UpTo 1_000_000
586+
587+
log "\nbenching 1,000"
588+
benchWith 1000 $ \_ -> foldrDefault (+) 0 sm
589+
log "\nbenching 10,000"
590+
benchWith 1000 $ \_ -> foldrDefault (+) 0 m
591+
log "\nbenching 100,000"
592+
benchWith 100 $ \_ -> foldrDefault (+) 0 lg
593+
log "\nbenching 1,000,000"
594+
benchWith 50 $ \_ -> foldrDefault (+) 0 xl
595+

0 commit comments

Comments
 (0)