framework for composing monads?
Does someone like to comment on this? I'm planning a new cli/odbc-based database connectivity library for Haskell 98 and want to manage hidden state (various management information) on the Haskell side. Some libs. i.e. for gui, extend the IO monad for this using some "start" function: main :: IO () main = start prog prog :: GUI () ... I could do the same with my library, but then it is difficult to combine, for example, Gui operations with Db operations. I found a better solution in Jeuring/Meijer 1995 in Mark P. Jones' contribution which consists of using monad transformers and lifting monad operations to class level like this: type M a = DB (GUI IO) a startM = start . start main = startM prog prog :: (Io m, Gui m, Db m) => m () prog = ... I hope that this approach not only solves the problem mentioned above but also supports a more modular approach to handling state etc. in general and encourages the programmer to add his own monads (instead of packing all into IO or passing around state etc. explicitly). However, to be really useful, it would be best if there could be established a "standard framework" for such libraries. It would be nice if there were at least agreement on how to lift IO operations to class level: 'Io.putStr' (which provokes name clashes with the standard prelude),'Io.putStrC' (C for class level), or some other prefix/suffix. In addition to Mark P. Jones' examples in the mentioned article, - Class Io must be provided, - all specific monad operations should be encapsulated in ADTs, - it must be possible to map 'lift' into the ADT In the attachements I have worked out a scenario, where DB and GUI are state monad transformers and Io and Gui are defined as follows: class Monad m => Io m where io :: IO a -> m a class Monad m => Db m where db :: DBImpl m a DBImpl is an ADT which encapsulates a set of characteristic monad operations, in this case the 'modify' operation of a state transformer. For such ADTs, a special class Liftable l where mapLift :: (MonadT t, Monad m) => l m -> l (t m) has been introduced to supply a unified symbol for applying lift to the encapsulated monad transformation(s). (I don't like the class name either but didn't find a better up to now...) However, there remains a problem with the type system. Ideally, it would be possible to require: class (Monad m, Monad (t m)) => MonadT t where lift = ... such that MonadT applications become automatically members of class Monad, and declare instance (Db m, MonadT t) => Db (t m) where db = mapLift db (same for class Gui etc.) with the obvious effects. Both seems to be not even possible in the extended type systems of Hugs and ghc. I want to create a library for Haskell 98, so this is of minor importance, but if there was a way to make life easier for those who use type system extensions without complicating the situation for Haskell 98 users, I would of course prefer such a solution. Until then, again, I think that "standard framework" would make it easier for users to create those instance declarations by themselves needed to combine monad transformers from different sources. Elke. --- "If you have nothing to say, don't do it here..." Elke Kasimir Skalitzer Str. 79 10997 Berlin (Germany) fon: +49 (030) 612 852 16 mail: elke.kasimir@catmint.de> see: <http://www.catmint.de/elke> for pgp public key see: <http://www.catmint.de/elke/pgp_signature.html>
Elke Kasimir <elke.kasimir@catmint.de> wrote,
I'm planning a new cli/odbc-based database connectivity library for Haskell 98 and want to manage hidden state (various management information) on the Haskell side.
Some libs. i.e. for gui, extend the IO monad for this using some "start" function:
main :: IO () main = start prog
prog :: GUI () ...
I could do the same with my library, but then it is difficult to combine, for example, Gui operations with Db operations.
[...proposal for standardised monad transformers...] Personally, I would stick with the IO monad. As you have illustrated, a rather heavy mechanism is needed for combining state transformers and your are getting into rather complicated overloading and typing issues. The result of which are at least that when a user of your library gets a type error, it will probably be cluttered with cryptic contexts and such. The from a functional programming point of view certainly ugly, but, on the other hand, very pragmatic solution is to have your database library use `IORef's to maintain your global state. Then, all your operations can work in the plain IO monad and you have no problem combining with other libraries using the IO monad. In fact, it is actually possible to argue that this is a clean solution, because all you are doing is to extend the state maintained by the IO monad. Cheers, Manuel
On 16-Feb-2001 Manuel M. T. Chakravarty wrote:
Elke Kasimir <elke.kasimir@catmint.de> wrote, (...) [...proposal for standardised monad transformers...]
Personally, I would stick with the IO monad.
Beyound personal preference, in my case having an "own" monad has some objective advantages: (a) initialization and clean-up is automatically done in the right places, (b) there are database operations (DML) which should always be performed inside of transaction, and others (DDL) which should be performed outside a transaction. With the help of an (additional) monad context, this is checked at compile time. (c) with an additional context for transactions, unadvertantly nesting of transactions is detected at runtime.
As you have illustrated, a rather heavy mechanism is needed for combining state transformers and your are getting into rather complicated overloading and typing issues. The result of which are at least that when a user of your library gets a type error, it will probably be cluttered with cryptic contexts and such.
There is surely the possibility of tricky errors when initially setting up the desired context. In the rest of the program, typing issues and type errors should be rather simple, as functions using the library always have function results of type: (Io m, Gui a, Db m) => m a (where some of the contexts may be sometimes ommitted), which I find rather intuitive and tranparent.
The from a functional programming point of view certainly ugly, but, on the other hand, very pragmatic solution is to have your database library use `IORef's to maintain your global state.
It is even acceptable for me to manage the state in C - independent of the API design - but then some time there will be the question: Why do I always say that that Haskell is the better programming language, when I'm really doing all the tricky stuff in C?...
Then, all your operations can work in the plain IO monad and you have no problem combining with other libraries using the IO monad. In fact, it is actually possible to argue that this is a clean solution, because all you are doing is to extend the state maintained by the IO monad.
Given that IO represents the "state of the world", at least parts of "my" state (which represents state outside of Haskell) have a natural right to reside in IO. But going back to software design issues, what IO actually does not is representing the state of the world in a pretty modular fashion. Consequences: Given that here on the mailing list there is the opinion that "sticking to IO" is to be preferred, and that a "monad composition" approach requires measures beyound one single library, I should probably offer both solutions, the "sticking to IO" solution being the standard, and the other being declared experimental. Elke.
Cheers, Manuel
_______________________________________________ Haskell mailing list Haskell@haskell.org http://www.haskell.org/mailman/listinfo/haskell
--- "If you have nothing to say, don't do it here..." Elke Kasimir Skalitzer Str. 79 10997 Berlin (Germany) fon: +49 (030) 612 852 16 mail: elke.kasimir@catmint.de> see: <http://www.catmint.de/elke> for pgp public key see: <http://www.catmint.de/elke/pgp_signature.html>
Elke Kasimir <elke.kasimir@catmint.de> wrote,
On 16-Feb-2001 Manuel M. T. Chakravarty wrote:
Elke Kasimir <elke.kasimir@catmint.de> wrote, (...) [...proposal for standardised monad transformers...]
Personally, I would stick with the IO monad.
Beyound personal preference, in my case having an "own" monad has some objective advantages: (a) initialization and clean-up is automatically done in the right places, (b) there are database operations (DML) which should always be performed inside of transaction, and others (DDL) which should be performed outside a transaction. With the help of an (additional) monad context, this is checked at compile time. (c) with an additional context for transactions, unadvertantly nesting of transactions is detected at runtime.
Re (a): Usually, you have to process command line options etc, which also provides a natural place for initialization. See, eg, the `init' function for Gtk+HS http://cvs.gnome.org/lxr/source/gtk%2bhs/gtk/GtkMain.chs Clean up should really be handled by a finaliser that is automatically invoked before program termination, but unfortunately, there is no standard Haskell 98 support for that. Re (b) & (c): Yes, these are good points.
The from a functional programming point of view certainly ugly, but, on the other hand, very pragmatic solution is to have your database library use `IORef's to maintain your global state.
It is even acceptable for me to manage the state in C - independent of the API design - but then some time there will be the question: Why do I always say that that Haskell is the better programming language, when I'm really doing all the tricky stuff in C?...
Sure - therefore, I proposed to use `IORef's rather than C routines.
Consequences:
Given that here on the mailing list there is the opinion that "sticking to IO" is to be preferred, and that a "monad composition" approach requires measures beyound one single library, I should probably offer both solutions, the "sticking to IO" solution being the standard, and the other being declared experimental.
Sounds like a good idea to me. It has an additional benefit: Existing GUI libraries - like Gtk+HS - which are based on the IO monad can be used in conjunction with your library, too. Cheers, Manuel
Sun, 18 Feb 2001 15:01:18 +1100, Manuel M. T. Chakravarty <chak@cse.unsw.edu.au> pisze:
Re (a): Usually, you have to process command line options etc, which also provides a natural place for initialization. See, eg, the `init' function for Gtk+HS
http://cvs.gnome.org/lxr/source/gtk%2bhs/gtk/GtkMain.chs
Clean up should really be handled by a finaliser that is automatically invoked before program termination,
Shutdown can be provided in the form similar to Socket.withSocketsDo :: IO a -> IO a so that it still can be forgotten, but only together with initialization, and the shutdown code may use some handles created by initialization without using global variables. If the initialization produces a handle which is used for all subsequent operations (i.e. the C interface does not rely on the global state), it can be passed as an argument to the argument to this function and used explititly each time - but this time wrapping the whole thing in a monad makes more sense, as it saves the programmer from passing the handle explicitly. -- __("< Marcin Kowalczyk * qrczak@knm.org.pl http://qrczak.ids.net.pl/ \__/ ^^ SYGNATURA ZASTÊPCZA QRCZAK
participants (3)
-
Elke Kasimir -
Manuel M. T. Chakravarty -
qrczak@knm.org.pl