time-interval-0.1.0.0: src/Data/Time/Interval.hs
{- This file is part of time-interval.
-
- Written in 2015 by fr33domlover <fr33domlover@rel4tion.org>.
-
- ♡ Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
-- | Suppose you have a program which periodically reloads state and saves some
-- logs, and you'd like the intervals for these periodic actions to be
-- expressed using time units, and abstract away the internal representation
-- until the site of actual use. Your code may look like this:
--
-- > data AppState tr ts = AppState
-- > { userName :: String
-- > , newMessages :: [String]
-- > , reloadInterval :: tr
-- > , saveInterval :: ts
-- > }
-- >
-- > type App tr ts = StateT (AppState tr ts) IO
-- >
-- > setReloadInterval :: TimeUnit t => t -> App ()
-- > setReloadInterval new = modify $ \ s -> s { reloadInterval = new }
-- >
-- > syncStateWithFiles :: (TimeUnit tr, TimeUnit ts) => App tr ts ()
-- > syncStateWithFiles = do
-- > reloadMicrosec <- liftM toMicroseconds $ gets reloadInterval
-- > saveMicrosec <- liftM toMicroseconds $ gets saveInterval
-- > {- ... use the values ... -}
--
-- And usage looks like this:
--
-- > setReloadInterval (5 :: Second)
--
-- So every time you add, change or remove a time field, all your type
-- signatures need to be updated and all your @App@ actions, even unrelated to
-- those times, are stuck with time type parameters in their type signatures.
--
-- An easy and common approach to avoid all the mess is to store the time as an
-- integer, i.e. applying 'toMicroseconds' when setting the value rather than
-- when using it. That makes things much easier, but then your type (and your
-- API) expose the time directly as a number of microseconds, and people end up
-- writing things like @1000 * 1000 * 60 * 5@ to say "5 minuts". It's also an
-- internal technical detail there's no reason to expose, and should be
-- possible to change without breaking anything - e.g. what if your scheduling
-- tool one day moves to a higher precision than microseconds? Your high-level
-- API would ideally not let that change float all the way up.
--
-- Here's how things can work when using this library.
--
-- > data AppState = AppState
-- > { userName :: String
-- > , newMessages :: [String]
-- > , reloadInterval :: TimeInterval
-- > , saveInterval :: TimeInterval
-- > }
-- >
-- > type App = StateT AppState IO
-- >
-- > setReloadInterval :: TimeUnit t => t -> App ()
-- > setReloadInterval new = modify $ \ s -> s { reloadInterval = time new }
-- >
-- > syncStateWithFiles :: App ()
-- > syncStateWithFiles = do
-- > reloadMicrosec <- liftM microseconds $ gets reloadInterval
-- > saveMicrosec <- liftM microseconds $ gets saveInterval
-- > {- ... use the values ... -}
--
-- And usage looks the same:
--
-- > setReloadInterval (5 :: Second)
--
-- Also, even if you let the user use the 'time' function in their code, e.g.
-- like this:
--
-- > setReloadInterval $ time (5 :: Second)
--
-- ... you stil get the advantages from both worlds.
module Data.Time.Interval
( TimeInterval ()
, time
, microseconds
)
where
import Data.Time.Units (TimeUnit (..))
-- | A time duration.
newtype TimeInterval = TimeInterval Integer deriving (Eq, Ord, Show)
-- | Convert a time value expressed in a some time unit into a 'TimeInterval'.
time :: TimeUnit t => t -> TimeInterval
time = TimeInterval . toMicroseconds
-- | Express a 'TimeInterval' in microseconds.
microseconds :: TimeInterval -> Integer
microseconds (TimeInterval i) = i