circular-enum-0.2.0.0: src/Data/Enum/Circular.hs
{-|
Module : Data.Enum.Circular
Description : Circular successor & predecessor for bounded enum types
Copyright : (c) 2023-2026 Mirko Westermeier
License : MIT
Sometimes, bounded enum types should be circular. Consider this enum
type of directions:
@
data Direction = North
| East
| South
| West
deriving (Eq, Enum, Bounded)
@
The 'Enum' instance allows for @succ North@ to be @East@ and @succ East@
to be @South@. But in this case, one would like to have some kind of
@succ@ with @succ West = North@ again. With 'Eq' and 'Bounded' instances,
the functions defined in this module act like circular versions of 'succ'
and 'pred'.
Note: this module is designed for small, finite enum types created with
@deriving Enum@. It is not suitable for types where
@fromEnum maxBound + 1@ overflows 'Int', such as 'Int' or 'Word' itself.
-}
module Data.Enum.Circular (csucc, cpred, Circular(..)) where
-- | Circular version of 'succ'
csucc :: (Eq a, Enum a, Bounded a) => a -> a
csucc = unCircular . succ . Circular
-- | Circular version of 'pred'
cpred :: (Eq a, Enum a, Bounded a) => a -> a
cpred = unCircular . pred . Circular
-- | Type Alias you can use to express your intent and avoid 'Enum'
-- functions from biting you.
--
-- Beware: unlike regular 'Enum' types, backwards enumeration via
-- @[West, South ..]@ leads to singleton results. Since the enum is
-- circular, a "backwards" step is equivalent to a large forwards step,
-- but the enumeration ends at the "to" element earlier than expected.
-- Use 'pred' or 'cpred' explicitly with 'iterate' to go backwards.
--
-- Also assumes that the underlying 'Enum' instance is well-behaved,
-- i.e. @fromEnum minBound == 0@ and @fromEnum maxBound + 1@ does not
-- overflow 'Int'. Both hold for all types using @deriving Enum@ with a
-- small number of constructors, but /not/ for 'Int', 'Word', or similar.
newtype Circular a = Circular {unCircular :: a}
deriving (Show, Eq)
instance (Enum a, Bounded a) => Enum (Circular a) where
toEnum :: Int -> Circular a
toEnum = Circular . toEnum . (`mod` len)
where len = fromEnum (maxBound :: a) + 1
fromEnum :: Circular a -> Int
fromEnum = fromEnum . unCircular
enumFromTo :: Circular a -> Circular a -> [Circular a]
enumFromTo start end = map toEnum $ enumFromTo i (i + dist)
where i = fromEnum start
j = fromEnum end
dist = (j - i) `mod` len
len = fromEnum (maxBound :: a) + 1
enumFromThenTo :: Circular a -> Circular a -> Circular a -> [Circular a]
enumFromThenTo from next to
| step == 0 = repeat from
| otherwise = map toEnum $ enumFromThenTo i (i + step) (i + dist)
where i = fromEnum from
j = fromEnum next
k = fromEnum to
step = (j - i) `mod` len
dist = (k - i) `mod` len
len = fromEnum (maxBound :: a) + 1