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

发布于 2024-11-08 02:13:06 字数 686 浏览 1 评论 0原文

我是 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 中转换回来。

谢谢! 西蒙

I'm new to Haskell and working my way through the Write Yourself a Scheme in 48 Hours project and I came upon an instance where I wanted to get the underlying type out of a data type and I'm not sure how to do it without writing conversions for each variant in the type.
For example, in the data type

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

I want to write something like: (I know this doesn't work)

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

or even

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

Is it possible to do this?
Basically I want to be able to cast back out of the LispVal if I need to use the basic type.

Thanks!
Simon

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

心安伴我暖 2024-11-15 02:13:06

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

在你的情况下,如果你只对提取特定类型的值感兴趣,或者如果你可以将它们转换为单一类型,你可以编写一个像 extractStringsAndAtoms :: LispVal -> > 的函数。例如,可能是字符串。

返回几种可能类型之一的唯一方法是将它们组合成一种数据类型并对其进行模式匹配 - 其通用形式是 Either a b,即 a code> 或 b 通过构造函数来区分。您可以创建一个允许提取所有可能类型的数据类型...并且它与 LispVal 本身几乎相同,因此这没有帮助。

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


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

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

    extractAtom :: LispVal ->;细绳
    extractAtom (原子 a) = a
    

    如果应用于 Atom 构造函数之外的其他内容,这将产生运行时错误,因此请务必小心。但在许多情况下,您可以通过算法中的某个时刻知道您所拥有的内容,因此可以安全地使用它。一个简单的例子是,如果您有一个 LispVal 列表,并且已经过滤掉了所有其他构造函数。

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

    extractAtom :: LispVal ->;也许是字符串
    extractAtom (原子 a) = 只是一个
    extractAtom _ = 没有
    

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

    isAtom :: LispVal ->;布尔
    isAtom = isJust 。提取原子
    
    假设Atom :: LispVal - >细绳
    假设Atom x = case extractAtom x of 
                       只是一个->一个
                       没什么->错误$“假设Atom应用于”++显示x
    
  • 定义类型时使用记录语法,如 Don 的第二个示例所示。这是一种语言魔法,在大多数情况下,定义了一堆部分函数,​​如上面的第一个 extractAtom ,并为您提供了构造值的奇特语法。如果结果类型相同,您还可以重复使用名称,例如 AtomString

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

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

    extractLispVal :: (String -> r) -> ([LispVal] -> r) -> ([LispVal] -> LispVal -> r) 
                   -> (整数-> r)-> (字符串 -> r) -> (布尔 -> r) -> (双 -> r)
                   -> LispVal ->; r
    extractLispVal f _ _ _ _ _ _ (原子 x) = fx
    extractLispVal _ f _ _ _ _ _ (列表 xs) = f xs
    ...
    

    是的,我知道这看起来很可怕。标准库中的一个示例(在更简单的数据类型上)是函数 maybeeither,它们解构同名的类型。本质上,这是一个具体化模式匹配的函数,让您可以更直接地使用它。它可能很难看,但你只需要编写一次,并且在某些情况下它可能很有用。例如,您可以使用上述函数执行以下操作:

    exprToString :: ([String] -> String) ->; ([字符串] -> 字符串 -> 字符串) 
                 -> LispVal ->;细绳
    exprToString fg = extractLispVal id (f .map recur) 
                                      (\xs x -> g (映射递归 xs) $ 递归 x)
                                      秀秀秀秀
      其中 recur = exprToString fg
    

    ...即,一个简单的递归漂亮打印函数,通过如何组合列表元素进行参数化。您还可以轻松编写 isAtom 等内容:

    isAtom = extractLispVal (const True) no (const no) no no no no
      其中 no = const False
    
  • 另一方面,有时您想要做的是匹配一个或两个构造函数,使用嵌套模式匹配,以及一个包罗万象的情况你不关心的构造函数。这正是模式匹配最擅长的,而上述所有技术只会让事情变得更加复杂。因此,不要将自己束缚于一种方法!

Unfortunately this sort of generic matching on constructors isn't possible directly, but even if it was yours wouldn't work--the extractLispVal function doesn't have a well-defined type, because the type of the result depends on the value of the input. There are various kinds of advanced type-system nonsense that can do things sort of like this, but they're not really something you'd want to use here anyway.

In your case, if you're only interested in extracting particular kinds of values, or if you can convert them to a single type, you can write a function like extractStringsAndAtoms :: LispVal -> Maybe String, for instance.

The only way to return one of several possible types is by combining them into a data type and pattern matching on that--the generic form of this being Either a b, which is either a or b distinguished by constructors. You could create a data type that would permit all possible types to extract... and it would be pretty much the same as LispVal itself, so that's not helpful.

If you really want to work with various types outside of a LispVal you could also look at the Data.Data module, which provides some means for reflection on data types. I doubt that's really what you want, though.


EDIT: Just to expand on things a bit, here's a few examples of extraction functions you can write:

  • Create single-constructor extraction functions, as in Don's first example, that assume you already know which constructor was used:

    extractAtom :: LispVal -> String
    extractAtom (Atom a) = a
    

    This will produce runtime errors if applied to something other than the Atom constructor, so be cautious with that. In many cases, though, you know by virtue of being at some point in an algorithm what you've got, so this can be used safely. A simple example would be if you've got a list of LispVals that you've filtered every other constructor out of.

  • Create safe single-constructor extraction functions, which serve as both a "do I have this constructor?" predicate and an "if so, give me the contents" extractor:

    extractAtom :: LispVal -> Maybe String
    extractAtom (Atom a) = Just a
    extractAtom _ = Nothing
    

    Note that this is more flexible than the above, even if you're confident of what constructor you have. For example, it makes defining these easy:

    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
    
  • Use record syntax when defining the type, as in Don's second example. This is a bit of language magic, for the most part, defines a bunch of partial functions like the first extractAtom above and gives you a fancy syntax for constructing values. You can also reuse names if the result is the same type, e.g. for Atom and String.

    That said, the fancy syntax is more useful for records with many fields, not types with many single-field constructors, and the safe extraction functions above are generally better than ones that produce errors.

  • Getting more abstract, sometimes the most convenient way is actually to have a single, all-purpose deconstruction function:

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

    Yeah, it looks horrendous, I know. An example of this (on a simpler data type) in the standard libraries are the functions maybe and either, which deconstruct the types of the same names. Essentially, this is a function that reifies the pattern matching and lets you work with that more directly. It may be ugly, but you only have to write it once, and it can be useful in some situations. For instance, here's one thing you could do with the above function:

    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
    

    ...i.e., A simple recursive pretty-printing function, parameterized by how to combine the elements of a list. You can also write isAtom and the like easily:

    isAtom = extractLispVal (const True) no (const no) no no no no
      where no = const False
    
  • On the other hand, sometimes what you want to do is match one or two constructors, with nested pattern matches, and a catch-all case for the constructors you don't care about. This is exactly what pattern matching is best at, and all the above techniques would just make things far more complicated. So don't tie yourself to just one approach!

但可醉心 2024-11-15 02:13:06

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

extractLispValDouble (Double val) = val

或者使用记录选择器:

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

但是,您不能编写返回 String 或 Bool 或天真地使用 Double (以及其他任何东西),因为您无法为此编写类型。

You can always extract the fields from your data type, either by pattern matching on individual constructors:

extractLispValDouble (Double val) = val

or using a record selector:

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

However, you can't write a function that returns a String or a Bool or a Double (and whatever else) naively, since you can't write a type for that.

清浅ˋ旧时光 2024-11-15 02:13:06

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

You can get more-or-less what you want by using GADTs. It gets a scary quickly, but it works :-) I have strong doubts how far this approach will get you, though!

Here's something I whipped up quickly, with a not-quite-right (a bit too many spaces) printLispVal function thrown in - I wrote that to see whether you can actually use my construction. Notice that the boilerplate of extracting a basic type is in the extractShowableLispVal function. I think this approach will quickly run into trouble when you start doing more complicated things like trying to do arithmetic and so.

{-# 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" ))
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文