Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Dependent optional" data in Haskell

Consider a DateTime type where the date must be present, but the time part in seconds is optional. If the time part is there, there might be an optional milliseconds part, too. If milliseconds are present, there might be a nanoseconds part, too.

There are many ways to deal with this, e.g.:

--rely on smart constructors 
data DateTime = DateTime { days:: Int, 
                           sec :: Maybe Int, 
                           ms :: Maybe Int, 
                           ns :: Maybe Int 
                         }

-- list all possibilities
data DateTime = DateOnly Int 
              | DateWithSec Int Int
              | DateWithMilliSec Int Int Int
              | DateWithNanoSec Int Int Int Int    

-- cascaded Maybe
data DateTime = DateTime Int (Maybe (Int, Maybe (Int, Maybe Int)))

-- cascaded data
data Nano = NoNano | Nano Int
data MilliSec = NoMilliSec | MilliSec Int Nano
data Sec = NoSec | Sec Int MilliSec
data Date = Date Int Sec

Which construct would you use (of course not limited to the examples above), and why?

[Intentions]

I'm exploring the possibilities for a date type in Frege ( http://code.google.com/p/frege/ ), using date4j's DateTime as a guide line (as Haskell's date and time lib is way too complicated, and java.util.Date too broken). In my current toy implementation all fields are mandatory, but of course it would be nice to free the user from unwanted precision (and the original implementation has optional fields).

So the main goals are:

  • safety: Illegal states must be avoided at all costs
  • convenience: It should be easy to work with the type, e.g. pattern matching would be cool, calendar calculations should be easy...

Not so important are:

  • performance: Of course working with the type shouldn't be too slow, but for the typical usage it doesn't have to sqeeze out the last clock cycle
  • memory: In cases where this really matters, it would be easy to derive a more compact storage format
  • terse implementation: It's a library, and I'm willing to add all the code needed to make things smooth

That said, all of this is very tentative and shouldn't be taken too serious.

like image 518
Landei Avatar asked Apr 13 '12 06:04

Landei


2 Answers

(This isn't an answer, but it's too long for a comment and will be clearer here.)

There is another way that you could handle it: have a single DateTime type that stores all fields always along with a parameter representing the precision, e.g.

data Precision = Days | Seconds | Milliseconds | Nanoseconds deriving (Ord, Eq {- etc -})
data DateTime = DateTime { prec :: Precision,
                           days :: Int, 
                           sec :: Int,
                           ms :: Int,
                           ns :: Int }

And use smart constructors that set the unused parameters to 0. If you have dateDifference or whatever, you can propagate the precision through (the Ord instance would make this neat).

(I've got little idea about how good/Haskell-y this is, but the other solutions seem quite messy, maybe this is more elegant.)

like image 126
huon Avatar answered Oct 05 '22 01:10

huon


“Illegal states must be avoided at all costs” and “pattern matching would be cool” are fine principles that in this case are in direct conflict with each other.

In addition, dates and times are gnarly human cultural constructs with lots of edge cases and irregular corners. They are not the sort of rules we can easily encode in the type system.

So in this case I would go with an opaque data type, smart constructors, and smart deconstructors. There's always view patterns and pattern guards for occasions when we want to use pattern matching.

(And I haven't even discussed dependent optional data as a motivating factor.)

like image 22
dave4420 Avatar answered Oct 05 '22 01:10

dave4420