使用Parsec将字符串映射到数据类型的简洁方法

时间:2014-08-09 04:21:04

标签: haskell parsec

我似乎经常将关键字直接映射到数据类型,我解决它如下所示。当你必须重复字符串值时,它很快就会失控。

有更简洁的表达方式吗?

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

编辑:

作为后续问题,因为这似乎太容易了。紧凑型解决方案如何与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

3 个答案:

答案 0 :(得分:9)

如果您只是想避免重复,可以使用(<$)运算符:

import Text.ParserCombinators.Parsec
import Control.Applicative ((<$))

data Keyword = Apple | Banana | Cantaloupe

parseKeyword :: Parser Keyword
parseKeyword
    =   Apple      <$ string "apple"
    <|> Banana     <$ string "banana"
    <|> Cantaloupe <$ string "cantaloupe"

对于只使用GHC.Generics的单位构造函数的任何类型,也可以制作完全通用的解决方案:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

import Text.ParserCombinators.Parsec
import Control.Applicative ((<*))
import Data.Char (toLower)
import GHC.Generics

class GParse f where
    gParse :: Parser (f a)

instance (GParse f, Constructor c) => GParse (C1 c f) where
    gParse = fmap M1 gParse <* string (map toLower $ conName (undefined :: t c f a))

instance GParse f => GParse (D1 c f) where
    gParse = fmap M1 gParse

instance (GParse a, GParse b) => GParse (a :+: b) where
    gParse = try (fmap L1 gParse) <|> fmap R1 gParse

instance GParse U1 where
    gParse = return U1

genericParser :: (Generic g, GParse (Rep g)) => Parser g
genericParser = fmap to gParse

这是很多样板文件,但现在你只需要为任何兼容类型创建一个解析器:

{-# LANGUAGE DeriveGeneric #-}

data Keyword = Apricot | Apple | Banana | Cantaloupe deriving (Show, Generic)

parseKeyword :: Parser Keyword
parseKeyword = genericParser

GHCI测试:

> parseTest parseKeyword "apple"
Apple
> parseTest parseKeyword "apricot"
Apricot
> parseTest parseKeyword "banana"
Banana

处理像RedApple这样的多字构造函数只是为"RedApple" - &gt;编写字符串翻译函数的问题。 "red_apple"并在C1实例中使用它。即。

import Data.List (intercalate)
import Data.Char (toLower, isLower)

mapName :: String -> String
mapName = intercalate "_" . splitCapWords where
    splitCapWords "" = []
    splitCapWords (x:xs) =
        let (word, rest) = span isLower xs
        in (toLower x : word) : splitCapWords rest

instance (GParse f, Constructor c) => GParse (C1 c f) where
    gParse = fmap M1 gParse <* string (mapName $ conName (undefined :: t c f a))

答案 1 :(得分:5)

我不确定这是一个非常优雅的解决方案,但是如果你派出更多的类型类:

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

您可以突然获得所有值:

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

对于任何特定值,我们可以解析它然后返回值:

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

然后我们可以将它们组合起来解析它的任何值:

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

尝试一下:

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"

答案 2 :(得分:5)

这个怎么样?

parseKeyword
    =   (string "apple"      >> return Apple)
    <|> (string "banana"     >> return Banana)
    <|> (string "cantaloupe" >> return Cantaloupe)

对于你的后续行动,这似乎与我执行的六个随机测试的实现一样好

parseKeyword :: Parser Keyword
parseKeyword
    = try (string "apple"      >> return Apple)
    <|>   (string "apricot"    >> return Apricot)
    <|>   (string "banana"     >> return Banana)
    <|>   (string "cantaloupe" >> return Cantaloupe)

该技术只是使每个子表达式返回最终类型,而不是将其委托给case语句的块结尾。返回不会改变解析器的行为。