I'm looking for the pair of functions
previous :: (Enum a, Bounded a) => a -> a
next :: (Enum a, Bounded a) => a -> a
Such that previous
is pred :: Enum a => a -> a
if the resulting value will be constrained by the bounds, and cycle it to the other side otherwise (symmetrical for next
and succ
).
Example
data X = A | B | C deriving (Bounded, Enum)
next A = B
next B = C
next C = A
This would be easy if I were to add the Eq a => a
constraint, as discussed in these similar questions.
But this constraint seems unnecessary, as a Bounded a => a
type to me sounds like it should be able to tell if its within bound or not. The succ
and pred
functions themselves have some kind of control over this, but I would rather not use Exceptions in my otherwise pure code:
Prelude> succ (maxBound :: Int)
*** Exception: Prelude.Enum.succ{Int}: tried to take `succ' of maxBound
How does succ
/pred
test against the bounds without requiring Eq a => a
(or even Bounded a => a
)? Can I replicate this behavior in my own function? Or is there another way to write a function with this behavior and not requiring the Eq
constraint?
The Enum
class defines two functions
toEnum :: Enum a => Int -> a
fromEnum :: Enum a => a -> Int
These functions can be used to compare an Enum
to another, if the range of that Enum
fits into the size of Int
. If you are willing to accept this slight limitation, you can write your functions as follows.
{-# LANGUAGE ScopedTypeVariables #-}
previous :: forall a . (Enum a, Bounded a) => a -> a
previous x | from x > from minBound = pred x
| otherwise = maxBound where from :: a -> Int; from = fromEnum
next :: forall a . (Enum a, Bounded a) => a -> a
next x | from x < from maxBound = succ x
| otherwise = minBound where from :: a -> Int; from = fromEnum
These functions make several assumptions; that given (Enum a, Bounded a)
, every value of a
will be between the minBound
and maxBound
, inclusive, and that {to/from}Enum
preserve this ordering, with respect to the ordering on integers. This is true for simple enumerations:
>import Control.Arrow
>map (next &&& previous) [A,B,C]
[(B,C),(C,A),(A,B)]
What's the problem? If you can add the Eq
class too, it's easy:
module CycleEnum where
data XEnum = A1 | A2 | A3 deriving (Bounded, Enum, Eq, Show)
cyclePrev :: (Eq a, Enum a, Bounded a) => a -> a
cyclePrev x = if x == minBound then maxBound else pred x
cycleNext :: (Eq a, Enum a, Bounded a) => a -> a
cycleNext x = if x == maxBound then minBound else succ x
Without Eq
, it's slightly more complicated...
module CycleEnum where
data XEnum = A1 | A2 | A3 deriving (Bounded, Enum, Show)
isSingle :: [a] -> Bool
isSingle [x] = True
isSingle _ = False
isLastElement :: Enum a => a -> Bool
isLastElement x = isSingle $ enumFrom x
isFirstElement :: (Enum a, Bounded a) => a -> Bool
isFirstElement x = isSingle $ enumFromTo minBound x
cyclePrev :: (Enum a, Bounded a) => a -> a
cyclePrev x = if isFirstElement x then maxBound else pred x
cycleNext :: (Enum a, Bounded a) => a -> a
cycleNext x = if isLastElement x then minBound else succ x
Note that I wrote isSingle
with pattern matching, and not by calling length
; because calculating the length of a list can be very inefficient if you just want to know if the list contains exactly one element.
It looks like, for some of the typeclass instances, it simply defines the functions as you have done with your example:
instance Enum Ordering where
succ LT = EQ
succ EQ = GT
succ GT = error "Prelude.Enum.Ordering.succ: bad argument"
pred GT = EQ
pred EQ = LT
pred LT = error "Prelude.Enum.Ordering.pred: bad argument"
Otherwise, as you say, it seems most likely that you're going to need to allow for Eq
, because fundamentally there must be some concept of comparison (in order to compare the bounds against the current value), if the full space of the type can't be easily enumerated.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With