Matvey Aksenov
Matvey Aksenov

Reputation: 3900

Common interface for several modules

I want to give a user an ability to run the program with import DryRun first and then with import Do if he thinks everything is correct.
So there is a module which does actual work:

doThis ∷ SomeStack ()
doThis = actuallyDoThis
...
doThat ∷ SomeStack ()
doThat = actuallyDoThat

and a module for shy user:

doThis ∷ SomeStack ()
doThis = liftIO $ putStrLn "DoThis"
...
doThat ∷ SomeStack ()
doThat = liftIO $ puStrlLn "DoThat"

The problem is I cannot be sure that interfaces in Do and DryRun are the same (compiler cannot help) and this mess is hard to maintain during development.
Are there any common idioms to solve this kind of problem?

Upvotes: 3

Views: 165

Answers (1)

yatima2975
yatima2975

Reputation: 6610

You could reify the modules in question. That is, in a base module define a datatype:

module Base where
data MyModule = MyModule {
    doThis_ :: SomeStack (),
    doThat_ :: SomeStack ()
}

with the stuff you need each module to export (The reason for the underscores will become apparent soon).

Then you could in each of the modules define values of this type, e.g.:

module DryRun(moduleImpl) where
import Base
moduleImpl :: MyModule
moduleImpl = MyModule doThis doThat
-- doThis and doThat defined as above, with liftIO . putStrLn

module Do(moduleImpl) where
import Base
moduleImpl :: MyModule
moduleImpl = MyModule doThis doThat
-- doThis and doThat defined as above, where the real work gets done

I don't construct the MyModule values using record syntax to make sure that if MyModule changes, the type checker will start complaining in most cases.

In the client module you could do

module Client where
import DryRun
-- import Do -- uncomment as needed

doThis = doThis_ moduleImpl
doThat = doThat_ moduleImpl

-- do whatever you want here

Now you know that the same operations are exported by both modules. This is tedious and clunky, for sure, but since Haskell doesn't have first-class modules, you'll always have to work around the limitations of the module system. The good thing is that you only have to write the Base and Client modules once, and that you can start defining combinators operating on MyModule values. E.g.

doNothing = MyModule (return ()) (return ())
addTracing impl = MyModule ((liftIO $ putStrLn "DoThis") >> doThis_ impl) 
                           ((liftIO $ putStrLn "DoThat") >> doThat_ impl)

will allow you to replace the DryRun module implementation by addTracing doNothing if I'm not mistaken.

Upvotes: 2

Related Questions