Haskell类型和模式匹配问题:从数据类型中提取字段

时间:2011-05-18 15:52:11

标签: haskell syntax types pattern-matching

我是Haskell的新手,并且在48小时项目中通过自己编写一个方案开始工作,我遇到了一个实例,我希望从数据类型中获取基础类型,我不知道如何在不为类型中的每个变体编写转换的情况下执行此操作。 例如,在数据类型

data LispVal = Atom String
             | List [LispVal]
             | DottedList [LispVal] LispVal
             | Number Integer
             | String String
             | Bool Bool
             | Double Double

我想写一些类似的东西:(我知道这不起作用)

extractLispVal :: LispVal -> a
extractLispVal (a val) = val

甚至

extractLispVal :: LispVal -> a
extractLispVal (Double val) = val
extractLispVal (Bool val) = val

有可能这样做吗? 基本上,如果我需要使用基本类型,我希望能够从LispVal中退出。

谢谢! 西蒙

3 个答案:

答案 0 :(得分:8)

不幸的是,这种关于构造函数的泛型匹配是不可能直接的,但即使它是你的也不行 - extractLispVal函数没有明确定义的类型,因为类型结果取决于输入的值。有各种类型的高级类型系统废话可以做类似这样的事情,但它们并不是你想要在这里使用的东西。

在您的情况下,如果您只对提取特定类型的值感兴趣,或者您可以将它们转换为单一类型,则可以编写类似extractStringsAndAtoms :: LispVal -> Maybe String的函数。

返回几种可能类型之一的唯一方法是将它们组合成数据类型和模式匹配 - 这是Either a b的通用形式,它是a或{ {1}}由构造函数区分。您可以创建一个允许所有可能类型提取的数据类型......它与b本身几乎相同,所以这没有帮助。

如果您真的想使用LispVal之外的各种类型,您还可以查看LispVal模块,该模块提供了一些反映数据类型的方法。我怀疑这真的是你想要的。


编辑:只是稍微扩展一下,这里有一些你可以编写的提取函数的例子:

  • 创建单构造函数提取函数,如Don的第一个示例所示,假设您已经知道使用了哪个构造函数:

    Data.Data

    如果应用于extractAtom :: LispVal -> String extractAtom (Atom a) = a 构造函数之外的其他内容,则会产生运行时错误,因此请谨慎对待。但是,在许多情况下,你知道你在某个算法的某个方面已经得到了什么,所以这可以安全地使用。一个简单的例子就是如果你有一个Atom的列表,你已经过滤了其他每个构造函数。

  • 创建安全的单构造函数提取函数,它既是“我有这个构造函数吗?”谓词和“如果是这样,给我内容”提取器:

    LispVal

    请注意,即使您对自己的构造函数有信心,这也比上述更灵活。例如,它使定义这些变得容易:

    extractAtom :: LispVal -> Maybe String
    extractAtom (Atom a) = Just a
    extractAtom _ = Nothing
    
  • 在定义类型时使用记录语法,如Don的第二个示例中所示。这是一种语言魔力,在大多数情况下,定义了一堆部分函数,​​如上面的第一个isAtom :: LispVal -> Bool isAtom = isJust . extractAtom assumeAtom :: LispVal -> String assumeAtom x = case extractAtom x of Just a -> a Nothing -> error $ "assumeAtom applied to " ++ show x ,并为您提供了一个用于构造值的精美语法。如果结果是相同类型,您也可以重复使用名称,例如extractAtomAtom

    也就是说,花哨的语法对于包含许多字段的记录更有用,而不是具有许多单字段构造函数的类型,并且上面的安全提取函数通常比产生错误的函数更好。

  • 获得更抽象,有时最方便的方法实际上是拥有一个单一的,通用的解构函数:

    String

    是的,我知道它看起来很可怕。标准库中的一个示例(在更简单的数据类型上)是函数extractLispVal :: (String -> r) -> ([LispVal] -> r) -> ([LispVal] -> LispVal -> r) -> (Integer -> r) -> (String -> r) -> (Bool -> r) -> (Double -> r) -> LispVal -> r extractLispVal f _ _ _ _ _ _ (Atom x) = f x extractLispVal _ f _ _ _ _ _ (List xs) = f xs ... maybe,它们解构相同名称的类型。从本质上讲,这是一个功能,它可以激活模式匹配,让您更直接地使用它。它可能很难看,但你只需要写一次,它在某些情况下很有用。例如,您可以使用上述功能执行以下操作:

    either

    ...即一个简单的递归漂亮打印函数,通过如何组合列表的元素进行参数化。您也可以轻松编写exprToString :: ([String] -> String) -> ([String] -> String -> String) -> LispVal -> String exprToString f g = extractLispVal id (f . map recur) (\xs x -> g (map recur xs) $ recur x) show show show show where recur = exprToString f g 之类的内容:

    isAtom
  • 另一方面,有时您想要做的是匹配一个或两个构造函数,嵌套模式匹配,以及您不关心的构造函数的全包情况。这正是模式匹配最擅长的,而上述所有技术都会让事情变得更加复杂。所以不要只想一种方法!

答案 1 :(得分:6)

您始终可以通过各个构造函数上的模式匹配从数据类型中提取字段:

extractLispValDouble (Double val) = val

或使用记录选择器:

data LispVal = Atom { getAtom :: String }
             ...          
             | String { getString :: String }
             | Bool   { getBool :: Bool }
             | Double { getDouble :: Double }

但是,你不能写一个能够天真地返回String或Bool或Double(以及其他任何东西)的函数,因为你不能为它写一个类型。

答案 2 :(得分:2)

使用GADT可以获得或多或少的效果。它很快就会变得很吓人,但它确实有效:-)我怀疑这种方法会给你带来多大的帮助!

这是我快速掀起的东西,有一个不太正确(有点太多空格)printLispVal函数抛出 - 我写了一下,看看你是否真的可以使用我的构造。请注意,提取基本类型的样板位于extractShowableLispVal函数中。我认为当你开始做更复杂的事情,比如尝试做算术时,这种方法会很快遇到麻烦。

{-# LANGUAGE GADTs #-}
data Unknown = Unknown

data LispList where
    Nil :: LispList
    Cons :: LispVal a -> LispList -> LispList

data LispVal t where
    Atom :: String -> LispVal Unknown
    List :: LispList -> LispVal Unknown
    DottedList :: LispList -> LispVal b -> LispVal Unknown
    Number :: Integer -> LispVal Integer
    String :: String -> LispVal String
    Bool   :: Bool -> LispVal Bool
    Double :: Double -> LispVal Double

data Showable s where
    Showable :: Show s => s -> Showable s

extractShowableLispVal :: LispVal a -> Maybe (Showable a)
extractShowableLispVal (Number x) = Just (Showable x)
extractShowableLispVal (String x) = Just (Showable x)
extractShowableLispVal (Bool x) = Just (Showable x)
extractShowableLispVal (Double x) = Just (Showable x)
extractShowableLispVal _ = Nothing

extractBasicLispVal :: LispVal a -> Maybe a
extractBasicLispVal x = case extractShowableLispVal x of
    Just (Showable s) -> Just s
    Nothing -> Nothing

printLispVal :: LispVal a -> IO ()
printLispVal x = case extractShowableLispVal x of    
    Just (Showable s) -> putStr (show s)
    Nothing -> case x of
        Atom a -> putStr a
        List l -> putChar '(' >> printLispListNoOpen (return ()) l
        DottedList l x -> putChar '(' >> printLispListNoOpen (putChar '.' >> printLispVal x) l

printLispListNoOpen finish = worker where
    worker Nil = finish >> putChar ')'
    worker (Cons car cdr) = printLispVal car >> putChar ' ' >> worker cdr

test = List . Cons (Atom "+") . Cons (Number 3) . Cons (String "foo") $ Nil
test2 = DottedList (Cons (Atom "+") . Cons (Number 3) . Cons (String "foo") $ Nil) test
-- printLispVal test prints out (+ 3 "foo" )
-- printLispVal test2 prints out (+ 3 "foo" .(+ 3 "foo" ))