很遗憾,这种构造函数的通用匹配不能直接实现,但即使可以,你的构造函数也行不通——extractLispVal函数没有明确定义的类型,因为结果的类型取决于输入值的值。有各种各样的高级类型系统非常规操作可以完成类似的事情,但在这里使用它们并不是你想要的。
在你的情况下,如果你只对提取特定类型的值感兴趣,或者如果你可以将它们转换为单个类型,你可以编写一个函数,例如extractStringsAndAtoms:: LispVal -> Maybe String。
返回几种可能类型中的一种的唯一方法是将它们组合成数据类型并在其上进行模式匹配——其通用形式为Either a b,它是由构造函数区分的a或b之一。你可以创建一个数据类型,允许提取所有可能的类型...它几乎与LispVal本身相同,所以这并不有用。
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 LispVal
s 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!
Maybe
的“安全提取器”除了直接模式匹配之外,是最好的默认方法。 - C. A. McCann