Как получить только определенный тип элементов из списка в 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 xs = [ x | DbDate x <- xs ]

есть несколько хороших ответов, но я хотел бы добавить еще один подход, как вы можете найти решения таких задач.

во-первых,напишите самое простое возможное решение, т. е. решение прямой рекурсии.

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate ((DbDate time):items) = time:(filterDbDate items)
filterDbDate ( _           :items) =       filterDbDate items

это помогает понять соответствующие структуры и позволяет ознакомиться с фактическими шагами, необходимыми. Это не самая эффективная версия, но ее легко написать, и ее часто достаточно для выполнения задачи на рука.

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

  1. определить тип аккумулятора. Это часто также тип возврата; в этом случае, список. Это дает вам новые первые строки

    filterDbDate :: [DatabaseItem] -> [UTCTime]
    filterDbDate = go []
      where ...
    
  2. Теперь возьмите исходную функцию и превратите ее во внутреннюю go функция путем замены каждого рекурсивного вызова с аккумулятор, а затем положить результат в рекурсивный вызов go.

        go acc ((DbDate time):items) = go (time:acc) items
        go acc ( _           :items) = go       acc  items
    
  3. добавить обработку конечного случая. Остерегайтесь, что порядок операций будет изменен.

        go acc  []                   = reverse acc
    
  4. переместите обработку конечного случая в исходный вызов. Это не обязательно, если вы хотите остановиться здесь, но это помогает на пути к раза.

    filterDbDate = reverse . go []
      where 
        go acc  [] = acc
        ...
    

теперь превратите это в складку. Аккумулятор тот же самый, который будет использовать складка, и преобразование, опять же, почти механическое.

  1. замените вызов go с вызовом в фолд.

    filterDbDate :: [DatabaseItem] -> [UTCTime]
    filterDbDate = reverse . foldl f []
    
  2. поворот go на f удалив список-часть совпадений шаблона, конечный регистр и рекурсивные вызовы.

      where f acc (DbDate time) = time:acc
            f acc  _            =      acc
    
  3. задумайтесь, если было бы лучше изменить направление рекурсия.

    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!