Search code examples
haskellparsec

Compact way to map strings to datatype using Parsec


I seem to be coming across mapping keywords straight to a datatype fairly often and I solve it as below. It can quickly get out of hand as you have to repeat the string values.

Is there a more compact way to express this?

import Text.ParserCombinators.Parsec

data Keyword = Apple | Banana | Cantaloupe

parseKeyword :: Parser Keyword
parseKeyword = (  string "apple"
              <|> string "banana"
              <|> string "cantaloupe"
               ) >>= return . strToKeyword
                    where strToKeyword str = case str of
                           "apple"      -> Apple
                           "banana"     -> Banana
                           "cantaloupe" -> Cantaloupe

EDIT:

As a followup question, since this seemed to be too easy. How would the compact solution work with try?

E.g.

import Text.ParserCombinators.Parsec

data Keyword = Apple | Apricot | Banana | Cantaloupe

parseKeyword :: Parser Keyword
parseKeyword = (  try (string "apple")
              <|> string "apricot"
              <|> string "banana"
              <|> string "cantaloupe"
               ) >>= return . strToKeyword
                    where strToKeyword str = case str of
                           "apple"      -> Apple
                           "apricot"    -> Apricot
                           "banana"     -> Banana
                           "cantaloupe" -> Cantaloupe

Solution

  • I’m not sure this is a terribly elegant solution, but if you derive a few more typeclasses:

    data Keyword = Apple | Banana | Cantaloupe deriving (Eq, Read, Show, Enum, Bounded)
    

    You can suddenly get all of the values:

    ghci> [minBound..maxBound] :: [Keyword]
    [Apple,Banana,Cantaloupe]
    

    For any particular value, we can parse it and then return the value:

    parseEnumValue :: (Show a) => a -> Parser a
    parseEnumValue val = string (map toLower $ show val) >> return val
    

    Then we can combine these to parse any value of it:

    parseEnum :: (Show a, Enum a, Bounded a) => Parser a
    parseEnum = choice $ map parseEnumValue [minBound..maxBound]
    

    Try it out:

    ghci> parseTest (parseEnum :: Parser Keyword) "cantaloupe"
    Cantaloupe
    ghci> parseTest (parseEnum :: Parser Keyword) "orange"
    parse error at (line 1, column 1):
    unexpected "o"
    expecting "apple", "banana" or "cantaloupe"