Как получить только определенный тип элементов из списка в 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 UTCTime
s; использование понимания списка и удобного синтаксиса, который они имеют для фильтрации несоответствующих шаблонов; использование монадической структуры списков для включения или пропуска результатов; и написание индивидуальной рекурсивной функции. В этом ответе я обращусь к вашему прямому вопросу, показывая, как использовать структура программы у вас уже есть без полного переосмысления вашего подхода к манипуляциям со списком -- 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
!