I have a very strong requirement for my Web application: The user must create haskell procedures by means of web page formularies. The procedures must be created from the menu resposes. They must be stored in pesisten storage , retrieved and executed later when the user need it.
Of course I dont want to permit the creation of arbitrary computations, but to constrain the user freedon trough a web navigation with a restricted set of options. Tha´s because the user is not a programmer and because the created computation is domain specific.
Of course I dont want to permit the creation of arbitrary computations, but to constrain the user freedon trough a web navigation with a restricted set of options. Tha´s because the user is not a programmer and because the created computation is domain specific.
This is one of tme most complex requirements I may think of for an interactive application. To do so I need:
- To create a DSL
- An interpreter of the DSL
- A serializer and deserializer of the DSL
- A set of Web forms
- And a logic that maps the Web navigation with the options of the DSL
What if I say that I can do it all in a single procedure, with the addition that the generated computation will run at compiled speeds? That would be magic, but this is the power of monads. Actually, the DSL, the interpreter, the serializer-deserializer and the set of menus have the same sematic, with the same set of repetitions, conditionals and sequences. Why not define this abstract semantics in a independent way and left the details for whatever needed to the underlying monads that navigate the arrows of this semantic definition? Once defined this navigation, you "only" need a monad that ask to the user, store the response, retrieve it and interpret it to assemble the different steps to generate the resulting computation.
This approach has been proved to work fine in the example of an applicative serializer-deserializer that I presented in my previous post (see below "parseLets")
The Workflow monad transformer brings automatic serlialization and deserialization of the intermediate results of a computation. If I store the user answers to a set of interactive menus, I can return a function made with the responses of these menus. But I don´t want to ask the user everytime, I want to store the responses and return the function with these stored responses when they are stored, and ask for them when they are not.
But that is what Workflow does. An already executed workflow , when re-executed, will ever return the same final result, composed with the stored intermediate results.
For example, this program will ask your name the first time that it is executed. The rest of the executions it will say hello to you and exit (unless the log is deleted)
1 2 3 4 5 6 7 8 9 10 11 12 | module Main where import Control.Workflow main = getName >>= putStrLn getName= exec1nc "test" $ do name <- step $ do putStrLn "your name?" getLine return $ "hello " ++ name |
>runghc hello
your name?
Alberto
hello Alberto
>runghc hello
hello Alberto
>runghc hello
hello Alberto
The magic is in the step monad transformer in getName , that stores the getLine response. When it is executed for a second time, step will read the response from the storage instead of asking again, so getName will do nothing but to return the "hello yourname" string
This is is the log of execution of getName located at ./TCacheData/Workflow/Stat/test/void :
There are other intruder here: exec1nc (line 6) is the command that execute the workflow. This variant does not delete the log upon finalization neither deletes the workflow from the list of active workflows. That is what we need, because the workflow scheduler will find this procedure unfinished and will recover its execution state. because everything has been executed already, exec1nc just return the result.
The first parameter of exec1nc is an identifier for the workflow in persistent storage.
MFlow is a library that add web interfaces to workflows. Well, actually MFlow it is a Web application server that run stateful server procedures, that may or may not be in the workflow monad and offers a set of user-interface combinators that produce type safe responses. Because an MFlow process can be stateful and persistent, we can ask to the user, in a web browser, a set of questions in a single computation.
Here below is a complete menu-driven definition of a simple function. Just install MFlow from hackage, runghc the program and in the browser go to http://localhost.
Almost all of a MFlow procedure is problem specific. There is very little plumbing, so I´m sure that you will get it:
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 | {-# OPTIONS -XDeriveDataTypeable #-} module Main where import Data.Typeable import MFlow.Wai.XHtml.All import Control.Concurrent main= do addMessageFlows [("",runFlow ops)] forkIO $ run 80 waiMessageFlow adminLoop ops= do i <- ask $ p << ("Enter a number. This number will be the parameter for a function\n" ++ "that will be defined by menu if it has not been defined previously") ++> getInt Nothing f <- runFlowIn "fun" getf ask $ p << ("The result is: " ++ show (f i)) ++> wlink () (bold << "next") ops where
getf = do op <- step . ask $ p << "let define the function: which operation?" ++> getSelect( setOption Plus (bold << "+") <|> setOption Times (bold << "*")) <** submitButton "submit" num <- step . ask $ p << "give me another number" ++> getInt Nothing return $ case op of Plus -> (+ num) Times -> (* num) data Ops= Plus | Times deriving (Read, Show, Typeable) |
In the line 18. runFlowIn is the equivalent of exec1nd for web flows. The flow getf ask for a binary operation (either + or *) in the lines 26-27. After that it ask for a number (line 30). It returns a function with a single argument, either ( + num) or (* num) .
Once the dialogs of getf are executed, the user will not be asked again, even if you stop and restart the program. So in successive executions, the program will just ask for a number (line 14) and will show the result of the application of the function (line 20).
The function returned is not interpreted, it is "compiled" by the MFlow process
The function returned is not interpreted, it is "compiled" by the MFlow process
It is easy to see that any kind of expression can be defined with the appropriate web form navigation.
And now, what kind of computations my aplication must compose by web menus? They are workflows in the workflow monad! So my flows will create workflows. There is no problem with this, since this approach could be used to compose not just functions but any monadic computation.
And now, what kind of computations my aplication must compose by web menus? They are workflows in the workflow monad! So my flows will create workflows. There is no problem with this, since this approach could be used to compose not just functions but any monadic computation.