Вопрос по типу и шаблону Haskell: извлечение полей из типа данных - PullRequest
6 голосов
/ 18 мая 2011

Я новичок в 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, если мне нужно использовать базовый тип.

Спасибо!Simon

Ответы [ 3 ]

8 голосов
/ 18 мая 2011

К сожалению, такого рода общее сопоставление для конструкторов не возможно напрямую, но даже если бы оно было вашим, оно не сработало бы - функция extractLispVal не имеет четко определенного типа, потому что тип результата зависит от значения ввода. Существуют различные виды продвинутой бессмысленной системы типов, которые могут делать что-то вроде этого, но на самом деле это не то, что вы бы хотели использовать здесь.

В вашем случае, если вы заинтересованы только в извлечении определенных типов значений или если вы можете преобразовать их в один тип, вы можете написать функцию, например extractStringsAndAtoms :: LispVal -> Maybe String.

Единственный способ вернуть один из нескольких возможных типов - это объединить их в тип данных и сопоставление с ним по шаблону - общая форма этого существа Either a b, который либо a, либо b отличается конструкторы. Вы можете создать тип данных, который позволит извлекать все возможные типы ... и он будет почти таким же, как и сам LispVal, так что это бесполезно.

Если вы действительно хотите работать с различными типами вне LispVal, вы также можете взглянуть на модуль Data.Data, который предоставляет некоторые средства для отражения типов данных. Я сомневаюсь, что это действительно то, что вы хотите.


РЕДАКТИРОВАТЬ : Просто, чтобы немного подробнее разобраться, вот несколько примеров функций извлечения, которые вы можете написать:

  • Создание функций извлечения с одним конструктором, как в первом примере Дона, предполагающих, что вы уже знаете, какой конструктор использовался:

    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
    
  • Используйте синтаксис записи при определении типа, как во втором примере Дона. Это немного языковой магии, по большей части, определяет набор частичных функций, таких как первый 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
    ...
    

    Да, это выглядит ужасно, я знаю. Примером этого (для более простого типа данных) в стандартных библиотеках являются функции maybe и either, которые деконструируют типы с одинаковыми именами. По сути, это функция, которая устанавливает соответствие шаблону и позволяет вам работать с этим более напрямую. Это может быть ужасно, но вы должны написать это только один раз, и это может быть полезно в некоторых ситуациях. Например, вот одна вещь, которую вы можете сделать с помощью вышеуказанной функции:

    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
    
  • С другой стороны, иногда то, что вы хотите сделать, - это сопоставить один или два конструктора с вложенными совпадениями с образцом и универсальным случаем для конструкторов, которые вас не интересуют.Это именно то, что лучше всего подходит для сопоставления с образцом, и все вышеперечисленные методы только усложнят ситуацию.Так что не привязывайте себя к одному подходу!

6 голосов
/ 18 мая 2011

Вы всегда можете извлечь поля из вашего типа данных либо путем сопоставления с шаблоном на отдельных конструкторах :

extractLispValDouble (Double val) = val

или с помощью селектора записей:

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

Однако вы не можете написать функцию, которая наивно возвращает String, Bool или Double (и все остальное), поскольку вы не можете написать тип для этого.

2 голосов
/ 18 мая 2011

Вы можете получить более или менее то, что вы хотите, используя 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" ))
...