Skip to content

Instantly share code, notes, and snippets.

@agocorona
Created February 25, 2015 15:16
Show Gist options
  • Save agocorona/abf9bb9eb899baeca124 to your computer and use it in GitHub Desktop.
Save agocorona/abf9bb9eb899baeca124 to your computer and use it in GitHub Desktop.

In haskell, continuations have been right in front of your eyes all the time, Use then to undo actions by means of the backtracking effect added to the the hardworking programmer EDSL.

#A word on continuations# In Haskell, the continuation is the second parameter in the bind operation. While most languages that use imperative and eager execution have to resort to continuations to implement special kinds of flows -so the continuation play a central role in them- haskell has not such problem: It uses continuations natively; The monad instance define what each kind of computation has to do with these continuations.

a bind has two parameters: a closure and a continuation.

 x >>=(f1>>=(f2 >>=f3))

So at every moment you know what is the continuation. In any monad. You don't need any special Cont structure for this!!!. It is right there, in the monad instance:

 instance Monad ...
    x >>= f = ....
    ...

For example when executing the second >>= this bind operation has the closure as first parameter. It is the result of x>>=f1, already executed. In the second parameter, the continuation is f2>>=f3.

If you are tired of the usual and boring monadic effects and you are tired also of managing event handlers and threads by hand, just store your continuations in a state monad and make use of them wherever you need it. That is what my Transient monad does. In the previous article It has been used to implement asyncronous event handling, parallelism and thread control. Now I will use for another exotic effect, that may be very useful:

#Backtracking#

The Transient monad of the previous article, has user state managemet, event/signal handling, thread control, parallelism and early termination effects. But another important effect that I wish to make available for the hard working programmer is backtracking. with this additional effect I can undo transactions and I can express Web navigations. As I demonstrated here with MFlow. The backtracking in MFlow is done using a different mechanism, explained in this article in The monad reader. This time I will use the Transient continuations.

I previous articles I presented the Transient monad, that stores the closure and the continuation in a state monad. it also facilities for user-definable session state management. Can we implement backtracking without touching the Base package where the Transient monad is defined?. Yes, we can.

Instead of using intimidating words like "backtracking" as a concept, let´s start with an application of it. let´s code some primitives like undo and onUndo so that we can, for example, undo the reservation of some product when the payment process fails because the user give up for whatever reason. The semantics of these two primitives can be understood by looking at this example:

transaction=   do
       option "back" "backtracking test"  
       productNavigation    
       reserve    
       payment

       liftIO $ print "done!"

       where

       productNavigation = liftIO $ putStrLn "product navigation"

       reserve= liftIO (putStrLn "product reserved,added to cart")
                 `onUndo` liftIO (putStrLn "product un-reserved")

       payment = do
           liftIO $ putStrLn "Payment failed"
           undo

Instead of undoing the reservation manually when the fail is verified, I call undo and let each action undo himself, so I can give the responsibility to the actions themselves. The advantage is that the programmer of the flow don't care about such low level things.

To implement these primitives I will define a registration method registerUndo that register a statement to be re-executed when backtracking.

I need a definition of the backtrack stack, which will contain a flag that indicates if backtracking is being executed and also will contain all the continuations of the back points.

The call registerUndo (below) get the continuation and stores it in the Backtrack structure. This Backtrack data will be stored in the session state using getSessionData and setSessionData

data Backtrack= forall a b.Backtrack{backtracking :: Bool
                                    ,backStack :: [EventF]}


registerUndo :: TransientIO a -> TransientIO a
registerUndo f  = Transient $ do
      cont  <- getCont  
      md  <- getSessionData
      setSessionData $   case md of
         Just   Backtrack b $ cont:bs
         Nothing ->  Backtrack False [cont]
      runTrans f

getCont is the Transient primitive that gives the computation state at that point, including the closure and the continuation.

Then, we define the onUndo primitive, that has two actions as parameters:

    onUndo :: TransientIO a -> TransientIO a -> TransientIO a
    onUndo ac bac= registerUndo $ do
       Backtrack back _ <- getSData <|> return (Backtrack False [])
       if back then bac else ac

When going forward the first action is executed, but when the flag signals that onUndo is being executed under backtracking, the second action is executed.

And now the primitive that executes the backtracking:

undo :: TransientIO a
undo= Transient $ do
  bs <- getSessionData  `onNothing` return nullBack              
  goBackt  bs

  where
  nullBack= Backtrack False []
  goBackt (Backtrack _ [])= return Nothing                      
  goBackt (Backtrack b (stack@(first: bs)))= do
        put first
        setSData $ Backtrack True stack
        mr <-  runClosure first
        Backtrack back _ <- getSessionData `onNothing` return nullBack
        case back of
           True ->  goBackt $ Backtrack True bs                 
           False -> case mr of
                   Nothing -> return Nothing                    
                   Just x -> runContinuation first x             

First It get the back stack, which contains closures and continuations of different back points. then it set the backtracking flag and execute the first closure (that is the last statement registered). If the closure changed the back flag, (False) then the continuation of that closure is executed, so the flow continue forward from that statement on. If the closure return Nothing (early termination) then undo stop.

If the closure don't change the back flag, the next back point in the stack is executed in the same way until there is no more backpoints.

This code below contains all the programs of the Hard working programmer 1 plus the backtracking example(s).




{-# START_FILE main.hs #-}

{-# LANGUAGE ScopedTypeVariables #-}

module Main where 

import           Base
import           Backtrack
import           Control.Applicative
import           Control.Concurrent
import           Control.Exception
import           Control.Monad.State
import           Data.Monoid
import           System.IO.Unsafe

import           Network.HTTP

import           Network
import           System.IO

-- show

main=  do
    runTransient $ do   
      async inputLoop  <|> return ()
      
      option "main" "to return to the main menu"  <|> return ""
      liftIO $ putStrLn "MAIN MENU"

      transaction <|> transaction2 <|> colors <|> 
        app  <|> sum1 <|> sum2 <|> server <|> menu

    stay

transaction=   do
       option "back" "backtracking test"
       productNavigation
       reserve
       payment
       
transaction2= do
       option "back2" "backtracking test 2"
       productNavigation
       reserveAndSendMsg
       payment


       liftIO $ print "done!"

       
productNavigation = liftIO $ putStrLn "product navigation" 

reserve= liftIO (putStrLn "product reserved,added to cart") 
                 `onUndo` liftIO (putStrLn "product un-reserved") 

payment = do
           liftIO $ putStrLn "Payment failed"
           undo

reserveAndSendMsg= do
            reserve
            liftIO $ print "MIDDLE"
            liftIO  (putStrLn "update other database necesary for the reservation")
                 `onUndo` liftIO (putStrLn "database update undone")

colors :: TransientIO ()
colors= do
       option "colors" "choose between three colors"
       r <-  color 1  "red"  <|> color 2 "green" <|> color 3 "blue"
       liftIO $ print r
       where
       color :: Int -> String -> TransientIO String
       color n str= option (show n) str >> return  str

app :: TransientIO ()
app= do
       option "app" "applicative expression that return a counter in 2-tuples every second"
       r <-  (,) <$>  number  <*> number
       liftIO $ putStrLn $ "result=" ++ show r
       where
       number= waitEvents $ do
          threadDelay 1000000
          n <- takeMVar counter
          putMVar counter (n+1)
          return  n

       counter=unsafePerformIO $ newMVar (0 :: Int)

sum1 :: TransientIO ()
sum1= do
       option "sum1" "access to two web pages concurrently and sum the number of words using Applicative"
       (r,r') <- (,) <$> async  (worker "http://www.haskell.org/")
                     <*> async  (worker "http://www.google.com/")

       liftIO $ putStrLn $ "result="  ++ show (r + r')

getURL= simpleHTTP . getRequest

worker :: String -> IO Int
worker url=do
      r <- getURL url
      body <- getResponseBody r
      putStrLn $ "number of words in " ++ url ++" is: " ++ show(length (words body))
      return . length . words $ body

sum2 :: TransientIO ()
sum2= do
       option "sum2" "access to N web pages concurrenty and sum the number of words using map-fold"
       rs <- foldl (<>) (return 0) $ map (async . worker)
                  [ "http://www.haskell.org/"
                  , "http://www.google.com/"]

       liftIO $ putStrLn $ "result="  ++ show rs

instance Monoid Int where
      mappend= (+)
      mempty= 0

server :: TransientIO ()
server=  do
       option "server" "A web server in the port 8080"
       liftIO $ print "Server Stated"
       sock <-  liftIO $  listenOn $ PortNumber 8080
       (h,_,_) <- spawn $ accept sock
       liftIO $ do
           hPutStr h msg
           putStrLn "new request"
           hFlush h
           hClose h
         `catch` (\(e::SomeException) -> sClose sock)

msg = "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nPong!\r\n"


menu :: TransientIO ()
menu=  do
     option "menu"  "a submenu with two options"
     colors  <|> sum2 

-- / show






{-# START_FILE Backtrack.hs #-}

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}

-- show
module Backtrack (registerUndo, onUndo, undo, retry, undoCut) where
-- /show

import Base
import Data.Typeable
import Control.Applicative
import Control.Monad.State
import Unsafe.Coerce

data Backtrack= forall a b.Backtrack{backtracking :: Bool
                                    ,backStack :: [EventF]}
                                    deriving Typeable

-- | assures that backtracking will not go further
undoCut :: TransientIO ()
undoCut= Transient $ do
     delSessionData $ Backtrack False []
     return $ Just ()

-- | the secod parameter will be executed when backtracking 
{-# NOINLINE onUndo #-}
onUndo :: TransientIO a -> TransientIO a -> TransientIO a
onUndo ac bac= do
   r<-registerUndo $ Transient $ do 
     Backtrack back _ <- getSessionData `onNothing` return (Backtrack False [])
     runTrans $ if back then bac  else ac 
   return r
   
-- | register an actions that will be executed when backtracking
{-# NOINLINE registerUndo #-}
registerUndo :: TransientIO a -> TransientIO a
registerUndo f  = Transient $ do
   cont@(EventF _ _ _ i _ _ )  <- get   !> "backregister"
   md  <- getSessionData
   setSessionData $   case md of
        Just (bss@(Backtrack b (bs@((EventF _ _ _ i' _ _ ):_)))) -> if False then bss else  Backtrack b $ cont:bs
        Nothing ->  Backtrack False [cont]
   runTrans f

-- | restart the flow forward from this point on
retry :: TransientIO ()
retry= do
    Backtrack _ stack <- getSessionData `onNothing` return (Backtrack False [])
    setSData $ Backtrack False stack

-- | execute backtracking. It execute the registered actions in reverse order. 
--
-- If the backtracking flag is changed the flow proceed  forward from that point on. 
--
--If the backtrack stack is finished or undoCut executed, `undo` will stop.
undo :: TransientIO a
undo= Transient $ do
  bs <- getSessionData  `onNothing` return nullBack            !>"GOBACK"
  goBackt  bs

  where
  nullBack= Backtrack False []
  goBackt (Backtrack _ [])= return Nothing                     !> "END"
  goBackt (Backtrack b (stack@(first@(EventF x fs _ _ _  _ ): bs)))= do
        put first{replay=True} 
        setSData $ Backtrack True stack
        mr <-  runClosure first                                !> "RUNCLOSURE"
        Backtrack back _ <- getSessionData `onNothing` return nullBack 
                                                               !>"END RUNCLOSURE"
        case back of
           True ->  goBackt $ Backtrack True bs                !> "BACK AGAIN"
           False -> case mr of
                   Nothing -> return empty                     !> "FORWARD END"
                   Just x ->  runContinuation first x          !> "FORWARD EXEC"


{-# START_FILE Base.hs #-}

-----------------------------------------------------------------------------
--
-- Module      :  Base
-- Copyright   :
-- License     :  GPL (Just (Version {versionBranch = [3], versionTags = []}))
--
-- Maintainer  :  agocorona@gmail.com
-- Stability   :
-- Portability :
--
-- |
--
-----------------------------------------------------------------------------
{-# LANGUAGE ExistentialQuantification,FlexibleContexts,
             FlexibleInstances, MultiParamTypeClasses #-}

-- show
module Base  where 
-- /show

import Control.Monad.State
import Unsafe.Coerce
import System.IO.Unsafe
import Control.Applicative
import qualified Data.Map as M
import Data.Dynamic
import Debug.Trace
import Data.Monoid

--import Data.IORef
import Control.Concurrent
import Control.Concurrent.STM
import GHC.Conc
import Data.Maybe
import System.Mem.StableName
import Data.List

(!>) =  const . id -- flip trace
infixr 0 !>

data Transient m x= Transient  {runTrans :: m (Maybe x)}
type SData= ()

type EventId= Int



data EventF  = forall a b . EventF{xcomp :: TransientIO a
                                  ,fcomp :: a -> TransientIO b
                                  ,mfData :: M.Map TypeRep SData
                                  ,mfSequence :: Int
                                  ,row :: P RowElem
                                  ,replay :: Bool
                                  }

type P= MVar

type Buffer= Maybe ()
type NodeTuple= (EventId, ThreadId, Buffer)

type Children=  Maybe (P RowElem)

data RowElem=   Node NodeTuple |  RowList Row Children

instance Show RowElem where
  show (Node (e,_,_))= show e
  show (RowList r ch)= show ( reverse r)  ++ "->" ++ show ch

type Row = [P RowElem]

instance Eq NodeTuple where
     (i,_,_) ==  (i',_,_)= i == i'


instance Show x => Show (MVar x) where
  show  x = show (unsafePerformIO $ readMVar x)

eventf0= EventF  empty (const  empty) M.empty 0
          rootRef False

-- {-# NOINLINE topNode #-}
-- topNode= (-1 :: Int,unsafePerformIO $ myThreadId,False,Nothing)

{-# NOINLINE rootRef #-}
rootRef :: MVar RowElem
rootRef=  unsafePerformIO $ newMVar $ RowList []  Nothing                      

instance MonadState EventF  TransientIO where
  get=  Transient $ get >>= return . Just
  put x= Transient $ put x >> return (Just ())

type StateIO= StateT EventF  IO

type TransientIO= Transient StateIO

--runTrans ::  TransientIO x -> StateT EventF  IO (Maybe x)
--runTrans (Transient mx) = mx

runTransient :: TransientIO x -> IO (Maybe x, EventF)
runTransient t= runStateT (runTrans t) eventf0


newRow :: MonadIO m => m (P RowElem)
newRow= liftIO $ newMVar $ RowList [] Nothing

setEventCont ::   TransientIO a -> (a -> TransientIO b) -> StateIO EventF
setEventCont x f  = do
   st@(EventF   _ fs d _  ro r)  <- get
   n <- if replay st then return $ mfSequence st
     else  liftIO $ readMVar refSequence
   ro' <- newRow
   ro `eat` ro'
   put $ EventF   x ( \x -> f x >>= unsafeCoerce fs) d n  ro' r !> ("stored " ++ show n)
   return st

eat ro ro'= liftIO $
 modifyMVar_  ro $ \(RowList es t) -> return $ RowList (ro':es) t

resetEventCont (EventF x fs _ _  _ _)=do
   st@(EventF   _ _ d  n  ro r )  <- get
   put $ EventF  x fs d n  ro r


getCont ::(MonadState EventF  m) => m EventF
getCont = get

runCont :: EventF -> StateIO ()
runCont (EventF  x fs _ _  _ _)= do runIt  x (unsafeCoerce fs); return ()
   where
   runIt  x fs= runTrans $ do
         st <- get
         --put st{mfSequence=i}
         r <- x
         put st
         fs r


runClosure :: EventF -> StateIO (Maybe a)
runClosure (EventF x _ _ _ _ _) =  unsafeCoerce $ runTrans x

runContinuation ::  EventF -> a -> StateIO (Maybe b)
runContinuation (EventF _ fs _ _ _ _ ) x= runTrans $  (unsafeCoerce fs) x 

instance   Functor TransientIO where
  fmap f x=   Transient $ fmap (fmap f) $ runTrans x -- 


instance Applicative TransientIO where
  pure a  = Transient  .  return $ Just a
  Transient f <*> Transient g= Transient $ do
       k <- f
       x <- g
       return $  k <*> x

instance  Alternative TransientIO where
  empty= Transient $ return  Nothing
  Transient f <|> Transient g= Transient $ do
       k <- f
       x <- g
       return $  k <|> x


-- | a sinonym of empty that can be used in a monadic expression. it stop the
-- computation
stop :: TransientIO a
stop= Control.Applicative.empty

instance Monoid a => Monoid (TransientIO a) where
  mappend x y = mappend <$> x <*> y  
  mempty= return mempty

instance Monad TransientIO where
      return x = Transient $ return $ Just x
      x >>= f  = Transient $ do
        cont <- setEventCont x  f
        mk <- runTrans x
        resetEventCont cont
        case mk of
           Just k  -> do addDescent' !> "ADDROW" ; runTrans $ f k

           Nothing -> return Nothing

        where
        addDescent'= do
            r <- gets row
            n <- addDescent r
            modify $ \s -> s{row= n}
addDescent r=
            liftIO $ do
              n <- newMVar $ RowList [] Nothing
              modifyMVar_ r $ \(RowList ns ch) ->  return $ RowList  ns $ Just n
             --   case ch of
             --     Just x -> error $ "children not empty: "++ show x
             --     Nothing ->  return $ RowList  ns $ Just n
              return n

addChild row ref= modifyMVar_  row $ \(RowList ns t) -> return $  RowList (ref : ns) t
   
instance MonadTrans (Transient ) where
  lift mx = Transient $ mx >>= return . Just

instance MonadIO TransientIO where
  liftIO = lift . liftIO --     let x= liftIO io in x `seq` lift x



-- | Get the session data of the desired type if there is any.
getSessionData ::  (MonadState EventF m,Typeable a) =>  m (Maybe a)
getSessionData =  resp where
 resp= gets mfData >>= \list  ->
    case M.lookup ( typeOf $ typeResp resp ) list of
      Just x  -> return . Just $ unsafeCoerce x
      Nothing -> return $ Nothing
 typeResp :: m (Maybe x) -> x
 typeResp= undefined

-- | getSessionData specialized for the View monad. if Nothing, the monadic computation
-- does not continue. getSData is a widget that does not validate when there is no data
--  of that type in the session.
getSData :: MonadState EventF m => Typeable a =>Transient m  a
getSData= Transient getSessionData


-- | setSessionData ::  (StateType m ~ MFlowState, Typeable a) => a -> m ()
setSessionData  x=
  modify $ \st -> st{mfData= M.insert  (typeOf x ) (unsafeCoerce x) (mfData st)}

-- | a shorter name for setSessionData
setSData ::  ( MonadState EventF m,Typeable a) => a -> m ()
setSData= setSessionData

delSessionData x=
  modify $ \st -> st{mfData= M.delete (typeOf x ) (mfData st)}

delSData :: ( MonadState EventF m,Typeable a) => a -> m ()
delSData= delSessionData

withSData ::  ( MonadState EventF m,Typeable a) => (Maybe a -> a) -> m ()
withSData f= modify $ \st -> st{mfData=
    let dat = mfData st
        mx= M.lookup typeofx dat
        mx'= case mx of Nothing -> Nothing; Just x -> unsafeCoerce x
        fx=  f mx'
        typeofx= typeOf $ typeoff f
    in  M.insert typeofx  (unsafeCoerce fx) dat}
    where 
    typeoff :: (Maybe a -> a) -> a
    typeoff = undefined
----

genNewId :: MonadIO m => MonadState EventF m =>  m Int
genNewId=  do
      st <- get
      case replay st of
        True -> do
          let n= mfSequence st
          put $ st{mfSequence= n+1}
          return n
        False -> liftIO $
          modifyMVar refSequence $ \n -> return (n+1,n)

{-# NOINLINE refSequence #-}
refSequence :: MVar Int
refSequence= unsafePerformIO $ newMVar 0


--- IO events

--buffers :: IORef [(EventId,Dynamic)]
--buffers= unsafePerformIO $ newIORef []

data Loop= Once | Loop | Multithread deriving Eq

waitEvents ::  IO b -> TransientIO b
waitEvents= parallel Loop


async  :: IO b -> TransientIO b
async = parallel Once

spawn= parallel Multithread

parallel  ::  Loop ->  IO b -> TransientIO b
parallel hasloop receive =  Transient $ do
      cont <- getCont
      id <- genNewId
      liftIO $ forkCont id hasloop receive cont

forkCont::  EventId -> Loop -> IO a -> EventF -> IO (Maybe a)
forkCont id hasloop receive cont= do
      let currentRow= row cont
      mnode  <-   liftIO $ lookTree id currentRow !> ("idToLook="++ show id++ " in: "++ show currentRow)
      
      case mnode of
        Nothing ->do
                 return () !> "NOT FOUND"
                 forkCont' id cont hasloop receive
                 return Nothing
        
        Just (node@(id',th', mrec)) -> do
         -- modify $ \cont -> cont{nodeInfo=Nothing}
          return $ if isJust mrec then Just $ unsafeCoerce $ fromJust mrec else Nothing

        where
        forkCont' id cont hasloop receive= liftIO $ forkIO $ do
                     th <- myThreadId
                     ref <-newMVar  $  Node (id,th,Nothing)
                     addChild (row cont) ref

                     loop hasloop  receive $ \r -> do
                       modifyMVar_  ref $ \(Node(i,th,_)) -> return
                                       $ Node(i,th,Just $ unsafeCoerce r)
                       (flip runStateT) cont $ do
                           cont@(EventF  x fs _  _ _ _) <- get
                           
                           put cont{replay= True{-,-mfSequence=i,-}{-nodeInfo=Just ref-}}
                           
                           mr <- runClosure cont 
                           case mr  of
                             Nothing ->return Nothing
                             Just r ->do
                               row1 <- gets row 
                               liftIO $ delEvents  row1              !> ("delEvents: "++ show row1)
                               id <- liftIO $ readMVar refSequence
                               n <-  addDescent  row1
                               modify $ \cont -> cont{row=n,replay= False,mfSequence=id } !> ("SEQ=" ++ show(mfSequence cont))
                               runContinuation cont r
                       return ()



        loop Once rec x  = rec >>= x
        loop Loop rec f = do
            r <- rec
            f r
            loop Loop rec f

        loop Multithread rec f = do
            r <- rec
            forkIO $ f r
            loop Multithread rec f

        lookTree :: EventId -> P RowElem -> IO (Maybe NodeTuple)
        lookTree id ref=  do
            RowList ns _<- readMVar ref
            lookList id ns



        lookList id mn= case mn of
              [] -> return Nothing
              (p:nodes) -> do
                  me <- readMVar p
                  case me of
                    Node(node@((id',_,_))) ->
                      if id== id'
                         then return $ Just node
                         else lookList id nodes
                    RowList row _ -> do
                         mx <- lookList id nodes
                         case mx of
                           Nothing -> lookList id row
                           Just x -> return $ Just x
        delEvents :: P RowElem  -> IO()
        delEvents ref = do
            RowList mevs mch <- takeMVar ref
            maybeDel mch
            putMVar ref $ RowList mevs Nothing

        maybeDel mch=  case mch of
              Nothing -> return ()
              Just p -> do
                  RowList es mch' <- readMVar p
                  delList es !> ("toDelete="++ show es)
                  maybeDel mch'


        delList es=  mapM_ del es where
          del p = readMVar p >>= del'
          del' (Node(node@(_,th,_)))= killThread th !> ("DELETING " ++ show node)
          del' (RowList l mch)= delList l >> maybeDel mch


type EventSetter eventdata response= (eventdata ->  IO response) -> IO ()
type ToReturn  response=  IO response
react
  :: Typeable eventdata
  => EventSetter eventdata response
  -> ToReturn  response
  -> TransientIO eventdata

react setHandler iob= Transient $ do
        cont    <- getCont
        mEvData <- getSessionData
        case mEvData of
          Nothing -> do
            liftIO $ setHandler $ \dat ->do
--              let cont'= cont{mfData = M.insert (typeOf dat)(unsafeCoerce dat) (mfData cont)}
              runStateT (setSData dat >> runCont cont) cont
              iob
            return Nothing
          Just dat -> delSessionData dat >> return (Just  dat)


{-# NOINLINE getLineRef #-}        
getLineRef= unsafePerformIO $ newTVarIO Nothing


option1 x  message=  inputLoop `seq` (waitEvents  $ do
     liftIO $ putStrLn $ message++"("++show x++")"
     atomically $ do
       mr <- readTVar getLineRef
       th <- unsafeIOToSTM myThreadId
       case mr of
         Nothing -> retry
         Just r ->
            case reads1 r !> ("received " ++  show r ++  show th) of
            (s,_):_ -> if  s == x  !> ("waiting" ++ show x)
                     then do
                       writeTVar  getLineRef Nothing !>"match"
                       return s

                     else retry
            _ -> retry)
     where
     reads1 s=x where
      x= if typeOf(typeOfr x) == typeOf "" then unsafeCoerce[(s,"")] else readsPrec 0 s
      typeOfr :: [(a,String)] ->  a
      typeOfr  = undefined

option ret message= do
    liftIO $ putStrLn $"Enter "++show ret++"\tto: " ++ message
    waitEvents  $ getLine' (==ret)
    liftIO $do putStrLn $ show ret ++ " chosen"
    return ret
    
getLine' cond=   inputLoop `seq` do
     atomically $ do
       mr <- readTVar getLineRef
       th <- unsafeIOToSTM myThreadId
       case mr of
         Nothing -> retry
         Just r ->
            case reads1 r !> ("received " ++  show r ++ show th) of
            (s,_):_ -> if cond s  !> show (cond s)
                     then do
                       writeTVar  getLineRef Nothing !>"match"
                       return s

                     else retry
            _ -> retry
     where
     reads1 s=x where
      x= if typeOf(typeOfr x) == typeOf "" then unsafeCoerce[(s,"")] else readsPrec 0 s
      typeOfr :: [(a,String)] ->  a
      typeOfr  = undefined

inputLoop=  do
    print "Press end to exit"
    inputLoop'
    where
        inputLoop'= do 
           r<- getLine                      !> "started inputLoop"
           if r=="end" then putMVar rexit () else do
              atomically . writeTVar  getLineRef $ Just r
              inputLoop'


rexit= unsafePerformIO newEmptyMVar

stay=  takeMVar rexit

onNothing iox iox'= do
       mx <- iox
       case mx of 
           Just x -> return x
           Nothing -> iox'

           

If you press the option "back", it executes the backtracking test, corresponding to the first snippet of code in this article. The sequence executed is the one intended:

  "back" chosen
  product navigation
  product reserved,added to cart
  Payment failed
  product un-reserved

This is a simple undo with one single back point, but suppose that the reserve call update a database, but, for some reason, it is necessary in the future to update a second database, so you add to reserve this modification without changing the main flow:

reserve= do
            liftIO (putStrLn "product reserved,added to cart")
                 `onUndo` liftIO (putStrLn "product un-reserved")
                 
            liftIO  (putStrLn "update other database necessary for the reservation")
                 `onUndo` liftIO (putStrLn "database update undone")

The undo in the main flow will undo both changes.

There are two more primitives in the library

  • undoCut to empty the stack, so previous back points will not be executed by the next undo
  • retry changes the backtracking flag, so the flow will proceed forward from that point on

You can play with them and tell me the about the results.

The Transient repo:

    https://github.com/agocorona/transient

#Conclussions and future work# With the use of session state and backtracking it is possible to do complex navigations when exploring tree structores or even doing web navigations. I plan to adapt MFlow to this transient Monad.

Execution state persistence, like the Workflow and MFlow packages is also necessary for the hardworking programmer. This can be done by storing events and replaying them.

This is one more effect added to my hardworking programmer super-monad. It is intended to super-charge the Haskell newbie with a set of powerful but intuitive primitives ad combinators to give unprecendented expressive power without adding complexity.

More effects will come...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment