1

I am making my way through "Haskell Programming..." and, in Chapter 10, have been working with a toy database. The database is defined as:

data DatabaseItem = DBString String
                  | DBNumber Integer
                  | DBDate   UTCTime
                  deriving (Eq, Ord, Show)

and, given a database of the form [databaseItem], I am asked to write a function

dbNumberFilter :: [DatabaseItem] -> [Integer]

that takes a list of DatabaseItems, filters them for DBNumbers, and returns a list the of Integer values stored in them.

I solved that with:

dbNumberFilter db = foldr selectDBNumber [] db
  where
    selectDBNumber (DBNumber a) b = a : b
    selectDBNumber _ b          = b 

Obviously, I can write an almost identical to extract Strings or UTCTTimes, but I am wondering if there is a way to create a generic filter that can extract a list of Integers, Strings, by passing the filter a chosen data constructor. Something like:

dbGenericFilter :: (a -> DataBaseItem) -> [DatabaseItem] -> [a]
dbGenericFilter DBICon db = foldr selectDBDate [] db
  where
    selectDBDate (DBICon a) b = a : b
    selectDBDate _ b          = b 

where by passing DBString, DBNumber, or DBDate in the DBICon parameter, will return a list of Strings, Integers, or UTCTimes respectively.

I can't get the above, or any variation of it that I can think of, to work. But is there a way of achieving this effect?

1
  • The two big problems: 1) the three constructors all have different types and 2) the return type of dbGenericFilter would depend on the value (not the type) of its first argument. The first problem could be overcome using RankNTypes, but the second one I think requires some level of support for dependent types. Commented Jul 3, 2021 at 21:56

1 Answer 1

4

You can't write a function so generic that it just takes a constructor as its first argument and then does what you want. Pattern matches are not first class in Haskell - you can't pass them around as arguments. But there are things you could do to write this more simply.

One approach that isn't really any more generic, but is certainly shorter, is to make use of the fact that a failed pattern match in a list comprehension skips the item:

dbNumberFilter db = [n | DBNumber n <- db]

If you prefer to write something generic, such that dbNUmberFilter = genericFilter x for some x, you can extract the concept of "try to match a DBNumber" into a function:

import Data.Maybe (mapMaybe)

genericFilter :: (DatabaseItem -> Maybe a) -> [DatabaseItem] -> [a]
genericFilter = mapMaybe

dbNumberFilter = genericFilter getNumber
  where getNumber (DBNumber n) = Just n
        getNumber _ = Nothing

Another somewhat relevant generic thing you could do would be to define the catamorphism for your type, which is a way of abstracting all possible pattern matches for your type into a single function:

dbCata :: (String -> a) 
       -> (Integer -> a) 
       -> (UTCTime -> a) 
       -> DatabaseItem -> a
dbCata s i t (DBString x) = s x
dbCata s i t (DBNumber x) = i x
dbCata s i t (DBDate x) = t x

Then you can write dbNumberFilter with three function arguments instead of a pattern match:

dbNumberFilter :: [DatabaseItem] -> [Integer]
dbNumberFilter = (>>= dbCata mempty pure mempty)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.