How do I correctly define a Haskell module containing functions with a shared variable parameter?
I am trying to write a Haskell module that defines functions for a remote XML-RPC API using the haxr library . Here's how the haxr documentation suggests you define a Haskell function that calls examples.add
on the server in 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
It looks like I have one or two XML-RPC methods (then I don't need a separate module). However, duplication server
in the code is a problem since I have about 100 functions. I cannot define server
in a module, for example:
someRemote :: Remote
someRemote = remote "http://example.com/XMLRPC"
add :: Int -> Int -> IO Int
add = someRemote "examples.add"
as a url cannot be hardcoded if it needs to be flexible for the code that uses it. I also cannot define someRemote
as a function parameter, as it has the same duplication problem.
The Haxr examples do not provide any guidance on how to fix this problem.
I usually write programs in imperative OOP languages (i.e. Java, Python). If I were using these languages, I would define a class with a constructor that accepts server
all functions using an object instance variable server
, instead of asking for the calling code for it.
I've looked for the equivalent of this in Haskell, but I don't seem to know the correct keywords to find it. Generic classes don't seem to be the answer. I could write a higher-order function that returns partially applied functions, but unpacking them would be even uglier.
source to share
I'm not entirely sure if "server duplication" is actually a bad thing. Of course, you should never duplicate a long literal, but for a single variable name that doesn't interfere with the code much and is easy to replace, it shouldn't be a big problem.
But of course, you can easily avoid this duplication by adding a shared variable to the monad you are working in, similar to how you attach it to an OO class object. This is called the 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
source to share
You can simply emulate Haskell's OOP approach 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 Server on every call, but now you can change the server view (for example, make it a handle to a persistent connection).
Alternatively, you can use the reader monad to make the transfer implicit by the server:
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 class, any reader monad you use can be extended to support an implicit server argument.
source to share