idiom for different implementations of same idea
Hi all, I'm looking for some advice on what's the cleanest way to implement something. The basic idea is that I have a task to solve, T. There are many steps to solving this task, but they can be broken down into a small list of elementary steps: - prepareData - initialize - doThingOne - doThingTwo - getResults where the main driver does something like: prepareData initialize iterate until converged doThingOne doThingTwo getResults As is standard in my field (statistical natural langauge processing), I have several models defined to perform task T (though only the first, most basic one is implemented). Call these Model0, Model1, Model2, and so on. All of these models, since they're solving the same basic task, have the same basic types on their functions, something like (a bit simplified, but): prepareData :: Data () -> Data markup initialize :: Data markup -> ST s table doThingOne :: Data markup -> table -> ST s alignments doThingTwo :: Data markup -> alignments -> ST s table getResults :: Data markup -> table -> alignments -> String Simple enough. Now, say I have three models (0-2) which implement these functions with varying complexities (also of varying complexities in terms of the types of 'markup', 'table' and 'alignments'). Each model has a different idea of what these three types are. Now, I want in my executable my user to be able to say "-model=0" and so on in the command line and for it to use the appropriate model. Each of these models will go in a separate module. One way to do this would be to import all of the models qualified and then if they choose Model0, pass to the "go" function Model0.prepareData, Model0.initialize, etc. This is fine, simple, good. But it doesn't enforce at all the types of the functions. Another way to go would be to make a class, something like: class Model model markup table alignments | model -> markup, table, alignments where prepareData :: model -> Data () -> Data markup initialize :: model -> Data markup -> ST s table doThingOne :: model -> Data markup -> table -> ST s alignments doThingTwo :: model -> Data markup -> alignments -> ST s table getResults :: model -> Data markup -> table -> alignments -> String where the model type/parameter is essentially a dummy to tie everything together. This could be implemented a bit more cleaning with the definition: data T a and then the first parameter for all of those class functions changing from "model" to "T model". Each model module would then have it's own datatype, something like:
(in module Model0:) data Model0
instance Model Model0 Int Table Al where ...
This is another option; however I don't think it's very clean for two reasons: - there are a lot of fundeps; in the actual application there are a few more type variables than I presented here, and it gets very long :) - the model parameter is used only to determine which model we're using and doesn't actually do anything other than satisfy the typechecker There are probably a plethora of alternatives I haven't considered, but I'm sure people have done something similar to this before and I'm curious how they handled it... Thanks for reading this far :) - Hal -- Hal Daume III "Computer science is no more about computers | hdaume@isi.edu than astronomy is about telescopes." -Dijkstra | www.isi.edu/~hdaume
On Thu, Aug 01, 2002 at 02:34:00PM -0700, Hal Daume III wrote:
... Now, I want in my executable my user to be able to say "-model=0" and so on in the command line and for it to use the appropriate model. Each of these models will go in a separate module.
One way to do this would be to import all of the models qualified and then if they choose Model0, pass to the "go" function Model0.prepareData, Model0.initialize, etc. This is fine, simple, good. But it doesn't enforce at all the types of the functions.
I don't understand what you mean by this. Surely the "go" function is polymorphic over the types in the model, but requires matching types? I don't think you can get Haskell to not "enforce at all the types of the functions". I must be missing something. --Dylan
One way to do this would be to import all of the models qualified and then if they choose Model0, pass to the "go" function Model0.prepareData, Model0.initialize, etc. This is fine, simple, good. But it doesn't enforce at all the types of the functions.
I don't understand what you mean by this. Surely the "go" function is polymorphic over the types in the model, but requires matching types?
Right right, in the actual "driver" program the types are checked; what I mean is that there isn't anything tying the different Model modules together in the way that a class would. It's not a huge differentiation, but, eh...
I don't think you can get Haskell to not "enforce at all the types of the functions". I must be missing something.
I would hope not :) ...though there is unsafePerformIO... ;) - Hal
Hal Daume III writes: | Hi all, | | I'm looking for some advice on what's the cleanest way to implement | something. : | where the main driver does something like: | | prepareData | initialize | iterate until converged | doThingOne | doThingTwo | getResults | | As is standard in my field (statistical natural langauge | processing), I have several models defined to perform task T | (though only the first, most basic one is implemented). Call these | Model0, Model1, Model2, and so on. If the following assumptions hold: - Model0.prepareData and Model0.initialize and ... and Model0.getResults are only ever used together - The `iterate until converged' aspect is the only major thing in the main driver which needs to be shared among the models then I'd be inclined to reduce the amount of traffic between modules thus: - Export only a doItAll function from each model's module - Write an iterateUntilConverged function which all the models' modules can use, with a type signature along the lines of iterateUntilConverged :: (Table -> ST s Alignments) -> (Alignments -> ST s Table) -> Table -> ST s (Alignments, Table) - In each model's module: doItAll dataUnit = do let dm = prepareData dataUnit -- "do let": the Enid Blyton idiom t <- initialize dm (as,t') <- iterateUntilConverged (doThingOne dm) (doThingTwo dm) t getResults dm as t' Regards, Tom
Hal Daume III writes:
Hi all,
I'm looking for some advice on what's the cleanest way to implement something. The basic idea is that I have a task to solve, T. There are many steps to solving this task, but they can be broken down into a small list of elementary steps:
- prepareData - initialize - doThingOne - doThingTwo - getResults
In similar situations, especially if there is more than one useful way to use the various parts of an algorithm, I used often prefer existentials: data Model = forall markup table alignments. Model { prepareData :: Data () -> Data markup, initialize :: Data markup -> ST s table, doThingOne :: Data markup -> table -> ST s alignments, doThingTwo :: Data markup -> alignments -> ST s table, getResults :: Data markup -> table -> alignments -> String } So each model-module only needs to export one model record and the standard algorithm can be implemented easily: (simplified, not checked for stupid errors) execModel model data = let mark = prepareData model $ data in runST $ do tab <- (initialize model) mark align <- (doThingOne model) mark tab res <- (doThingTwo model) mark align return $ (getResults model) mark res align Of course, by using existentials, no model internal type can escape.
Now, I want in my executable my user to be able to say "-model=0" and so on in the command line and for it to use the appropriate model. Each of these models will go in a separate module.
This way one can even program a top-level like doForAll data = map (\m -> execModel m data) [model0, model1, model2, ...] which is rather useful. Regards, Harald
Hi,
In similar situations, especially if there is more than one useful way to use the various parts of an algorithm, I used often prefer existentials:
data Model = forall markup table alignments. Model { prepareData :: Data () -> Data markup, initialize :: Data markup -> ST s table, doThingOne :: Data markup -> table -> ST s alignments, doThingTwo :: Data markup -> alignments -> ST s table, getResults :: Data markup -> table -> alignments -> String }
I like this a lot! The only problem is I don't think GHC does. It complains: /nfs/isd/hdaume/projects/MESumm/new/Model.hs:13: Can't combine named fields with locally-quantified type variables In the declaration of data constructor Model In the data type declaration for `Model' ....
Von: Hal Daume III Hi,
In similar situations, especially if there is more than one useful way to use the various parts of an algorithm, I used often prefer existentials:
data Model = forall markup table alignments. Model { prepareData :: Data () -> Data markup, initialize :: Data markup -> ST s table, doThingOne :: Data markup -> table -> ST s alignments, doThingTwo :: Data markup -> alignments -> ST s table, getResults :: Data markup -> table -> alignments -> String }
I like this a lot! The only problem is I don't think GHC does. It complains:
/nfs/isd/hdaume/projects/MESumm/new/Model.hs:13: Can't combine named fields with locally-quantified type variables In the declaration of data constructor Model In the data type declaration for `Model'
.... Yuck, now I remember this nasty problem, each time I stumble over it again... The solution is easy, but not very nice:
data Model = forall markup table alignments. Model (Data () -> Data markup) {- prepare -} (Data markup -> ST s table) {- init -} (Data markup -> table -> ST s alignments) {- ThingOne -} (Data markup -> alignments -> ST s table) {- ThingTwo -} (Data markup -> table -> alignments -> String) {- result -} I think the reason to go through such loops is that function like 'prepareData' are not typeable in the current type system. Perhaps one of the experts can tell us more? Regards, Harald
participants (4)
-
Dylan Thurston -
Hal Daume III -
holtmannh@t-online.de -
Tom Pledger