Haskell type and pattern matching question: extracting fields from a data type
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开发者_StackOverflow社区 be able to cast back out of the LispVal if I need to use the basic type.
Thanks! Simon
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 ofLispVal
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. forAtom
andString
.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
andeither
, 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!
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.
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" ))
精彩评论