{-# LANGUAGE RankNTypes #-}

{-| Pure abstractions for time and clocks. -}

module Control.Clock
  ( Clock(..)
  , clockTick
  , Clocked(..)
  , module Data.Schedule
  )
where

-- internal
import           Data.Schedule (Tick, TickDelta)

{-| A maybe-impure supplier of time, to a pure scheduled computation.

The type @c@ is the computational context where clock operations occur, e.g. a
'Monad' such as 'IO'.

Clock implementations /must/ be monotonic. See "System.Time.Monotonic" for an
example on how to wrap non-monotonic clocks to be monotonic.
-}
data Clock c = Clock {
    -- | Get the current time.
    clockNow   :: !(c Tick)
    {-| Suspend the current computation for a given number of ticks.

    Nothing else in the computation runs until the suspension is over.
    Afterwards, 'clockNow' will give the expected value, i.e. for all @n@:

    > do
    >     old <- clockNow
    >     clockDelay n
    >     new <- clockNow
    >     let new' = assert (old + n <= new) new

    The relation is '<=' not '==', because the computer might have slept during
    the mean time or something. On the other hand, if the underlying physical
    clock might delay for a shorter period than requested, then implementations
    of this function /must/ loop-delay until the '<=' condition is satisfied.

    The above is the only condition that scheduled computations should rely on,
    and any actual physical real delay is up to the implementation.
    -}
  , clockDelay :: !(TickDelta -> c ())
    {-| Interleave actions with ticks.

    This is typically recommended for the use-case where your action represents
    a stream of inputs, e.g. from the network or the user. It is meant to
    satisfy the same functionality as the @select@ system call found in common
    operating systems, used with a timeout parameter.

    If @action@ when executed repeatedly gives a sequence of results, then in
    the expression @clkAct <- 'clockWith' clock action@, a subsequent call to
    @'runClocked' clkAct@ when executed repeatedly gives the same sequence of
    results but with ticks interleaved in between them. Executing @'finClocked'
    clkAct@ closes any resources and invalidates any future calls to @clkAct@.

    It is not necessary to call 'finClocked' if any part of 'runClocked' (e.g.
    child threads) throws an exception - implementations should detect these
    situations and clean these up automatically. This frees the user of this
    function from having to add extra constraints which would be the case if it
    had been necessary to run @'Control.Exception.finally' ... (finClocked
    clkAct)@ as cleanup.
    -}
  , clockWith  :: !(forall a. c a -> c (Clocked c a))
    {-| Given an action, run it with a timeout.

    This is typically recommended for the use-case where your action represents
    the response to a single previously-sent request.

    The action may complete despite the timeout firing, in which case its
    result will be lost. This is in general unavoidable and is a common
    property that one simply has to live with in distributed systems. If you
    run the input action repeatedly, then this property applies *for every
    execution*, i.e. it is possible that you get 10 timeouts even though the
    action succeeded 10 times, and you'll lose 10 results.

    If you want all results of all actions, use @clockWith@ instead. The
    downside with that, is that it's slightly less efficient than this, as it
    will interleave every single 'Tick' event and it is up to you to deal with
    skipping/ignoring any of them.
    -}
  , clockTimer :: !(forall a. TickDelta -> c a -> c (Either Tick a))
}

-- | Run 'clockDelay' then 'clockNow'.
clockTick :: Monad c => Clock c -> TickDelta -> c Tick
clockTick clock d = clockDelay clock d >> clockNow clock

-- | See 'clockWith' for details on what this is for.
data Clocked c a = Clocked {
    runClocked :: !(c (Either Tick a))
  , finClocked :: !(c ())
  }