К сожалению, такого рода общее сопоставление для конструкторов не возможно напрямую, но даже если бы оно было вашим, оно не сработало бы - функция extractLispVal
не имеет четко определенного типа, потому что тип результата зависит от значения ввода. Существуют различные виды продвинутой бессмысленной системы типов, которые могут делать что-то вроде этого, но на самом деле это не то, что вы бы хотели использовать здесь.
В вашем случае, если вы заинтересованы только в извлечении определенных типов значений или если вы можете преобразовать их в один тип, вы можете написать функцию, например extractStringsAndAtoms :: LispVal -> Maybe String
.
Единственный способ вернуть один из нескольких возможных типов - это объединить их в тип данных и сопоставление с ним по шаблону - общая форма этого существа Either a b
, который либо a
, либо b
отличается конструкторы. Вы можете создать тип данных, который позволит извлекать все возможные типы ... и он будет почти таким же, как и сам LispVal
, так что это бесполезно.
Создание функций извлечения с одним конструктором, как в первом примере Дона, предполагающих, что вы уже знаете, какой конструктор использовался:
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
С другой стороны, иногда то, что вы хотите сделать, - это сопоставить один или два конструктора с вложенными совпадениями с образцом и универсальным случаем для конструкторов, которые вас не интересуют.Это именно то, что лучше всего подходит для сопоставления с образцом, и все вышеперечисленные методы только усложнят ситуацию.Так что не привязывайте себя к одному подходу!