简体   繁体   中英

How to cleanly define a Haskell module containing functions with a common variable parameter?

I'm trying to write a Haskell module that defines functions for a remote XML-RPC API using the library haxr . Here's how haxr's documentation suggests you define a Haskell function that calls examples.add on the server at url :

add :: String -> Int -> Int -> IO Int
add url = remote url "examples.add"

called like this:

server = "http://localhost/~bjorn/cgi-bin/simple_server"
add server x y

This seems ok to me if I had one or two XML-RPC methods (I wouldn't need a seperate module then). However, the duplication of server in the code is a problem since I have close to 100 functions. I can't define server in the module, like this:

someRemote :: Remote
someRemote = remote "http://example.com/XMLRPC"

add :: Int -> Int -> IO Int
add = someRemote "examples.add"

since the URL can't be hard-coded if it is to be flexible for the code that uses it. I also can't define the someRemote as a parameter of the functions, as it has the same duplication issue.

Haxr's examples provide no clues on how to solve this.

I usually write programs in imperative OOP languages (ie Java, Python). If I were using those languages, I would define a class with an constructor that takes server , and all the functions using the object instances' server variable, rather than asking the calling code for it.

I've looked for an equivalent to this in Haskell, but I don't seem to know the right keywords to find it. Type classes don't seem to be the answer. I could write a higher order function that returns the partially applied functions, but unpacking those would be even uglier.

I'm not quite sure "duplication of server" is that bad actually. Of course you should never duplicate a lengthy literal, but for a single variable name that doesn't clutter the code much and is easy to replace this shouldn't be much of an issue.

But of course you can avoid such duplication easily, by attaching the shared variable to the monad you're working in, similar to how you'd attach it to an OO class object. That's called a reader .

import Control.Monad.Trans.Reader
type RemoteIO = ReaderT String IO  -- or perhaps `ReaderT Remote IO`

add :: Int -> Int -> RemoteIO Int
add x y = do
   url <- ask
   lift $ remote url "examples.add" x y

You could just emulate the OOP approach in Haskell, by wrapping the server in an "object" and passing it to all your "methods" as the first parameter:

module MyServer (
    Server, -- don't expose constructor
    newServer,
    add,
) where

data Server = Server String

newServer :: String -> IO Server
newServer = return . Server

add :: Server -> Int -> Int -> IO Int
add (Server url) = remote url "examples.add"

You still have to pass the Server every time you make a call, but now you can change the representation of Server (to make it a handle to a persistent connection, for example).

Additionally, you could use a reader monad to make passing the server implicit:

class MonadServer m where
    withServer :: (Server -> m a) -> m a

instance MonadServer (ReaderT Server m) where
    withServer f = ReaderT (\server -> runReaderT (f server) server)

add :: (MonadIO m, MonadServer m) => Int -> Int -> m Int
add x y = withServer (\(Server url) -> liftIO $ remote url "examples.add" x y)

By making MonadServer a typeclass, any reader monad you use can be extended to support the implicit Server argument.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM