How can I determine if one Enum value is the successor of another?
Asked Answered
I

1

7

I'm trying to write a function that tells me whether one Enum is the successor of another. Here was my first attempt:

isSuccessorOf x y = x == succ y

Looks reasonable. Let's try it:

λ> isSuccessorOf 3 2
True
λ> isSuccessorOf 1 5
False
λ> isSuccessorOf 3 (maxBound :: Int)
*** Exception: Prelude.Enum.succ{Int}: tried to take `succ' of maxBound

Whoops. That should have been False. Let's make sure we don't try to do succ maxBound:

isSuccessorOf x y = y /= maxBound && x == succ y

Let's try it again:

λ> isSuccessorOf 3 (maxBound :: Int)
False
λ> isSuccessorOf 3 (2 :: Integer)
<interactive>:2:1: error:
    • No instance for (Bounded Integer)
        arising from a use of ‘isSuccessorOf’
    • In the expression: isSuccessorOf 3 (2 :: Integer)
      In an equation for ‘it’: it = isSuccessorOf 3 (2 :: Integer)

Hmm, now it only works on bounded types. I'd like to avoid needing a separate function for unbounded and bounded Enums, especially if there's nothing at compile-time to keep you from using the unbounded function on a bounded type. Let's use an Ord constraint instead:

isSuccessorOf x y = x > y && x == succ y

And let's try it:

λ> isSuccessorOf 3 (maxBound :: Int)
False
λ> isSuccessorOf 3 (2 :: Integer)
True

But now I'm making an unwarranted assumption. Let's try one more thing (note: this depends on Down having an Enum instance, which it only has in GHC 8.10):

λ> import Data.Ord (Down(..))
λ> let delisleFreezing = Down 150
λ> isSuccessorOf (succ delisleFreezing) delisleFreezing
False

Well that's less than ideal.

So is there any way to do this seemingly-simple task, without one of these three flaws?

  • Fails to compile for types that aren't Bounded
  • Bottoms for types that are Bounded
  • Gives the wrong answer for types where succ x > x doesn't hold
Ineducation answered 8/5, 2020 at 21:57 Comment(5)
You can perhaps use fromEnum to map it on Ints, but this is in fact also one of the "weak spots" of the Enum imho: that not all values per se fit in the Int range :(Tripp
Perhaps using enumFrom is better, since we can then just do pattern matching on the list.Tripp
What does GHCI say when you do like succ $ Data.Ord.Down 150?Revolver
@Revolver Down 151Ineducation
@WillemVanOnsem That looks interesting indeed: succMaybe x = case [x..] of (_:z:_) -> Just z ; _ -> Nothing. You should write an answer.Distance
T
6

Perhaps a more safe way to check this is making use of enumFromTo, and check if the second item of the list is the successor we are looking for. We can, like you say, simply pattern match on a list with two elements, we do not need to check if that second element is indeed y:

isSuccessorOf :: Enum a => a -> a -> Bool
isSuccessorOf y x
    | [_,_] <- [x .. y] = True
    | otherwise = False

or we can, like @chi says use this to look if there is a successor:

succMaybe :: Enum a => a -> Maybe a
succMaybe x = case [x ..] of
    (_:z:_) -> Just z
    _ -> Nothing
Tripp answered 8/5, 2020 at 22:10 Comment(5)
Wait, we can lose the Eq constraint too! isSuccessorOf y x = case [x..y] of [_,_] -> True; _ -> FalseIneducation
@JosephSible-ReinstateMonica Who can resist such a great golfing opportunity? (0 <$ [x..y]) == [0,0]... okay, so length [x..y] == 2 is even shorter, but it has worse asymptotic performance. =PFrazee
@JosephSible-ReinstateMonica Nice, but note that in such case you'll have isSuccessor 1.7 1.0, since the float/double instance is a bit bogus.Distance
@Distance Yeah, that's a pretty awful instance in general. IMO, their succ and pred should do what nextafter does, but I think it's a bit too late to change that now.Ineducation
@Distance It also doesn't bother me too much since using == on floating-point types is almost always the wrong thing to do anyway.Ineducation

© 2022 - 2024 — McMap. All rights reserved.