Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define a lambda function that filters list based on subtype of a sum type?

The example is taken from a "Haskell programming from first principles" The goal of filter function is get rid of all the objects except those of 'DbDate' type.

On somone's github I found a way to filter sum types with list comprehension and pattern matching(1). Now I am trying to find a way to redefine this filter with a lambda function(2) or normal "case of" of "if then" function. I do not know how to properly check the type of arguments of a function when I deal with custom data type.

Book doesn't introduce the reader to any super specific library functions, just standard maps, folds, filters and other stuff you'd find in prelude.

import Data.Time

data DatabaseItem = DbString String
                  | DbNumber Integer
                  | DbDate   UTCTime
                  deriving (Eq, Ord, Show)

--List that needs to be filtered
theDatabase :: [DatabaseItem]
theDatabase =
  [ DbDate (UTCTime (fromGregorian 1911 5 1)
                    (secondsToDiffTime 34123))
  , DbNumber 9001
  , DbString "Hello, world!"
  , DbDate (UTCTime (fromGregorian 1921 5 1)
                    (secondsToDiffTime 34123))
  ]



--1 works fine, found on someone's git hub
filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate dbes = [x | (DbDate x) <- dbes]

--2 Looking for the eqivalents with lambda or "case" or "if then"
--pattern is not satisfactory

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate dbes = filter (\(DbDate x) -> True) theDatabase
like image 308
Adaskos07 Avatar asked Feb 27 '19 20:02

Adaskos07


3 Answers

filter has the type (a -> Bool) -> [a] -> [a] so it is not able to change the type of your list.

According to The Haskell 98 Report (section 3.11) the list comprehension used in the code you found on github desugars to:

filterDbDate2 :: [DatabaseItem] -> [UTCTime]
filterDbDate2 dbes = let extractTime (DbDate time) = [time]
                         extractTime _             = []
                     in concatMap extractTime theDatabase

You can rewrite extractTime to use case ... of:

filterDbDate3 :: [DatabaseItem] -> [UTCTime]
filterDbDate3 dbes = let extractTime item = case item of (DbDate time) -> [time]
                                                         _             -> []
                     in concatMap extractTime theDatabase

And replace it by a lambda:

filterDbDate4 :: [DatabaseItem] -> [UTCTime]
filterDbDate4 dbes = concatMap (\item -> 
    case item of 
        (DbDate time) -> [time]
        _             -> []) 
    theDatabase

But imho your original solution using list comprehension looks the best:

filterDbDate dbes = [x | (DbDate x) <- dbes]
like image 70
Niko Avatar answered Oct 04 '22 21:10

Niko


As @Niko has already said in his answer, filter cannot change the type. However, there is a variant of filter which can: Data.Maybe.mapMaybe :: (a -> Maybe b) -> [a] -> [b]. The idea is that if you want to keep an element, then you return Just newvalue from the lambda; otherwise you return Nothing. In that case, you could rewrite filterDbDate as:

import Data.Maybe

filterDbDate dbes = mapMaybe (\x -> case x of { DBDate d -> Just d; _ -> Nothing }) dbes

Personally, I would say that this is the second-clearest way to write this function (after the list comprehension method).

like image 40
bradrn Avatar answered Oct 04 '22 21:10

bradrn


You were indeed on the right track, as pattern matching is an easy way of solving this, however you will get error as your pattern-matching is not comprehensive. Also, note that if you use filter, you will still get a list of [DatabaseItem] as filter never changes the type. You can however use map to do it. So:

Case Of

You can have a case .. of inside your lambda function:

filterDbDate' :: [DatabaseItem] -> [UTCTime]
filterDbDate' = map (\(DbDate x) -> x) .filter (\x ->
  case x of
    DbDate x -> True
    _        -> False)

Recursion + Pattern Matching

However I think it's more clear using a recursion:

filterDbDate'' :: [DatabaseItem] -> [UTCTime]
filterDbDate'' [] = []
filterDbDate'' ((DbDate d):ds) = d : filterDbDate ds
filterDbDate'' (_:ds)          =     filterDbDate ds

Best Way

To be honest, when you have to mix up filter and map, and your lambdas are easy like this one, list comprehensions like yours are the cleanest way:

filterDbDate ds = [d | (DbDate d) <- ds]
like image 30
Lorenzo Avatar answered Oct 04 '22 22:10

Lorenzo