Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle lots of constants in Haskell?

I’m working on a library allowing a developper to control a Minitel (the french videotex terminal).

I have a lot of constant values and I would like to know the best way to manage them with Haskell. It's a common question among beginners but I haven't found a satisfying answer.

You can have a look at my project (Note: yes, there are too many constants in only one module, that's what I'm working on ;-) )

I currently have modules keeping them as name = value. Though it works, I would like to know if it can be perfected or if I'm doing right.

aNUL = 0x00 -- Null
-- ...
aUS  = 0x1f -- Unit Separator

This method has a minor drawback: you cannot use pattern matching, you need to use guards if you want to keep the names:

completeReturn :: MString -> Bool
completeReturn []                 = False
completeReturn [0x19]             = False -- eSS2
completeReturn [0x1b, 0x5b, 0x32] = False -- eESC, eCSI, 0x32
completeReturn [0x1b, 0x5b, 0x34] = False -- eESC, eCSI, 0x34
completeReturn [0x19, 0x4b]       = False -- eSS2, 0x4b ; cedilla
completeReturn _                  = True

You must also use GHC options if you don't want GHC to yell at you for the missing signatures or the type defaults:

{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-type-defaults #-}

I once tried it with data deriving Enum using the trick to compensate undefined values but it becomes ugly as soon as the value does not start at 0. It is also error prone, if you omit or add one value, the following names will have their values plus or minus one:

data ASCII = NUL -- ^ 0x00, Null
           -- ... 
           | US  -- ^ 0x1f, Unit Separator
           deriving (Enum, Show, Eq, Ord)

data C0 = NUL   -- ^ 0x00, NULl
        | Res01 -- ^ 0x01, undefined value
        -- ...
        | APA   -- ^ 0x1f, Activate Position Address
        deriving (Enum, Show, Eq, Ord)

data SSCFS = Res00 | Res01 | Res02 | Res03 | Res04 | Res05 | Res06 | Res07
           -- ...
           | Res38 | Res39 | Res3A | Res3B | Res3C | Res3D | Res3E | Res3F
           | ABK -- ^ 0x40, Alpha BlacK
           -- ...
           | RMS -- ^ 0x5f
           deriving (Enum, Show, Eq, Ord)

This solution has a drawback: you cannot easily mix the values in a list because they are of different types:

codes = [ASCII.NUL, ASCII.SOH, C0.APB, C0.APF, 0x24] -- Error!

I thought of another solution:

class Value a where
    value :: a -> Int

-- ASCII codes
data ASCII = NUL | SOH | STX | ETX {- ... -} deriving Show

instance Value ASCII where
    value NUL = 0
    value SOH = 1
    -- ...

-- C0 codes
data C0 = APB | APF | APD | APU {- ... -} deriving Show

instance Value C0 where
    value APB = 10
    value APF = 11
    -- ...

-- Mini type
data Mini = ASCII ASCII | C0 C0 | Literal Int deriving Show

instance Value Mini where
    value (ASCII code)  = value code
    value (C0 code)     = value code
    value (Literal int) = int

codes = [ASCII NUL, C0 APB, Literal 0x20]

main = do
    print (fmap value codes)

For this solution, I must take care that constructors don't overlap. For example, NUL, SO and SI exist in both ASCII and C0 (They fortunately give the same values :-) ). I can handle the case by only defining them in ASCII for example. Using qualified import would make things uglier (ASCII ASCII.NUL).

Do you see other better ways to handle this case ?

like image 696
zigazou Avatar asked Feb 22 '15 15:02

zigazou


1 Answers

If you have ghc 7.8, a new language extension pattern synonyms (see section 7.3.8) elegantly solves this problem. Pattern synonyms are enabled with a LANGUAGE pragma or the -XPatternSynonyms flag.

{-# LANGUAGE PatternSynonyms #-}

Pattern synonyms definitions are prefixed by pattern

pattern NUL = 0x00
pattern SSC = 0x19
pattern ESC = 0x1b
pattern US  = 0x1f
pattern CSI = 0x5b

We can write your example in terms of these patterns.

type MString = [Int]

completeReturn :: MString -> Bool
completeReturn []                 = False
completeReturn [SSC]              = False -- eSS2
completeReturn [ESC , CSI , 0x32] = False -- eESC, eCSI, 0x32
completeReturn [ESC , CSI , 0x34] = False -- eESC, eCSI, 0x34
completeReturn [SSC , 0x4b]       = False -- eSS2, 0x4b ; cedilla
completeReturn _                  = True

Pattern synonyms are bidirectional, so we can also use them to construct expressions.

completeReturn [SSC]

You can write pattern synonyms that capture variables.

pattern EscCsi x = [ESC , CSI , x]

And use them like a constructor for both pattern matching

completeReturn :: MString -> Bool
completeReturn []                 = False
completeReturn [SSC]              = False -- eSS2
completeReturn (EscCsi 0x32)      = False -- eESC, eCSI, 0x32
completeReturn (EscCsi 0x34)      = False -- eESC, eCSI, 0x34
completeReturn [SSC , 0x4b]       = False -- eSS2, 0x4b ; cedilla
completeReturn _                  = True

and for constructing expressions.

completeReturn (EscCsi 0x7e)
like image 154
Cirdec Avatar answered Oct 03 '22 09:10

Cirdec