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

Haskell类型和模式匹配问题:从数据类型中提取字段,haskell,syntax,types,pattern-matching,Haskell,Syntax,Types,Pattern Matching,我是Haskell的新手,在48小时内完成了“编写自己的方案”项目,我遇到了一个实例,我想从数据类型中获取基础类型,但我不知道如何在不为类型中的每个变量编写转换的情况下实现这一点。 例如,在数据类型中 data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | Str

我是Haskell的新手,在48小时内完成了“编写自己的方案”项目,我遇到了一个实例,我想从数据类型中获取基础类型,但我不知道如何在不为类型中的每个变量编写转换的情况下实现这一点。 例如,在数据类型中

data LispVal = Atom String
             | List [LispVal]
             | DottedList [LispVal] LispVal
             | Number Integer
             | String String
             | Bool Bool
             | Double Double
我想写这样的东西:(我知道这行不通)

甚至

extractLispVal :: LispVal -> a
extractLispVal (Double val) = val
extractLispVal (Bool val) = val
有可能这样做吗? 基本上,如果需要使用基本类型,我希望能够从LispVal中强制转换出来

谢谢!
Simon

您始终可以从数据类型中提取字段,方法是在单个构造函数上进行模式匹配:

或使用记录选择器:

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

但是,您不能天真地编写返回字符串、Bool或Double(以及其他)的函数,因为您不能为此编写类型。

不幸的是,构造函数上的这种泛型匹配无法直接实现,但是,即使它是您的,也无法工作--
extractLispVal
函数没有定义良好的类型,因为结果的类型取决于输入的值。有各种各样的高级类型的系统可以做类似这样的事情,但它们并不是你想要在这里使用的东西

在您的例子中,如果您只对提取特定类型的值感兴趣,或者如果您可以将它们转换为单个类型,那么您可以编写一个函数,例如
extractstringsandaoms::LispVal->Maybe String

返回几种可能类型中的一种的唯一方法是将它们组合成一个数据类型,并在该类型上进行模式匹配——其一般形式是
a b
,这是由构造函数区分的
a
b
。您可以创建一个允许所有可能类型提取的数据类型。。。它与
LispVal
本身几乎相同,所以这没有什么帮助

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


编辑:这里是一些您可以编写的提取函数示例:

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

    extractAtom :: LispVal -> String
    extractAtom (Atom a) = a
    
    如果应用于非
    Atom
    构造函数,则会产生运行时错误,因此要小心。不过,在很多情况下,通过在算法中的某个点,你知道你得到了什么,所以这可以安全地使用。一个简单的例子是,如果你有一个
    LispVal
    s列表,你已经过滤掉了所有其他构造函数

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

    extractAtom :: LispVal -> Maybe String
    extractAtom (Atom a) = Just a
    extractAtom _ = Nothing
    
    请注意,这比上述方法更灵活,即使您对自己拥有的构造函数很有信心。例如,它使定义这些内容变得简单:

    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
    
  • 定义类型时使用记录语法,如Don的第二个示例所示。这是一种语言魔术,在大多数情况下,它定义了一系列部分函数,如上面的第一个
    extractAtom
    ,并为您提供了构造值的奇特语法。如果结果的类型相同,也可以重用名称,例如用于
    Atom
    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
    ...
    
    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
    
    是的,看起来很可怕,我知道。标准库中的一个例子是函数
    maybe
    other
    ,它们解构了相同名称的类型。本质上,这是一个具体化模式匹配的函数,可以让您更直接地使用它。它可能很难看,但你只需要写一次,在某些情况下它可能很有用。例如,使用上述函数可以做一件事:

    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
    ...
    
    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
    等:

    isAtom = extractLispVal (const True) no (const no) no no no no
      where no = const False
    
  • 另一方面,有时您要做的是使用嵌套模式匹配来匹配一个或两个构造函数,并为您不关心的构造函数提供一个全面的情况。这正是模式匹配最擅长的,所有上述技术只会让事情变得更加复杂。所以,不要只局限于一种方法


通过使用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" ))

谢谢,这就是我想的,我只是想确定一下。哈斯凯尔的打字系统对我来说仍然有点神秘,所以我想这里可能会发生某种魔法:)@Simon:嗯,也许会。如果您发现您的代码冗长或笨拙,并且希望使用一个有趣的工具来简化它