Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cycling a value (Enum a, Bounded a) => a

Tags:

enums

haskell

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?

like image 775
Mephy Avatar asked Oct 07 '15 20:10

Mephy


3 Answers

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)]
like image 194
user2407038 Avatar answered Nov 09 '22 05:11

user2407038


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.

like image 36
Michael Avatar answered Nov 09 '22 07:11

Michael


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.

like image 35
asthasr Avatar answered Nov 09 '22 06:11

asthasr