Haskell type and pattern matching issue: extracting fields from a data type

I am new to Haskell and worked through the Write Yourself Scheme at 48 hours, and I came across an instance where I wanted to get the base type from the data type, and I'm not sure how to do this without recording conversions for each variant in the type. For example, in a 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 does not work)

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

or even

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

Can this be done? Basically, I want me to be able to drop LispVal if I need to use a base type.

Thanks! Simon

+6
source share
3 answers

Unfortunately, such a generic approach to constructors is impossible directly, but even if it was yours, it will not work - the extractLispVal function extractLispVal not have a clearly defined type, since the type of the result depends on the input value. There are various kinds of advanced hacking type systems that can do something like this, but this is actually not what you would like to use here.

In your case, if you are only interested in extracting certain types of values, or if you can convert them to one type, you can write, for example, a function of type extractStringsAndAtoms :: LispVal -> Maybe String .

The only way to return one of several possible types is to combine them into a data type and pattern matching on this: the general form of this creature is Either ab , which is either a or b , different constructors. You can create a data type that allows you to retrieve all possible types ... and it would be almost the same as LispVal itself, so it did not help.

If you really want to work with various types outside of LispVal , you can also look at the Data.Data module, which provides some tools for reflecting data types. I doubt that really is what you want.


EDIT : Just a little reflection, here are some examples of extraction functions you can write:

  • Create functions to extract a single constructor, as in Don’s first example, suppose you already know which constructor was used:

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

    This will lead to runtime errors if applied to anything other than the Atom constructor, so be careful with this. In many cases, however, you know what at some point in the algorithm you have, so it can be used safely. A simple example would be if you have a LispVal list from which you filter out every other constructor.

  • Create safe single constructor highlight functions that serve as "Do I have this constructor?" predicate and "if yes, give me the contents" extractor:

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

    Note that this is more flexible than the higher, even if you are sure which constructor you have. For example, this simplifies the definition of:

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

    However, the fancy syntax is more useful for records with many fields, rather than with many single-field constructors, and safe extraction functions are generally better than errors that create errors.

  • Getting more abstract, sometimes the most convenient way is to have a single universal function of deconstruction:

     extractLispVal :: (String -> r) -> ([LispVal] -> r) -> ([LispVal] -> LispVal -> r) -> (Integer -> r) -> (String -> r) -> (Bool -> r) -> (Double -> r) -> LispVal -> r extractLispVal f _ _ _ _ _ _ (Atom x) = fx extractLispVal _ f _ _ _ _ _ (List xs) = f xs ... 

    Yes, it looks awful, I know. An example of this (for a simpler data type) in standard libraries is the maybe and either functions, which deconstruct the types of the same name. In fact, this is a function that confirms the conformity of templates and allows you to work with it more directly. It may be ugly, but you only need to write it once, and it may be useful in some situations. For example, here you can do one of the following functions:

     exprToString :: ([String] -> String) -> ([String] -> String -> String) -> LispVal -> String exprToString fg = extractLispVal id (f . map recur) (\xs x -> g (map recur xs) $ recur x) show show show show where recur = exprToString fg 

    ... i.e. A simple recursive print function, parameterized by how to combine list items. You can also write isAtom etc. Easy:

     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 a match with one or two constructors, with nested pattern matches and all the cases for constructors that you don't need. This is exactly what is best suited to match the pattern, and all of the above methods will simply complicate the situation. Therefore, do not tie yourself to one approach!

+8
source

You can always extract fields from your data type, or by matching patterns for individual constructors:

 extractLispValDouble (Double val) = val 

or using the record selector:

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

However, you cannot write a function that returns String or Bool or Double (and everything else) is naive, since you cannot write a type for this.

+6
source

You can get more or less what you want using GADT. It quickly gets scary, but it works :-) I have serious doubts about how far this approach will help you!

Here's something I quickly hacked, with a not quite right (a bit too many spaces) printLispVal function thrown in - I wrote this to see if you can really use my construct. Note that the template for extracting the base type is in the extractShowableLispVal function. I think that this approach will quickly encounter difficulties when you start doing more complex things, for example, trying to do arithmetic and so on.

 {-# 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 lx -> 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" )) 
+2
source

Source: https://habr.com/ru/post/888442/


All Articles