1
votes

In my journey exploring Idris, I am trying to write a small dates-handling module in an "idiomatic" way. Here is what I have so far.

First I have some basic types to represent days, months and years:

module Date 

import Data.Fin

Day : Type
Day = Fin 32

data Month : Type where
  January    : Month
  February   : Month
  .... 

toNat : Month -> Nat
toNat January    = 1
toNat February   = 2
... 

data Year : Type where
  Y : Integer -> Year

record Date where
  constructor MkDate
  day   : Day
  month : Month 
  year  : Year

I would like to implement a function addDays to add some number of days to a Date. To this end I defined the following auxiliary functions:

isLeapYear : Year -> Bool
isLeapYear (Y y) = 
  (((y `mod` 4) == 0) && ((y `mod` 100) /= 0)) || ((y `mod` 400) == 0) 

daysInMonth : Month -> Year -> Day
daysInMonth January _      = 31
daysInMonth February year  = if isLeapYear year then 29 else 28
daysInMonth March _        = 31
...

And finally try to define addDays as :

addDays : Date -> Integer -> Date
addDays (MkDate d m y) days =
  let maxDays = daysInMonth m y
      shiftedDays = finToInteger d + days
  in case integerToFin shiftedDays (finToNat maxDays) of  
          Nothing => ?hole_1
          Just x  => MkDate x m y

I am stuck with the very basic case where the added number of days fit in the current month's duration. Here is the output of compiler:

When checking right hand side of Date.case block in addDays at Date.idr:92:11 with expected type Date

 When checking argument day to constructor Date.MkDate:
         Type mismatch between
                 Fin (finToNat maxDays) (Type of x)
         and
                 Day (Expected type)

         Specifically:
                 Type mismatch between
                         finToNat maxDays
                 and
                         32

This is puzzling because the type of maxDays should be obviously Day which is simply Fin 32.

I suspect this might be related to non-totality of daysInMonth which stems from non-totality of isLeapYear which itself comes from non-totality of mod for Integer type.

1

1 Answers

1
votes

Well, this is not so trivial because Idris requires proofs from you on every step especially if you're using dependent types. All basic ideas are already written in this question:

Is there a way to define a consistent date in a dependent type language?

I'll comment your implementation and translate code in answer (it's probably Agda) to Idris. Also I made few adjustments to make your code total.

First, Month can be written a little bit simpler:

data Month = January
           | February
           | March

I'm not going to write all 12 month, it's just an example.

Second, Year type should store Nat instead of Integer because most functions that work with Integer aren't total. This is better:

data Year : Type where
    Y : Nat -> Year

It helps to make isLeapYear check total:

isLeapYear : Year -> Bool
isLeapYear (Y y) = check4 && check100 || check400
  where
    check4 : Bool
    check4 = modNatNZ y 4 SIsNotZ == 0

    check100 : Bool
    check100 = modNatNZ y 100 SIsNotZ /= 0

    check400 : Bool
    check400 = modNatNZ y 400 SIsNotZ == 0

Next, it's not good to make Day as a Fin 32. Better to specify day's number for each month specifically.

daysInMonth : Month -> Year -> Nat
daysInMonth January  _    = 31
daysInMonth February year = if isLeapYear year then 29 else 28
daysInMonth March    _    = 31

Day : Month -> Year -> Type
Day m y = Fin (daysInMonth m y)

And you should adjust a little your Date record:

record Date where
    constructor MkDate
    year  : Year
    month : Month
    day   : Day month year

Well, now about addDays. This function is actually very complex when you're working with dependent types. As you correctly noticed, you have several cases. For example: sum fits in the current month, sum goes to next month, sum skips several months, sum goes to year. And each such case need proof. If you want to ensure that sum fits in current month, you should provide a proof of that fact.

Before I move to code I want to warn you that writing even non-typed versions of date library is increadibly hard. Moreover, I suppose nobody haven't still tried to implement full-featured version in some language with dependent types. So my solution may be far from the best. But at least should give you some ideas about what you're doing wrong.

Then you can start with writing some function like this:

daysMax : (d: Date) -> Nat
daysMax (MkDate y m _) = daysInMonth m y

addDaysSameMonth : (d : Date)
                -> (n : Nat)
                -> Prelude.Nat.LT (finToNat (day d) + n) (daysMax d)
                -> Date
addDaysSameMonth d n x = ?addDaysSameMonth_rhs

Well, numeric operations with Fin are very limited according to current state of Data.Fin module. So you may have hard times even adding two Nats and converting them to Fin using given proof. Again, writing such stuff is harder than it seems :)

Here is the full sketch of code: http://lpaste.net/3314444314070745088