Question

I'm writing a program which reads and writes images that supports multiple pixel types (i.e. RGB, CMYK, greyscale etc.). These pixel types can use different types of components, sort of like this:

class (Storable c) => PixelComponent c where
    blackWhite :: c -> (c, c)
    toInt :: c -> Int
    toRealFrac :: (RealFrac a) => c -> a
    fromComponent :: (PixelComponent a) => a -> c

instance PixelComponent CUChar where
    blackWhite x = (minBound x, maxBound x)
    toInt = id
    toRealFrac = fromIntegral
    fromComponent x = ???

instance PixelComponent CFloat where
    black = 0.0
    white = 1.0
    toInt = truncate
    toReal = id
    fromComponent x = ???

class (Storable pix) => Pixel pix where
    red :: pix c -> c
    green :: pix c -> c
    blue :: pix c -> c
    alpha :: pix c -> c
    luminance :: pix c -> c
    fromPixel :: (Pixel a) => a c -> pix c

The idea is that you should be able to do getPixel myImage (10, 23) :: RGB CUChar or getPixel myImage (10, 23) :: RGB CFloat depending on the pixel format you want. The problem is that I don't know how to implement fromComponent in an efficient manner. Essentially, I would like unnecessary conversions such as fromComponent (1 :: CUChar) :: CUChar and fromComponent (0.5 :: CFloat) :: CFloat to be no-ops. I guessing that I'll have to rely on optimizations in any case.

Note: This might not be a good design anyway so if someone has a better suggestion I'm open to this. I'd still like to know how to make this solution work.

Was it helpful?

Solution

I'd suggest that handle this the same way as GHC handles numeric conversions (e.g. fromIntegral), which is via rewrite rules.

If you look in GHC.Real, you find

-- | general coercion from integral types
fromIntegral :: (Integral a, Num b) => a -> b
fromIntegral = fromInteger . toInteger

{-# RULES
"fromIntegral/Int->Int" fromIntegral = id :: Int -> Int
    #-}

The default is a lot of round-tripping for integral types, but fortunately that never happens because there are RULEs for all library-provided integral types.

There are many more RULEs specified in GHC.Int for example to handle the rest of the conversions. You'll find a similar setup for other similar functions (e.g. realToFrac).

Now there's one major problem with your use-case, which is that it's often difficult for RULEs to match on class methods. There are two ways around this. The first is to define a common type (e.g. Integer in GHC's code), and provide class methods to convert to and from that type. Then write a general-purpose conversion function (e.g. fromIntegral), use that everywhere, and have your RULEs match on it.

The other approach is to do something like this:

instance PixelComponent CUChar where
    blackWhite x = (minBound x, maxBound x)
    toInt = id
    toRealFrac = fromIntegral
    {-# INLINE fromComponent #-}
    fromComponent = toCUChar

toCUChar :: PixelComponent a => a -> CUChar
toCUChar = ...

{-# RULES "fromComponent/CUChar->CUChar" toCUChar = id :: CUChar -> CUChar #-}

The former is what GHC does, so it's likely to work well. I've been using the latter approach recently and haven't had any issues though, so either one should work.

OTHER TIPS

Multi-parameter type classes might be your friend here, but keep in mind you're going to have to type out things n^2 times here.

Here's a trivial example, lets say we have separate types for lower case strings, upper case strings, and mixed strings. Obviously, converting a string to it's own type shouldn't require any work, nor should converting to a mixed string. Only conversions to an upper or lower string not from itself should require a conversion.

The code below (ideone) illustrates this approach:

{-# LANGUAGE MultiParamTypeClasses #-}

import Data.Char (toUpper, toLower)

data Lower = Lower String deriving Show
data Mixed = Mixed String deriving Show
data Upper = Upper String deriving Show

toUpperStr = map toUpper
toLowerStr = map toLower

class Make a where
  make :: String -> a

instance Make Lower where
  make s = Lower (toLowerStr s)

instance Make Mixed where
  make s = Mixed s

instance Make Upper where
  make s = Upper (toUpperStr s)

class Convert a b where
  convert :: a -> b

instance Convert Lower Lower where
  convert = id

instance Convert Lower Mixed where
  convert (Lower s) = Mixed s

instance Convert Lower Upper where
  convert (Lower s) = Upper (toUpperStr s)

instance Convert Mixed Lower where
  convert (Mixed s) = Lower (toLowerStr s)

instance Convert Mixed Mixed where
  convert = id

instance Convert Mixed Upper where
  convert (Mixed s) = Upper (toUpperStr s)

instance Convert Upper Lower where
  convert (Upper s) = Lower (toLowerStr s)

instance Convert Upper Mixed where
  convert (Upper s) = Mixed s

instance Convert Upper Upper where
  convert = id

main = print (convert ((make "Hello World") :: Lower) :: Upper)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top