0
votes

In an attempt to find a quick and easy way to add different time periods, it occurred to me that I could declare them instances of Semigroup and Monoid. Are the following instantiations valid?

data TimeDuration = S Int | M Int | H Int deriving (Show, Eq)

instance Semigroup TimeDuration where
    S n1 <> S n2 = S (n1 + n2)
    M n1 <> M n2 = S (60 * (n1 + n2))
    H n1 <> H n2 = S (3600 * (n1 + n2))
    S n1 <> M n2 = S (n1 + 60 * n2)
    S n1 <> H n2 = S (n1 + 3600 * n2)
    M n1 <> S n2 = S (60 * n1 + n2)
    M n1 <> H n2 = S (60 * n1 + 3600 * n2)
    H n1 <> S n2 = S (3600 * n1 + n2)
    H n1 <> M n2 = S (3600 * n1 + 60 * n2)

instance Monoid TimeDuration where
    mempty = S 0

Example: mconcat [S 1, M 2, M 3, S 2, H 1] == S 3903

User leftaroundabout ask me to produce more significative example. So this is a new implementation and some examples that I hope will show better the possible variety of the results of the operation (<>)

instance Semigroup TimeDuration where
    S n1 <> S n2 = S (n1 + n2)
    M n1 <> M n2 = M (n1 + n2)
    H n1 <> H n2 = H (n1 + n2)
    S n1 <> M n2 = S (n1 + 60 * n2)
    M n1 <> S n2 = S n2 <> M n1
    S n1 <> H n2 = S (n1 + 3600 * n2)
    H n1 <> S n2 = S n2 <> H n1
    M n1 <> H n2 = M (n1 + 60 * n2)
    H n1 <> M n2 = M n1 <> H n1 


instance Monoid TimeDuration where
    mempty = S 0
    mconcat [] = S 0
    mconcat xs = foldr1 (\y acc -> y <> acc) xs

-- ex. mconcat [M 2, M 3] == M 5
-- ex. mconcat [H 2, H 3] == H 5
-- ex. mconcat [M 2, M 3, S 1] == S 301
-- ex. mconcat [H 2, H 3, M 1] == M 301
-- ex. mconcat [H 2, H 3, S 1] == S 18001
3

3 Answers

3
votes

Yeah, that's fine. It's basically equivalent to

{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}

import Data.AdditiveGroup
import GHC.Generics

newtype TimeDuration = TimeDuration {getDurationInSeconds :: Integer}
  deriving (Generic, AdditiveGroup)

...with extra constructors for the special cases TimeDuration 60, TimeDuration 180... TimeDuration 3600 etc. (which in your construction are actually redundant; you could better make simple builder-functions to do the same job).

The AdditiveGroup typeclass is a specialised monoid typeclass, that avoids any confusion that it might be a multiplication monoid instead – which wouldn't actually make any sense because dimension-wise, time × time doesn't match time, but for dimensionless number types like Int or Double the multiplication monoid is just as sensible as the addition one.

Because these still have an unambiguous AdditiveGroup instance, the AdditiveGroup TimeDuration instance can just be derived. It could also be written by hand

instance AdditiveGroup TimeDuration where
  zeroV = TimeDuration 0
  TimeDuration δt₀ ^+^ TimeDuration δt₁ = TimeDuration $ δt₀ + δt₁

One particularly nice thing about AdditiveGroup: it's a precursor to VectorSpace which gives you a multiplication that does make sense, namely

instance VectorSpace TimeDuration where
  type Scalar TimeDuration = Integer
  factor *^ TimeDuration δt = TimeDuration $ factor * δt
2
votes

If we are writing an instance for a class with laws, the first sanity check should, in general, be whether the instance follows them. For Semigroup, there is the associativity law...

(x <> y) <> z = x <> (y <> z)

... while Monoid adds the identity laws:

mempty <> x = x
x <> mempty = x

Your instance follows the laws as long as only S values are involved (as it boils down to adding seconds). However, the identity laws are broken once the other constructors are brought into play, as in:

mempty <> H 2 = S 7200

We might be tempted to argue that H 2 and S 7200 are morally the same, and the difference between them is merely a presentation issue (cf. how show . read is, strictly speaking, not id as it normalises formatting). The question, then, would become why are H and M necessary if they aren't meant to be used for any relevant distinctions (cf. the other answers).

1
votes

This looks ok but why not just do this?

newtype TimeDuration = Seconds (Sum Integer) deriving (Show, Eq, Monoid, Semigroup)

I think you need extensions to derive semigroup and monoid through the newtype, and you need to import Sum from Data.Foldable

You can have functions for the rest:

seconds = Seconds
minutes = seconds . (*60)
hours = minutes . (*60)

Another thing you might want would be:

data TimDuration = Duration { seconds :: Int, minutes :: Int, hours :: Integer }

And define a function to normalise this. But here’s a question to demonstrate that actually adding durations and times is difficult: is adding a minute the same as adding 60s? One would think so but what if say the time is 23:59:00 and the last minute has a leap second? In such a case if you add 60 seconds you get to 23:59:60. If you add a minute should you get 23:59:60 or 00:00:00 for the next day? In this case I think saying a minute is 60s is sensible. But how long is a day or a month? What happens if you add a day to 12:00 the day before the clocks change. Should you get 12:00 the next day or 13:00/11:00? And if you add a month to 29 January should you get 28 February or 1 March? And what about on leap years? Time is hard once things aren’t always seconds.