Workflow examples

From Clean
Jump to navigationJump to search

To determine the direction of the iTasks research we need examples of workflow scenarios to see if they can be implemented in the system.

Basic workflow

Lazy workflow

A lazy workflow is one where a user is not asked to provide any data until it is sure the data are needed for constructing the end result. Well, you can do this in any programming language, so we only consider a workflow to be lazy if not only the computer does a minimum job; also the programmer is allowed to be lazy: a minimum of special language constructs or manual program transformations needed to turn a program from strict into lazy.

We know Clean is lazy in this respect for ordinary program fragments that do not involve I/O. We also know that such laziness in workflows is impossible without compromising referential transparency. But all is not lost.

Teaser

The following assignment is a simple teaser to demonstrate that lazy workflows are even impossible in today's iTasks: Say we have a tree structure with a form of some type at each Node. The form could be for a mortgage application, an insurance claim, or anything else. For simplicity’s sake, we assume the form is a simple Integer number.

The assignment is to use your favorite workflow platform to display a task for each form and ask the user to enter a Boolean verdict for each form. For the mortgage application, the answer could be ‘granted’ or ‘not granted’, for the insurance claim it could be ‘covered’ or ‘not covered’. For the Integer number it could be ‘even’ or ‘odd’. The assignment includes delivering the result in a tree structure. The input tree and the result tree should have the same shape.

The result tree will be consumed by a black box process. We do not know in which order this process will consume the Leaves and Nodes of the tree, nor whether it will consume all Leaves and Nodes, nor how much time it will take. Parts of the tree could even be consumed in a parallel or interleaved way. We know nothing of the tree’s size. We must avoid submitting forms for a verdict if that particular verdict will never be consumed. We should also take into account that there may be multiple independent black box consuming processes: each form should be submitted for a verdict to a workflow user at most once.

Practical Examples

Say we have a two step workflow, in iTask terms: task1 =>> \x -> task2, where task1 delivers x which has a complex type, such as a record or an algebraic type. Examples are:

Task1: A problem is reported, such as a failure in an automated system, a car, a home heating device, or the wrong articles have been delivered by a company, or an order invoice’s amount is too high. Task2: The problem is solved, starting at the help desk. Depending on the problem’s nature, different solutions may be possible.

Task1: Articles are ordered. Task2: The order is processed, where some articles may be out of stock and may or may not be replaced, where alternatives may be more expensive or lower quality or just an uncertain fit.

Task1: An employee reports sick leave. Task2: A response is determined, such as waiting, sending flowers, sending over a medical doctor or formulate a project to have the employee fit to get back to work.

Task1: File a tax declaration. Task2: Determine the tax amount, where necessary details of the tax declaration may depend on other circumstances taken into account.

Task1: A quote is requested, for instance from a home construction company, or for building an automated system, or for a one year interior cleaning contract in an office building. Task2: A quote is assembled, where the company may need further details, depending on the situation.

In such cases, we cannot expect the user1 (carrying out Task1) to enter exactly all details that will be necessary in task2 and no more. It will generally not be acceptable to have user1 enter all possible details just in case.

In traditional workflows we would have a loop connecting these two tasks, going back to task1 until task2 is satisfied. But what if task2 is satisfied but task3, somewhere downstream, finds an omission in the result of task1? The loops and knots would quickly become a mess. It is not difficult to give examples of realistic workflows where there are multiple independent parallel task2’s each requesting different details of the result of task1.

In a lazy workflow, the evaluation machinery would automatically and correctly create all these loops as necessary. The workflow would also benefit from strictness analysis, automatically requesting everything from a user that will be necessary anyway, leaving the rest to be lazy.

Let's discuss the following iTask example by Bas Lijnse:

purchaseTask :: Task Void
purchaseTask =
  definePurchase                  =>> \purchase  ->
  selectSuppliers                 =>> \suppliers ->
  collectBids purchase suppliers  =>> \bids      ->
  selectBid bids                  =>> \bid       ->
  confirmBid purchase bid

collectBids :: String [(Int,String)] -> Task [((Int,String),Real)]
collectBids purchase suppliers
  = andTasks
    [("Bid for " +++ purchase +++ " from " +++ name, uid @: ("Bid request
       regarding " +++ purchase, collectBid purchase supplier)) \\
       supplier =: (uid,name) <- suppliers]
where
   collectBid :: String (Int,String) -> Task ((Int,String),Real)
   collectBid purchase bid
      = [ Text "Please make a bid to supply ",ITag [] [Text purchase]
        , HrTag [] ]
        ?>>
        editTask "Ok" createDefault =>> \price ->
        return_V (bid, price)

The iTasks semantics imply that collectBids does not start until the list of selected suppliers is complete. Also: selectBid does not do anything before all suppliers have returned a bid. This cannot be repaired inside these functions. Laziness would allow collectBid to start once the first supplier is selected, and would allow selectBid to terminate before all bids have arrived. Current iTasks forces one to add this plumbing on the purchaseTask level

Dynamic workflow

  • A user is given the task of producing some data (e.g. fill in a large form). Since he does not have all information to complete the task he decides to: fill in part of the form himself, divide the rest of the form and assigns the pieces to a number of experts. Depending on whether the user trusts his experts enough he may setup the task such that: A. the information of the expert is automatically merged with his own and the task is closed and data delivered, or B. he first wants to validate the information from the experts before passing the data along.

Store for anything workflow

  • In this example we construct a store for anything, i.e. the items that you want to sell are determined by their type. The store specification itself is fully overloaded and generic. Stock and orders are stored in a database. The complete source code can be downloaded at http://www.sws.cs.ru.nl/publications/papers/2009/shop.zip.
  • We define two workflow situations in this case study.
  • A catalogue management workflow in which a worker can edit, remove, and add items in the store. This is an example of a regular workflow, because each item in stock can be manipulated in the same way. This can be expressed concisely by a map of the stock items.
manageCatalog          :: a [HtmlTag] -> Task a | iData, DB a
manageCatalog _ prompt = stopTask (prompt ?>> foreverTask (dbReadAll =>> browseCatalog))
where
  browseCatalog        :: [a] -> Task a | iData, DB a
  browseCatalog items  = orTasksVert (map itemActions items ++ [chooseTask_btn [] [("Append",new)]])
  where
     new               = dbCreateItem =>> \first -> editTask "Store" first =>> dbUpdateItem

     itemActions       :: a -> Task a | iData, DB a
     itemActions item  = chooseTask_btn [toHtml item] 
                           [("Edit",   editTask "Store" item =>> dbUpdateItem)
                           ,("Delete", dbDeleteItem (getItemId item) #>> return item)
                           ]
  • A customer workflow. A customer can browse the shop catalogue, add and remove items in a shopping cart, buy the selected items of the cart, or leave the shop. This is an example of an irregular recursive workflow.
doShopping                 :: (Cart a) [a] -> Task (ShopAction,Cart a) | iData, Product a
doShopping cart []         = (shopPrompt ++ [normalText "Currently no items in catalogue, sorry."])
                             ?>> OK #>> return (LeaveShop,cart)
doShopping cart items      = orTasksVert [ navigateShop shopPrompt cart : map (itemActions cart) items ] 
where
   itemActions             :: (Cart a) a -> Task (ShopAction,Cart a) | iData, Product a
   itemActions cart item   = chooseTask_btn [toHtml item] [("Add to Cart", return (ToCart, add (toCartItem item) cart))]
   where
      add                  :: (CartItem a) [CartItem a] -> [CartItem a]
      add new []           = [new]
      add new [item:items] = if (eqItemNr new item) 
                                [amountOrderedUpd item (amountOrderedOf item+1):items]
                                [item:add new items]
  • Shop navigation is a matter of selecting the right button:
navigateShop               :: [HtmlTag] (Cart a) -> Task (ShopAction, Cart a) | iData a
navigateShop prompt cart   = chooseTask [] 
                                [ ("Do Shopping",       return (ToShop,   cart))
                                , ("Check Out And Pay", return (ToPay,    cart))
                                , ("Show Cart",         return (ToCart,   cart))
                                , ("Leave Shop",        return (LeaveShop,cart))
                                ] <<? [ BrTag  [], boldText "Total cost of ordered items = ", toHtml (totalCost cart)
                                      , DivTag [] prompt ]
  • The irregular recursive structure is connected together with the following two, mutually recursive, functions:
browseShop                 :: (Cart a) [HtmlTag] [HtmlTag] -> Task Void | iData, DB, Product a
browseShop initCart shopPrompt cartPrompt
                           = dbReadAll      =>> \items ->
                             doShopping initCart items =>> 
                             doAction   initCart items
doAction                   :: (Cart a) [a] (ShopAction, Cart a) -> Task Void | iData, DB, Product a
doAction initCart items (action,cart)
                           = case action of
                               LeaveShop = return Void
                               ToCart    = showCart   cart       =>> doAction initCart items
                               ToShop    = doShopping cart items =>> doAction initCart items
                               ToPay     = checkOutAndPay cart   #>> browseShop initCart shopPrompt cartPrompt
  • Do you want to sell books? Let's make a book type:
:: Book                    = { id_     :: DBRef Book
                             , title   :: String
                             , author  :: String
                             , price   :: HtmlCurrency
                             , inStock :: Int
                             }
  • It has to connect to a database:
instance DB Book where databaseId         = mkDBid "books" LSTxtFile
                       getItemId item     = id_Of  item
                       setItemId id item  = id_Upd item id
  • It has to connect to the store, that wants to know what kind of product it is:
class Product a | nameOf, priceOf, id_Of, inStockOf a
instance nameOf    Book where nameOf    r = r.Book.title
instance priceOf   Book where priceOf   r = r.Book.price
instance id_Of     Book where id_Of     r = r.Book.id_
instance inStockOf Book where inStockOf r = r.Book.inStock
  • It has to be the default product and cart content:
defaultProduct             :: Book
defaultProduct             = createDefault
defaultCart                :: Cart Book
defaultCart                = createDefault
  • You're done!