5
\$\begingroup\$

I am working through Chapter 10 of Haskell Programming from First Principles.

I've gotten the code to work, but I am uneasy about the fact that a few of my functions - namely, extractUTCTime and extractInteger - are not total functions. There is valid input for which those functions are bottom. I've added the code below, but I was wondering what strategies are available for making extractUTCTime and extractInteger safe. (One strategy that I do know of is changing the type signature of these functions so that they return a value of type Maybe: extractUTCTime :: DatabaseItem -> Maybe UTCTime. I was wondering if there was anything else that can be done).

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))]

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate [] = []
filterDbDate dataBase = [extractUTCTime item | item <- dataBase, isDbDate item]

isDbDate :: DatabaseItem -> Bool
isDbDate (DbDate _) = True
isDbDate _          = False

extractUTCTime :: DatabaseItem -> UTCTime
extractUTCTime (DbDate utcData) = utcData

filterDbNumber :: [DatabaseItem] -> [Integer]
filterDbNumber [] = []
filterDbNumber dataBase = [extractInteger item | item <- dataBase, isDbNumber item]

extractInteger :: DatabaseItem -> Integer
extractInteger (DbNumber int) = int

isDbNumber :: DatabaseItem -> Bool
isDbNumber (DbNumber _) = True
isDbNumber _ = False

mostRecent :: [DatabaseItem] -> UTCTime
mostRecent dataBase = foldr max y ys
                where dates = filterDbDate dataBase
                      y     = head dates
                      ys    = tail dates

sumDb :: [DatabaseItem] -> Integer
sumDb dataBase = foldr (+) 0 dbNumbers
                 where dbNumbers = filterDbNumber dataBase

avgDb :: [DatabaseItem] -> Double
avgDb dataBase = dividend / divisor
                 where dividend = fromIntegral $ sumDb dataBase
                       divisor = fromIntegral $ length $ filterDbNumber dataBase
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I was wondering if there was anything else that can be done

Well, morally, no. You can either return a nullable value (Maybe UTCTime) or lie (read "1970-01-01 00:00:00 UTC" :: UTCTime) but you can't turn a DbNumber 1 :: DatabaseItem into a UTCTime both unconditionally and truthfully.

The good news is, if you hit on the right set of higher-order functions you don't need to. Maybe values are very easy to work with when you know the extra tools available in Data.Maybe.

Taking as a whole this set of functions related to dates—

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate [] = []
filterDbDate dataBase = [extractUTCTime item | item <- dataBase, isDbDate item]

isDbDate :: DatabaseItem -> Bool
isDbDate (DbDate _) = True
isDbDate _          = False

extractUTCTime :: DatabaseItem -> UTCTime
extractUTCTime (DbDate utcData) = utcData

mostRecent :: [DatabaseItem] -> UTCTime
mostRecent dataBase = foldr max y ys
                where dates = filterDbDate dataBase
                      y     = head dates
                      ys    = tail dates

We'll begin by throwing out isDbDate and extractUTCTime and writing the DatabaseItem -> Maybe UTCTime function you avoided.

dbDate :: DatabaseItem -> Maybe UTCTime
dbDate (DbDate utc) = Just utc
dbDate _            = Nothing

Now pulling from Data.Maybe, we'll replace filterDbDate with an equivalent code fragment that utilizes mapMaybe.

-- filterDbDate :: [DatabaseItem] -> [UTCTime]
-- filterDbDate db =
     mapMaybe dbDate db

We can use that definition in mostRecent, a function from the standard Prelude, and point free style to achieve a one liner that more than just being clever, emphasizes the power of abstraction in producing concise code that will be instantly comprehensible to other Haskell programmers.

mostRecent :: [DatabaseItem] -> UTCTime
mostRecent = maximum . mapMaybe dbDate

mostRecent still commits the sin of using a partial function though, maximum throws an exception (much like your use of head and tail did before) when the filtered list is empty. Until such time as partial functions are excised from base (and O, do we all lust for and fear such a tumultuous day) you should familiarize yourself with the safe package, which has no dependencies besides base, and provides many alternatives to the partial functions we all know and love.

import Safe.Foldable (maximumMay)

mostRecent :: [DatabaseItem] -> Maybe UTCTime
mostRecent = maximumMay . mapMaybe dbDate
\$\endgroup\$
1
  • \$\begingroup\$ Rowan - thank you very much for such a great explanation. As a beginner, I haven't fully groked the code, but I'll be coming back to it when we get to that part of the book. I also really appreciate your first paragraph. Thank you! \$\endgroup\$ Commented Nov 8, 2016 at 11:58

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.