Skip to content

Commit c22de83

Browse files
authored
Make sure default impls for BoundedEnum are TCO'd (#37)
The implementations for `defaultCardinality`, `defaultToEnum`, and `defaultFromEnum` don't trigger tail-call optimization, which means that they are quite a bit slower than they could be (and in some cases will produce a stack overflow when they needn't). This commit will also have the fortunate effect that this library won't break if the compiler stops inlining function composition in the (arguably broken) way that it does currently; see purescript/purescript#3439 (comment)
1 parent b244240 commit c22de83

File tree

2 files changed

+64
-9
lines changed

2 files changed

+64
-9
lines changed

src/Data/Enum.purs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,11 @@ defaultPred toEnum' fromEnum' a = toEnum' (fromEnum' a - 1)
268268
-- |
269269
-- | Runs in `O(n)` where `n` is `fromEnum top`
270270
defaultCardinality :: forall a. Bounded a => Enum a => Cardinality a
271-
defaultCardinality = Cardinality $ defaultCardinality' 1 (bottom :: a)
272-
where
273-
defaultCardinality' i = maybe i (defaultCardinality' (i + 1)) <<< succ
271+
defaultCardinality = Cardinality $ go 1 (bottom :: a) where
272+
go i x =
273+
case succ x of
274+
Just x' -> go (i + 1) x'
275+
Nothing -> i
274276

275277
-- | Provides a default implementation for `toEnum`.
276278
-- |
@@ -279,10 +281,18 @@ defaultCardinality = Cardinality $ defaultCardinality' 1 (bottom :: a)
279281
-- |
280282
-- | Runs in `O(n)` where `n` is `fromEnum a`.
281283
defaultToEnum :: forall a. Bounded a => Enum a => Int -> Maybe a
282-
defaultToEnum n
283-
| n < 0 = Nothing
284-
| n == 0 = Just bottom
285-
| otherwise = defaultToEnum (n - 1) >>= succ
284+
defaultToEnum i' =
285+
if i' < 0
286+
then Nothing
287+
else go i' bottom
288+
where
289+
go i x =
290+
if i == 0
291+
then Just x
292+
-- We avoid using >>= here because it foils tail-call optimization
293+
else case succ x of
294+
Just x' -> go (i - 1) x'
295+
Nothing -> Nothing
286296

287297
-- | Provides a default implementation for `fromEnum`.
288298
-- |
@@ -291,7 +301,11 @@ defaultToEnum n
291301
-- |
292302
-- | Runs in `O(n)` where `n` is `fromEnum a`.
293303
defaultFromEnum :: forall a. Enum a => a -> Int
294-
defaultFromEnum = maybe 0 (\prd -> defaultFromEnum prd + 1) <<< pred
304+
defaultFromEnum = go 0 where
305+
go i x =
306+
case pred x of
307+
Just x' -> go (i + 1) x'
308+
Nothing -> i
295309

296310
diag :: forall a. a -> Tuple a a
297311
diag a = Tuple a a

test/Test/Data/Enum.purs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ module Test.Data.Enum (testEnum) where
22

33
import Prelude
44

5-
import Data.Enum (class BoundedEnum, class Enum, defaultCardinality, defaultFromEnum, defaultToEnum, downFrom, downFromIncluding, enumFromThenTo, enumFromTo, upFrom, upFromIncluding)
5+
import Data.Enum (class BoundedEnum, class Enum, Cardinality, defaultCardinality, defaultFromEnum, defaultToEnum, downFrom, downFromIncluding, enumFromThenTo, enumFromTo, upFrom, upFromIncluding)
66
import Data.Maybe (Maybe(..))
7+
import Data.Newtype (unwrap)
78
import Data.NonEmpty ((:|))
89
import Effect (Effect)
910
import Effect.Console (log)
@@ -44,6 +45,28 @@ instance boundedEnumT :: BoundedEnum T where
4445
toEnum = defaultToEnum
4546
fromEnum = defaultFromEnum
4647

48+
-- | A newtype over Int which is supposed to represent Ints bounded between 0
49+
-- | and 100,000. Why 100,000? It seems to be large enough that we are very
50+
-- | likely to see stack overflow errors if we've managed to break TCO.
51+
newtype Upto100k = Upto100k Int
52+
53+
derive newtype instance eqUpto100k :: Eq Upto100k
54+
derive newtype instance ordUpto100k :: Ord Upto100k
55+
derive newtype instance showUpto100k :: Show Upto100k
56+
57+
instance boundedUpto100k :: Bounded Upto100k where
58+
top = Upto100k 100000
59+
bottom = Upto100k 0
60+
61+
instance enumUpto100k :: Enum Upto100k where
62+
succ (Upto100k x) = if (x+1) > 100000 then Nothing else Just (Upto100k (x+1))
63+
pred (Upto100k x) = if (x-1) < 0 then Nothing else Just (Upto100k (x-1))
64+
65+
instance boundedEnumUpto100k :: BoundedEnum Upto100k where
66+
cardinality = defaultCardinality
67+
toEnum = defaultToEnum
68+
fromEnum = defaultFromEnum
69+
4770
testEnum :: Effect Unit
4871
testEnum = do
4972
log "enumFromTo"
@@ -153,3 +176,21 @@ testEnum = do
153176
{ actual: downFromIncluding A
154177
, expected: [A]
155178
}
179+
180+
log "defaultCardinality is stack safe"
181+
assertEqual
182+
{ actual: unwrap (defaultCardinality :: Cardinality Upto100k)
183+
, expected: 100001
184+
}
185+
186+
log "defaultToEnum is stack safe"
187+
assertEqual
188+
{ actual: defaultToEnum 100000
189+
, expected: Just (Upto100k 100000)
190+
}
191+
192+
log "defaultFromEnum is stack safe"
193+
assertEqual
194+
{ actual: defaultFromEnum (Upto100k 100000)
195+
, expected: 100000
196+
}

0 commit comments

Comments
 (0)