It is actually just a curiosity, but here's how I came up with it.
tl;dr
I could have wondered how to write a sensible Enum instance for a type larger than Int, say (Int, Int).
A bit longer
But actually I was wondering: why does Enum's minimal definition assumes that the possible values of Int are enough to enumerate the type of which I want to write the Enum class?
At fromEnum I do read that
Convert to an
Int. It is implementation-dependent whatfromEnumreturns when applied to a value that is too large to fit in anInt.
which I interpret as saying that I can indeed define an Enum class for a type that doesn't fit Int. I just have to accept that I'll have to return nonsense form fromEnum for some values of my type.
But how could succ and pred possibly work with such a broken type, given their implementations use toEnum and fromEnum?
succ = toEnum . (+ 1) . fromEnum
pred = toEnum . (subtract 1) . fromEnum
Or maybe I should define manually succ and pred so that they don't make use toEnum/fromEnum?
But if that's the case, then way isn't succ&pred a minimal definition?
Yes, I have read the comment from dfeuer here, and the answer, but the question remains as to why I need to define fromEnum/toEnum.
The origin of the question (in case you have spare suggestions)
I was using Word32, but then it took me quite a while to undertand the reason of a but in my program. Essentially, quite a good chunk of my logic was assuming that the value of Word32s is not 0.
Therefore, I thought of using a more appropriate data type implementing appropriate classes. I came across Integer.Positive, but that's only BoundedBelow, not Bounded, whereas Word32 is Bounded, so I decided to write a datatype myself and came up with a module like this:
module Positive32 (Positive32, positive32, getNum) where
import Data.Word (Word32)
newtype Positive32 = Positive32 { getNum :: Word32 } deriving (Enum, Eq, Ord, Show)
instance Bounded Positive32 where
minBound = Positive32 1
maxBound = Positive32 (maxBound :: Word32)
positive32 :: Word32 -> Positive32
positive32 0 = error "Attempt to create `Positive32` from `0`"
positive32 i = Positive32 i
so that
λ> minBound :: Positive32
Positive32 {getNum = 1}
λ> positive32 2
Positive32 {getNum = 2}
λ> positive32 0
Positive32 {getNum = *** Exception: Attempt to create `Positive32` from `0`
CallStack (from HasCallStack):
error, called at ......
However, soon I realized that, even though I have not exported Positive32's constructor, I can still create Positive32 0 via its derived Enum instance:
λ> pred $ minBound :: Positive32
Positive32 {getNum = 0}
Hence I tried to write the Enum instance myself, think it would have a minimal definition succ :: a -> a and pred :: a -> a, but in reality it requires toEnum :: Int -> a and fromEnum :: a -> Int.
Now, in my specific usecase, that poses no problem, because Word32 fits in Int, so I can't possibly overflow Int with Word32, but I'm still left puzzled with the question of why I can't have an Enum for types that have more possible values than Int.
I will wax a little philosophical here, as you're asking a question about the motivations of people writing a spec about three decades ago, which from what I know of human memory, may not even be accurately known to the people that were involved themselves.
But: I don't think, at the time Haskell was being defined, there was a really good picture of whether it would catch on at all or merely be a flash-in-the-pan academic curiosity. There are a number of things in the design that are, therefore, pragmatic decisions made for simplicity of the compiler writers and/or the folks writing essentially toy applications. Certainly nothing was being done at the scale of modern, industrial software engineering. I won't talk about all the decisions that I think differ between those two regimes, but certainly typeclass philosophy is a bit different.
At small scales, large type classes (i.e. classes that encompass multiple concepts and have many methods) are very convenient, in the sense of low boilerplate. Type signatures can be more compact; instance implementations have lighter overhead; more library functions in terms of those classes can be provided; functionality can be more easily shared between method implementations; and programmers don't need to keep track of as many concepts.
Of course, we now know there are also costs to this. When you have a wide variety of data types floating around in your application or in publically available libraries, you find that the larger your typeclass is, the fewer types can instantiate it. At small scales, this matters less, because you can simply export the functionality your type does support and leave it at that. But at large scales, modularity demands that some behaviors be typeclass-polymorphic, and so types that cannot implement those classes lose out on a lot of already-written functionality.
We've seen this play out repeatedly over the history of Haskell. The introduction of
Applicativeas a stepping stone betweenFunctorandMonadis one example; splittingMonoidout to have aSemigroupsuperclass is another; losing theEqsuperclass onNumalso goes on the list. More generally, I think the closer to the core libraries you are, the more important it is in today's designs that typeclasses be split into as many orthogonal pieces as you can. (Of course, you have to know what orthogonal pieces are useful! It's not at all clear thatApplicativecould reasonably have been identified as a useful building block back when makingIOa monad was a fresh, exciting insight.)And so we come to
Enum. In practice,Enumserves a variety of roles, including as a de-facto part of the FFI, wheretoEnumandfromEnumare often used for marshalling to C; iterating over numeric ranges; listing inhabitants of your favorite type; as the target of multiple forms of syntactic sugar; and likely more besides. With a modern design, these concerns would likely each be split out into its own class with its own concerns and requirements. But at a small scale, most types that support one of those support essentially all of them, and so it's convenient to lump them together and share implementation work and programmer mindspace.And if you think
Enumis bad, wait until you start thinking carefully about the numeric hierarchy...