0
votes

I want to convert an enum into Int or vice verse, i.e. implement an bidirectional mapping between the tag of sum type and Int. I have tried fromEnum but it seems not fast enough, and then I tried unsafeCoerce but it doesn't works as expected:

import Data.Time.Clock
import Data.Int
import Unsafe.Coerce
import Control.Monad (replicateM_)

data Color = R | G | B
    deriving (Enum)

main = do
    printT 1 $ (unsafeCoerce R :: Int8)
    printT 1000 $ (unsafeCoerce G :: Int8)
    printT 1000000 $ (unsafeCoerce B :: Int8)
    printT 1000000000 $ (unsafeCoerce R :: Int8)

    printT 1 $ (fromEnum R)
    printT 1000 $ (fromEnum G)
    printT 1000000 $ (fromEnum B)
    printT 1000000000 $ (fromEnum B)

---------- profile tools ------------

printT :: Show a => Int -> a -> IO ()
printT n x = print =<< timeIt n (pure x)

timeIt :: Int -> IO a -> IO a
timeIt n _ | n <= 0 = error "timeIt n | n <= 0"
timeIt n proc = do
    t0 <- getCurrentTime
    replicateM_ (n-1) proc
    x <- proc
    t1 <- getCurrentTime
    putStrLn ("-- Time Used (repeat " ++ show n ++ " times): " ++ show (t1 `diffUTCTime` t0))
    return x

So what is the fastest way to do this?

1
What is "not fast enough". I would be supprised if this is really a bottleneck. - Willem Van Onsem
I am not convinced you here run banchmarks on fromEnum. At first sight you run benchmarks on the pure itself, at first sight fromEnum R, etc. are never evaluated. Likely unsafeCoerce R and fromEnum R will produce (approximately) the same results, since we never really make the evaluation. - Willem Van Onsem
Note: When profiling times in Haskell, the easiest method to do so is usually by using criterion. I recommend using this library to ensure that your benchmarks are as accurate as possible. - bradrn
Note that unsafeCoerce is 100% incorrect here and liable to crash your program at surprising times. GHC does not represent Int8 and a 3-constructor sum type in anything resembling the same memory layout. - Carl
Derived toEnum and fromEnum are really, really fast. They are, in fact, the fastest way the GHC developers have found to "convert an enum into Int or vice [versa], i.e. implement an bidirectional mapping between the tag of sum type and Int." In the event that you find a faster way, you should submit a merge request to GHC; the performance of this code has always been a pretty high priority. - dfeuer

1 Answers

1
votes

Is the Enum you actually care about your type, or someone else's? If it's someone else's, then you're not guaranteed any methods you can use besides fromEnum, so you're out of luck. If it's your own type, then you can reimplement it with a newtype and pattern synonyms instead of deriving, so that fromEnum is literally free (provided the compiler can specialize it wherever you use it):

{-# LANGUAGE PatternSynonyms #-}

module ColorEnum (Color(R,G,B)) where

import Data.Coerce (coerce)

newtype Color = UnsafeColor Int

pattern R, G, B :: Color
pattern R = UnsafeColor 0
pattern G = UnsafeColor 1
pattern B = UnsafeColor 2

maxColor :: Int
maxColor = 2

instance Enum Color where
  succ (UnsafeColor a)
    | a == maxColor = error "succ{Color}: tried to take `succ' of last tag in enumeration"
    | otherwise = UnsafeColor (a + 1)
  pred (UnsafeColor a)
    | a == 0 = error "pred{Color}: tried to take `pred' of first tag in enumeration"
    | otherwise = UnsafeColor (a - 1)
  toEnum a
    | a >= 0 && a <= maxColor = UnsafeColor a
    | otherwise = error $ "toEnum{Color}: tag (" ++ show a ++ ") is outside of enumeration's range (0," ++ show maxColor ++ ")"
  enumFrom (UnsafeColor a) = coerce [a..maxColor]
  enumFromThen (UnsafeColor a) (UnsafeColor b) = coerce [a,b..if a > b then 0 else maxColor]
  fromEnum = coerce

Caveats:

  • This is not an endorsement of how you did your benchmark (in fact, it probably is wrong, as commenters pointed out)
  • There's a good chance that things other than fromEnum will be made slower by this change
  • All of that code is just to replace data Color = R | G | B deriving (Enum)