Control.Monad.Progress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
-- | The 'ProgressT' monad transformer, which allows an arbitrary
-- procedure to be monitored via progress events.
module Control.Monad.Progress
       ( -- * The Progress monad
         Progress
       , runProgress
         -- * The ProgressT monad transformer
       , ProgressT(..)
       , runProgressT
         -- * Progress metadata
       , TaskStack
       , Task(..)
         -- * Progress operations
       , task
       , step
       ) where

import Control.Monad
import Control.Monad.Coroutine
import Control.Monad.Coroutine.SuspensionFunctors
import Control.Monad.Identity
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.State

import Data.Word

{-|
The parameterizable progress monad.

Computations are tagged with 'task's, and can yield progress 'step'
events, which are used to track the progress of the wrapped
procedure.

The 'return' function simply creates a taskless procedure with no
steps, while '>>=' adds a procedure to the currently active step.
-}
type Progress l = ProgressT l Identity

-- | Runs a pure progress procedure and, if the procedure completes a
-- step, returns the 'TaskStack' at that step and the continuation of
-- the procedure, or, if the procedure is complete, returns the
-- computed value.
runProgress :: Progress l a     -- ^ The progress procedure to run
               -> Either (Progress l a, TaskStack l) a
runProgress = runIdentity . runProgressT

{-|
The progress monad transformer, with an inner monad.

Computations are tagged with 'task's, and can yield progress 'step'
events, which are used to track the progress of the wrapped
procedure.

The 'return' function simply creates a taskless procedure with no
steps, while '>>=' adds a procedure to the currently active step.
-}
newtype ProgressT l m a =
  ProgressT
  { -- | The underlying 'Coroutine' describing the procedure in progress.
    procedure ::
       Coroutine
       (Yield (TaskStack l))
       (StateT (TaskStack l) m) a
  }

instance MonadTrans (ProgressT l) where
  lift = ProgressT . lift . lift

instance Monad m => Monad (ProgressT l m) where
  return = ProgressT . return
  p >>= f = ProgressT (procedure p >>= procedure . f)

instance MonadIO m => MonadIO (ProgressT l m) where
  liftIO = lift . liftIO

-- | Runs a progress procedure and, if the procedure completes a
-- step, returns the 'TaskStack' at that step and the continuation of
-- the procedure, or, if the procedure is complete, returns the
-- computed value.
runProgressT :: Monad m
                => ProgressT l m a  -- ^ The progress procedure to run
                -> m (Either (ProgressT l m a, TaskStack l) a)
runProgressT action = do
  result <- evalStateT (resume . procedure $ action) []
  return $ case result of
    Left (Yield stack cont) -> Left (ProgressT cont, stack)
    Right a -> Right a

-- | A stack with information about running 'Task's. The currently
-- running task is the first element in the stack; parent tasks
-- follow subsequently.
type TaskStack l = [Task l]

-- | A currently running task, with an user-defined label describing
-- the task
data Task l =
  Task
  { taskLabel :: l          -- ^ The task label
  , taskTotalSteps :: Word  -- ^ Total steps required to complete the task
  , taskStep :: Word        -- ^ The step at which the running task is
  } deriving (Show, Eq)

-- | Creates a new 'Task' to be tracked for progress. The task is given
-- a label that can be used to mark it with arbitrary metadata.
task :: Monad m
        => l                -- ^ The task label
        -> Word             -- ^ Total number of steps required to
                            -- complete the task
        -> ProgressT l m a  -- ^ The action describing the steps
                            -- necessary to complete the task
        -> ProgressT l m a
task label steps action = ProgressT $ do
  -- Add the task to the task stack
  lift . modify $ pushTask newTask

  -- Perform the procedure for the task
  result <- procedure action

  -- Insert an implicit step at the end of the task
  procedure step

  -- TODO Check if all the steps completed
  -- The task is completed, and is removed
  lift . modify $ popTask

  return result
  where
    newTask = Task label steps 0
    pushTask = (:)
    popTask = tail

-- | Marks one step of the current task as completed. If the task
-- already is completed, meaning that all the steps have been
-- performed, does nothing.
step :: Monad m => ProgressT l m ()
step = ProgressT $ do
  (current : tasks) <- lift get
  let currentStep = taskStep current
      nextStep = currentStep + 1
      updatedTask = current { taskStep = nextStep }
      updatedTasks = updatedTask : tasks
  when (currentStep > taskTotalSteps current) $
    fail "The task has already completed"
  yield updatedTasks
  lift . put $ updatedTasks