進捗状況を追跡するためのモナドトランス

私はプロシージャの進行状況を追跡するために使用できるモナド変圧器を探しています。どのように使用するか説明するには、次のコードを検討してください。

procedure :: ProgressT IO ()
procedure = task "Print some lines" 3 $ do
  liftIO $ putStrLn "line1"
  step
  task "Print a complicated line" 2 $ do
    liftIO $ putStr "li"
    step
    liftIO $ putStrLn "ne2"
  step
  liftIO $ putStrLn "line3"

-- Wraps an action in a task
task :: Monad m
     => String        -- Name of task
     -> Int           -- Number of steps to complete task
     -> ProgressT m a -- Action performing the task
     -> ProgressT m a

-- Marks one step of the current task as completed
step :: Monad m => ProgressT m ()

私は、モナドの法則のために step が明示的に存在しなければならないこと、そしてプログラムの決定性/停止問題のために task

上記のようなモナドは、私が見ているように、次の2つの方法のいずれかで実装できます。

  1. 現在のタスク名/ステップインデックススタックを返す関数と、中断した時点でプロシージャの継続を使用します。返された継続でこの関数を繰り返し呼び出すと、プロシージャの実行が完了します。
  2. タスクステップが完了したときに何をすべきかを説明するアクションを実行した関数を使用します。この手続きは、アクションが完了するまで制御不能に実行され、提供されたアクションによる変更について環境に通知します。

ソリューション(1)については、 Yield サスペンドファンクタを使って Control.Monad.Coroutine を調べました。解決策(2)に関して、私は有用である既に利用可能なモナド変圧器について知らない。

私が探している解決策は、あまりにも多くのパフォーマンスのオーバーヘッドを持つべきではなく、できるだけ多くの制御を可能にするべきです(IOアクセスなどは必要ありません)。

これらのソリューションのいずれかが実行可能と思われるのですか、またはこの問題の他の解決策がすでにどこかにありますか?この問題は、私が見つけられなかったモナド変圧器で既に解決されていますか?

EDIT: The goal isn't to check whether all the steps have been performed. The goal is to be able to "monitor" the process while it is running, so that one can tell how much of it has been completed.

17
@ChrisKuklewicz、私はモナドの値がこのコンテキストでどのように有害であるかを見ていませんか?
追加された 著者 dflemstr,
(2) Chan や友人の仕事のように聞こえます。
追加された 著者 Dan Burton,
String - > ProgressT IO()タイプで putStr および putStrLn を再実装しない限り、それらを持ち上げる必要があります。これを行うには liftIO を使用します。
追加された 著者 Thomas Eding,
あなたは継続を述べました...私は何かが分かりませんが、継続モナド変圧器 ContT を使うことができるのだろうかと思います。
追加された 著者 mergeconflict,
進行状況は微妙です。 「forall(ST)」トリックを使用しない限り、リージョンはタイプセーフではありません。 hackage.haskell のモナド地域ライブラリを見てみてください。 org/packages/archive /… を参照してください。
追加された 著者 Chris Kuklewicz,
進捗情報の作成と表示はパブリッシュ/サブスクライブシステムです。フードの中でそれを実装する方法は、メインスレッドか特別な他のスレッドか他の多くのスレッドが進行状態で動作するかどうかによって異なります。
追加された 著者 Chris Kuklewicz,

3 答え

これはこの問題に対する私の悲観的な解決策です。 Coroutine を使用して各ステップの計算を中断し、ユーザーが任意の計算を実行して進捗状況を報告できるようにします。

EDIT: The full implementation of this solution can be found here.

このソリューションを改善できますか?

まず、どのように使用されます:

-- The procedure that we want to run.
procedure :: ProgressT IO ()
procedure = task "Print some lines" 3 $ do
  liftIO $ putStrLn "--> line 1"
  step
  task "Print a set of lines" 2 $ do
    liftIO $ putStrLn "--> line 2.1"
    step
    liftIO $ putStrLn "--> line 2.2"
  step
  liftIO $ putStrLn "--> line 3"

main :: IO ()
main = runConsole procedure

-- A "progress reporter" that simply prints the task stack on each step
-- Note that the monad used for reporting, and the monad used in the procedure,
-- can be different.
runConsole :: ProgressT IO a -> IO a
runConsole proc = do
  result <- runProgress proc
  case result of
    -- We stopped at a step:
    Left (cont, stack) -> do
      print stack     -- Print the stack
      runConsole cont -- Continue the procedure
    -- We are done with the computation:
    Right a -> return a

上記のプログラムは、

--> line 1
[Print some lines (1/3)]
--> line 2.1
[Print a set of lines (1/2),Print some lines (1/3)]
--> line 2.2
[Print a set of lines (2/2),Print some lines (1/3)]
[Print some lines (2/3)]
--> line 3
[Print some lines (3/3)]

実際の実装(コメント付きバージョンについては、こちらを参照してください):

type Progress l = ProgressT l Identity

runProgress :: Progress l a
               -> Either (Progress l a, TaskStack l) a
runProgress = runIdentity . runProgressT

newtype ProgressT l m a =
  ProgressT
  {
    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

runProgressT :: Monad m
                => ProgressT l m a
                -> 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

type TaskStack l = [Task l]

data Task l =
  Task
  { taskLabel :: l
  , taskTotalSteps :: Word
  , taskStep :: Word
  } deriving (Show, Eq)

task :: Monad m
        => l
        -> Word
        -> ProgressT l m a
        -> 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

  -- The task is completed, and is removed
  lift . modify $ popTask

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

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
4
追加された

これを行う最も明白な方法は StateT です。

import Control.Monad.State

type ProgressT m a = StateT Int m a

step :: Monad m => ProgressT m ()
step = modify (subtract 1)

しかし、あなたが task のセマンティクスを何にしたいか分かりません。

IOでこれを行う方法を示すための編集

step :: (Monad m, MonadIO m) => ProgressT m ()
step = do
  modify (subtract 1)
  s <- get
  liftIO $ putStrLn $ "steps remaining: " ++ show s

状態を出力するには、 MonadIO 制約が必要です。状態に異なる効果が必要な場合は、異なる種類の制約を設定できます(つまり、ステップ数がゼロ以下になると例外がスローされます)。

2
追加された
プロシージャが完了した後でのみ状態にアクセスできるため、進捗状況をまったく追跡できないため、これは有用ではありません。
追加された 著者 dflemstr,
私が procedure :: StateT Int IO();を持っていれば、 procedure = forever step で、 step を呼び出すたびに現在のステップ値を出力するように procedure を実行するにはどうすればいいですか? State モナドでは不可能です。
追加された 著者 dflemstr,
@ChrisKuklewicz私は勝つことができます! この解決策をご覧ください。
追加された 著者 dflemstr,
@ChrisKuklewiczしかし、これは手続きと "進捗報告アクション"を同じモナドにすることを強いられ、これは多くの制御を失うことを意味する。たとえば、長いテキストの単語を置き換えるプロシージャ(たとえば)を監視する場合、進捗報告アクションにIOが必要なため、そのプロシージャをIOモナドに入れないようにします。
追加された 著者 dflemstr,
ハァッ?あなたはいつでも get を呼び出して状態を読むことができます!
追加された 著者 sclv,
@dflemstr:あなたは勝てない(unsafePerformIOなしで)。純粋な計算は、監視プロセスと通信できません。一方、素数の純粋なリストの評価を強制し、あなたの進捗状況を定期的に印刷することを検討してください。
追加された 著者 Chris Kuklewicz,
@dflemstr:StateT _ IOのモナドでは可能です。現在のステップを印刷するなど、状態を変更して任意のIOを実行するために「ステップ」を書くことができます。
追加された 著者 Chris Kuklewicz,

これがまさにあなたが望むものであるかどうかは不明ですが、正しいステップ数を強制し、最後にゼロステップを残す必要がある実装がここにあります。簡単にするために、私はIO上のモナド変圧器の代わりにモナドを使用しています。私はPreludeモナドを使って私がやっていることをやっていないことに注意してください。

UPDATE:

残りのステップ数を抽出できます。 -XRebindableSyntaxで以下を実行します。

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

module Test where

import Prelude hiding (Monad(..))
import qualified Prelude as Old (Monad(..))

-----------------------------------------------------------

data Z = Z
data S n = S

type Zero = Z
type One = S Zero
type Two = S One
type Three = S Two
type Four = S Three

-----------------------------------------------------------

class Peano n where
  peano :: n
  fromPeano :: n -> Integer

instance Peano Z where
  peano = Z
  fromPeano Z = 0

instance Peano (S Z) where
  peano = S
  fromPeano S = 1

instance Peano (S n) => Peano (S (S n)) where
  peano = S
  fromPeano s = n `seq` (n + 1)
    where
      prev :: S (S n) -> (S n)
      prev S = S
      n = fromPeano $ prev s

-----------------------------------------------------------

class (Peano s, Peano p) => Succ s p | s -> p where
instance Succ (S Z) Z where
instance Succ (S n) n => Succ (S (S n)) (S n) where

-----------------------------------------------------------

infixl 1 >>=, >>

class ParameterisedMonad m where
  return :: a -> m s s a
  (>>=) :: m s1 s2 t -> (t -> m s2 s3 a) -> m s1 s3 a
  fail :: String -> m s1 s2 a
  fail = error

(>>) :: ParameterisedMonad m => m s1 s2 t -> m s2 s3 a -> m s1 s3 a
x >> f = x >>= \_ -> f

-----------------------------------------------------------

newtype PIO p q a = PIO { runPIO :: IO a }

instance ParameterisedMonad PIO where
  return = PIO . Old.return
  PIO io >>= f = PIO $ (Old.>>=) io $ runPIO . f

-----------------------------------------------------------

data Progress p n a = Progress a

instance ParameterisedMonad Progress where
  return = Progress
  Progress x >>= f = let Progress y = f x in Progress y

runProgress :: Peano n => n -> Progress n Zero a -> a
runProgress _ (Progress x) = x

runProgress' :: Progress p Zero a -> a
runProgress' (Progress x) = x

task :: Peano n => n -> Progress n n ()
task _ = return ()

task' :: Peano n => Progress n n ()
task' = task peano

step :: Succ s n => Progress s n ()
step = Progress ()

stepsLeft :: Peano s2 => Progress s1 s2 a -> (a -> Integer -> Progress s2 s3 b) -> Progress s1 s3 b
stepsLeft prog f = prog >>= flip f (fromPeano $ getPeano prog)
  where
    getPeano :: Peano n => Progress s n a -> n
    getPeano prog = peano

procedure1 :: Progress Three Zero String
procedure1 = do
  task'
  step
  task (peano :: Two) -- any other Peano is a type error
  --step -- uncommenting this is a type error
  step -- commenting this is a type error
  step
  return "hello"

procedure2 :: (Succ two one, Succ one zero) => Progress two zero Integer
procedure2 = do
  task'
  step `stepsLeft` \_ n -> do
    step
    return n

main :: IO ()
main = runPIO $ do
  PIO $ putStrLn $ runProgress' procedure1
  PIO $ print $ runProgress (peano :: Four) $ do
    n <- procedure2
    n' <- procedure2
    return (n, n')
1
追加された
これは依然として別の問題を解決します。何らかの進歩のステップを静的に目撃することは重要ではありません。そして、 procedure x = task "foo" xを実行します。 forM_ [1..x] $ const step はこの解決策では不可能になります。 この解決策は問題を解決しますが、理想的ではないかもしれません。
追加された 著者 dflemstr,
これは非常に素晴らしい解決策ですが、別の問題を解決します。元の質問の編集をご覧ください。
追加された 著者 dflemstr,
ああ、なるほど。私は他人のためにそれを残すでしょう。
追加された 著者 Thomas Eding,
@dflemstr:更新済み
追加された 著者 Thomas Eding,