Friday, February 04, 2011

Flexible state monad

I rewrote some code yesterday evening to make the manipulation of state monad easier (again this is F#). My supporting types are:

StateR<'state, 'ret, 'err> = StateRM of ('state -> RetState<'state,'ret,'err>)
RetState<'state,'ret,'err> =
| Success of 'ret * 'state
| Fail of Err<'state,'err>
Err<'state,'err> =
| ErrMsg of 'err*'state
| ErrOr of Err<'state,'err>*Err<'state,'err>
| ErrAnd of Err<'state,'err>*Err<'state,'err>
| ErrTag of 'err*Err<'state,'err>
Yesterday, I rewrote the following:
let rec mapErrState f = function
| ErrMsg(et,s) -> ErrMsg(et, f s)
| ErrOr(e1, e2) -> ErrOr(mapErrState f e1, mapErrState f e2)
| ErrAnd(e1, e2) ->ErrAnd(mapErrState f e1, mapErrState f e2)
| ErrTag(et, e) -> ErrTag(et, mapErrState f e)

let adapt' get set err m' = fun a -> toState (fun s ->
match (runState (m' a)) (get s) with
| Success (r,s') -> Success (r, set s s')
| Fail es -> Fail (err (mapErrState (fun s' ->set s s') es))

let adapt get set err m = toState (fun s ->
match (runState m) (get s) with
| Success (r,s') -> Success (r, set s s')
| Fail es -> Fail (err (mapErrState (fun s' ->set s s') es))
To understand the adapt' and adapt function take a look a the monadic bind function:

let bind m f = toState (fun s ->
match runState m s with
| Success (v,s')->
let n = f v
runState n s'
| Fail err ->
Fail err)
Here, runstate gets the function out of the monad m and applies it to the state s. This normally results in a success "pair" with the second term being the new state and the first term of the pair being the optional return type of the monad (is unit type when no return).

What adapt does is to adapt a monad to work with a different state, normally a larger state that includes a the state of the monad. Now I can rewrite the following functions:
let map1M' m' = adapt' Lib.first (fun (_,s) s' -> (s',s)) Lib.identity m'
let map1M m = adapt Lib.first (fun (_,s) s' -> (s',s)) Lib.identity m
let map2M' m' = adapt' Lib.second (fun (s,_) s' -> (s,s')) Lib.identity m'
let map2M m = adapt Lib.second (fun (s,_) s' -> (s,s')) Lib.identity m

let pairM' t m' = adapt' (fun s -> (t, s)) (fun s (t,s') -> s') Lib.identity m'
let pairM t m = adapt (fun s -> (t, s)) (fun s (t,s') -> s') Lib.identity m
These functions are used to locally grow the monadic state into a pair where the second arument is the original state and the first is provided as an argument (pairM and pairM') and then to run "local" monads that are defined either for the first or second state. The ' annotation indicates that we are working with functions that return monads and not "raw" state monads.

This brings me back to the my on Local versus global monadic states in large applications, a third solution to integrate a monadic module is to write the module purely with a local state and then to "adapt" it into a larger state with the functions above. The limitations of doing things this way is that is that these modules cannot call other modules as they have no other state to make available than their own.

Finally, you may note those "get and set" arguments for the adapt functions. These are not ideal but I think I need to rewrite my error management to remove my explicit state from the failure type. My gut feeling tell me that would allow a "two way" interation where the parameterizing argument to adapt could itself be a monad.

No comments: