Как получить только определенный тип элементов из списка в Haskell?
Я учусь Хаскелл Книги, и в главе 10 (складные списки) я пытаюсь решить упражнение, касающееся получения только одного конкретного типа элемента из списка, который включает в себя различные типы элементов.
авторы дают следующий код:
import Data.Time
data DatabaseItem = DbString String
| DbNumber Integer
| DbDate UTCTime
deriving (Eq, Ord, Show)
theDatabase :: [DatabaseItem]
theDatabase = [ DbDate (UTCTime
(fromGregorian 1911 5 1)
(secondsToDiffTime 34123))
, DbNumber 9001
, DbString "Hello, world!"
, DbDate (UTCTime
(fromGregorian 1921 5 1)
(secondsToDiffTime 34123))
]
и первый вопрос:
напишите функцию, которая фильтрует значения DbDate и возвращает список значения UTCTime внутри их.
filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate = undefined
так как глава Все о складных списков, я предполагаю, что это может быть сделано с помощью, например,foldr.
моей первоначальной попыткой было сначала написать некоторые вспомогательные функции и использовать их в foldr, например:
getDbDate1 :: DatabaseItem -> UTCTime
getDbDate1 (DbDate utcTime) = utcTime
isDbDate :: DatabaseItem -> Bool
isDbDate (DbDate _) = True
isDbDate _ = False
filterDbDate1 :: [DatabaseItem] -> [UTCTime]
filterDbDate1 database = foldr ((:) . getDbDate1) [] (filter isDbDate database)
это, кажется, делает работу, потому что:
λ> filterDbDate1 theDatabase
[1911-05-01 09:28:43 UTC,1921-05-01 09:28:43 UTC]
но мне не нравится это решение, потому что, прежде всего, оно дает следующее предупреждение:
/Users/emre/code/haskell/chapter10_folding_lists/database.hs:36:1: Warning: …
Pattern match(es) are non-exhaustive
In an equation for ‘getDbDate1’:
Patterns not matched:
DbString _
DbNumber _
и я использование двух вспомогательных функций, одна для фильтрации значений, которые не являются DbDate, другая для получения UTCTime компонент.
Итак, чтобы избавиться от неисчерпывающего предупреждения о сопоставлении шаблонов и использовать одну вспомогательную функцию, я решил написать ее так:
getDbDate2 :: DatabaseItem -> Maybe UTCTime
getDbDate2 (DbDate utcTime) = Just utcTime
getDbDate2 _ = Nothing
filterDbDate2 :: [DatabaseItem] -> [UTCTime]
filterDbDate2 database = foldr ((:) . getDbDate2) [] database
но, конечно, вышеприведенное не компилируется, потому что оно не набирает check, потому что, например:
λ> foldr ((:) . getDbDate2) [] theDatabase
[Just 1911-05-01 09:28:43 UTC,Nothing,Nothing,Just 1921-05-01 09:28:43 UTC]
другими словами, он может вернуть список Just UTCTime значения, вместе с Nothing ценностей, а не только список UTCTime значения.
Итак, мой вопрос: как я могу написать (помощник?) функция, которая, на одном дыхании (так что мне не нужно использовать filter), проверяет, является ли его значение DbNumber, и если да, то возвращает UTCTime компонент? (А если нет... он также должен что-то вернуть (например,Nothing?), и вот где у меня проблемы, то есть использование Maybe UTCTime, и Just UTCTime ценностей и т. д.)
5 ответов
здесь есть несколько других ответов, охватывающих хорошие предложения о другое способы думать о проблеме: использование catMaybes чтобы munge данные во второй раз после выбора Maybe UTCTimes; использование понимания списка и удобного синтаксиса, который они имеют для фильтрации несоответствующих шаблонов; использование монадической структуры списков для включения или пропуска результатов; и написание индивидуальной рекурсивной функции. В этом ответе я обращусь к вашему прямому вопросу, показывая, как использовать структура программы у вас уже есть без полного переосмысления вашего подхода к манипуляциям со списком -- calling foldr С вспомогательной функцией, которая делает все, что вам нужно за один раз.
прежде всего, я замечаю, что все существующие попытки отправить foldr функция, которая безоговорочно называет (:):
foldr ((:) . getDbDate1) [] (filter isDbDate database)
foldr ((:) . getDbDate2) [] database
дело в этом шаблоне в том, что это означает, что список, который вы получаете из foldr будет иметь ту же длину, что и функция, которую вы передаете -- с каждым (:) во входном списке превращается в (:) в списке вывода. В первом решении вы справились с этим, удалив некоторые записи, которые вас не интересовали, из входного списка; во втором решении вы справились с этим, имея дополнительные неинтересные элементы в выходном списке.
третье решение-посмотреть элемент списка, прежде чем решать, следует ли вызывать (:) или нет. Вот как это можно сделать:
conditionalCons :: DatabaseItem -> [UTCTime] -> [UTCTime]
conditionalCons (DbDate t) ts = t:ts
conditionalCons _ ts = ts
внимание, в частности, что во втором предложении мы не называем (:) -- это отфильтровывает несоответствующие элементы списка. Мы также не беспокоимся о пропавших образцах. Теперь мы можем писать
filterDbDate3 :: [DatabaseItem] -> [UTCTime]
filterDbDate3 = foldr conditionalCons []
тестирование в ghci:
> filterDbDate3 theDatabase
[1911-05-01 09:28:43 UTC,1921-05-01 09:28:43 UTC]
идеально!
есть несколько хороших ответов, но я хотел бы добавить еще один подход, как вы можете найти решения таких задач.
во-первых,напишите самое простое возможное решение, т. е. решение прямой рекурсии.
filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate ((DbDate time):items) = time:(filterDbDate items)
filterDbDate ( _ :items) = filterDbDate items
это помогает понять соответствующие структуры и позволяет ознакомиться с фактическими шагами, необходимыми. Это не самая эффективная версия, но ее легко написать, и ее часто достаточно для выполнения задачи на рука.
следующим шагом будет сделать код более эффективным с хвостовой рекурсией. Это простая, почти механическая трансформация.
-
определить тип аккумулятора. Это часто также тип возврата; в этом случае, список. Это дает вам новые первые строки
filterDbDate :: [DatabaseItem] -> [UTCTime] filterDbDate = go [] where ... -
Теперь возьмите исходную функцию и превратите ее во внутреннюю
goфункция путем замены каждого рекурсивного вызова с аккумулятор, а затем положить результат в рекурсивный вызовgo.go acc ((DbDate time):items) = go (time:acc) items go acc ( _ :items) = go acc items -
добавить обработку конечного случая. Остерегайтесь, что порядок операций будет изменен.
go acc [] = reverse acc -
переместите обработку конечного случая в исходный вызов. Это не обязательно, если вы хотите остановиться здесь, но это помогает на пути к раза.
filterDbDate = reverse . go [] where go acc [] = acc ...
теперь превратите это в складку. Аккумулятор тот же самый, который будет использовать складка, и преобразование, опять же, почти механическое.
-
замените вызов
goс вызовом в фолд.filterDbDate :: [DatabaseItem] -> [UTCTime] filterDbDate = reverse . foldl f [] -
поворот
goнаfудалив список-часть совпадений шаблона, конечный регистр и рекурсивные вызовы.where f acc (DbDate time) = time:acc f acc _ = acc -
задумайтесь, если было бы лучше изменить направление рекурсия.
filterDbDate :: [DatabaseItem] -> [UTCTime] filterDbDate = foldr f [] where f (DbDate time) = (time:) f _ = id
теперь для окончательной очистки, дополнительные очки домового и чтобы разозлить учителей Haskell, сделайте это как можно более общим, не разбивая вещи.
{-# LANGUAGE NoImplicitPrelude, GADTs #-}
import ClassyPrelude
filterDbDate :: ( MonoFoldable items, Element items ~ DatabaseItem
, Monoid times, SemiSequence times, Element times ~ UTCTime)
=> items -> times
filterDbDate = foldr f mempty
where f (DbDate time) = cons time
f _ = id
список-это монада. Таким образом, мы можем использовать функции Monad тип-класс.
utcTimes :: [UTCTime]
utcTimes =
theDatabase >>=
\ item ->
case item of
DbDate utcTime -> [utcTime]
_ -> []
на (>>=) функция здесь ключ. По сути, это то же самое, что и "flatMap" на других языках, если это звонит в колокол.
следующее То же самое, что и выше, выраженное в Do-notation:
utcTimes :: [UTCTime]
utcTimes =
do
item <- theDatabase
case item of
DbDate utcTime -> [utcTime]
_ -> []
фактически, мы можем даже обобщить это на функцию, которая будет работать для любой монады над UTCTime (ну, MonadPlus, на самом деле):
pickUTCTime :: MonadPlus m => DatabaseItem -> m UTCTime
pickUTCTime item =
case item of
DbDate utcTime -> return utcTime
_ -> mzero
utcTimes :: [UTCTime]
utcTimes =
theDatabase >>= pickUTCTime
простой способ сделать это следующим образом
filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate db = filterDbDate' [] db
where filterDbDate' :: [UTCTime] -> [DatabaseItem] -> [UTCTime]
filterDbDate' rest ((DbDate utcTime):xs) = filterDbDate' (rest ++ [utcTime]) xs
filterDbDate' rest (_:xs) = filterDbDate' rest xs
filterDbDate' rest _ = rest
то есть вы передаете другой аргумент, который содержит значения, которые вы хотите сохранить. Если вы посмотрите внимательно, вы увидите, что это именно то, что тип foldl указывает слишком foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b (вы также можете сделать это с помощью foldr, но я оставлю это вам), за исключением того, что он ожидает один элемент за раз. Так давайте перепишем filterDbDate' чтобы сделать это.
filterDbDate2 :: [DatabaseItem] -> [UTCTime]
filterDbDate2 db = foldl filterDbDate'' [] db
where filterDbDate'' :: [UTCTime] -> DatabaseItem -> [UTCTime]
filterDbDate'' rest (DbDate utcTime) = (rest ++ [utcTime])
filterDbDate'' rest _ = rest
это не самая эффективная функция, но, надеюсь, вы будете см. как перевести функции в использование складок. Попробуйте с foldr!