How to handle lots of constants in Haskell?
Asked Answered
F

2

5

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 ?

Feverwort answered 22/2, 2015 at 15:57 Comment(2)
"For this solution, I must take care that constructors don't overlap" Couldn't you just change the name of the constructors, ASCII_NUL instead of NUL ?Baseless
It wouldn't be better than using qualified imports: ASCII ASCII.NUL would be replaced by ASCII ASCII_NUL (the ASCII type must be encapsulated in the Mini type if I want to create lists combining many types)Feverwort
L
6

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)
Liquor answered 22/2, 2015 at 16:39 Comment(3)
Nice. Is it possible to use them with view patterns or similar?Stockman
@Stockman The proposal says so. "Together with ViewPatterns we can now create patterns that look like regular patterns to match on existing (perhaps abstract) types in new ways". I haven't tried it. This is in the same section that says we can declare the types for pattern synonyms with e.g. pattern NUL :: Int; pattern NUL = 0x00, but that didn't work.Liquor
Pattern Synonyms adopted ! <3Feverwort
S
3

The second thing that comes to mind (after something too ugly to think about) is to use Enum without always deriving it. So you could use the same ASCII you described, but

instance Enum ASCII where
  fromEnum a = case a of
    NUL -> 0x00
    ...
    US -> 0x1f
  toEnum a = case a of
    0x -> NUL
    0x1f -> US

I'm not familiar with Template Haskell, but I would venture to guess that you could almost certainly write something using it that would let you convert a one-way conversion table into a fromEnum and a toEnum.

Stockman answered 22/2, 2015 at 16:38 Comment(1)
I'd prefer not to use template Haskell. I both am not used to TH and believe (wrongly or not) that TH should be reserved for more complex matter. There's a solution here allowing you to reduce the amount of code one should write.Feverwort

© 2022 - 2024 — McMap. All rights reserved.