4
votes

Haskell beginner here, trying to wrap a HTTP REST API in a safe way and with automatic Aeson decoding of return values. I started with a Haskell function for every API call. It was a bit boilerplateish, but ok.

Looking to improve things I wanted to turn each API call into a data type of its own. For example for logging in, I would model that as Login type that specifies the method, Credentials type for method parameters and LoginReponse for the result of the API call. Parameters and response types of course have corresponding FromJSON and ToJSON instances.

For two API calls it looks something like this (using GADTs):

data Void           = Void
data Credentials    = Credentials {...}
data LoginResponse  = LoginResponse {...}
data LogoutResponse = LogoutResponse {...}

data Command a where
    Login  :: Credentials -> Command LoginResponse
    Logout :: Void        -> Command LogoutResponse

execute :: FromJSON a => Command a -> IO a
execute cmd = do
    manager <- newManager tlsManagerSettings
    let request = buildHttpRequest cmd

    result <- httpLbs request manager
    let body = responseBody result
    let parsed = fromJust $ decode body

    return parsed

This works great for my use case - I can introspect Commands before executing them, I can't construct invalid API calls and Aeson knows how to decode the return values!

Only problem with this approach is that I have to keep all of my Commands in a single file under single data declaration.

I'd like to move method definitions (in my example Login and Logout) to separate modules, but to keep the execute function similar, and of course keep the type safety and Aeson decoding.

I've tried to make something using type classes, but got nowhere.

Any tips how to do that are welcome!

1
I think you might be interested in servant.Zeta
Firstly, using an existing library like servant is definitely the way to go here for practical purposes. If this is about learning, then you can address this issue by using data families - allowing you to essentially have an "open" datatype. Then you can have a class with the buildHttpRequest request function, the Command data family, and anything else you need. As a side note, the type you have named "Void" is usually just called () in Haskell (Void is usually reserved for the empty type).user2407038

1 Answers

2
votes

Since the only thing you do differently in execute for different commands is call buildHttpRequest, I propose the following alternate data type:

type Command a = Tagged a HttpRequest

(I don't know the return type of buildHttpRequest, so I made something up and assumed it returned an HttpRequest. Hopefully the idea will be clear enough even though I'm sure I got this part wrong.) The Tagged type comes from tagged, and lets you attach a type to a value; it is the urphantom type. In our case, we will use the attached type to decide how to decode JSON in the execute step.

The type of execute needs a slight modification to demand that the attached type can be decoded:

execute :: FromJSON a => Command a -> IO a

However, its implementation remains basically unchanged; simply replace buildHttpRequest with untag. These commands are not conveniently introspectable, but you will be able to split things up along module boundaries; e.g.

module FancyApp.Login (Credentials(..), LoginResponse(..), login) where

import FancyApp.Types

data Credentials = Credentials
data LoginResponse = LoginResponse

login :: Credentials -> Command LoginResponse
login = {- the old implementation of buildHttpRequest for Login -}

module FancyApp.Logout (LogoutResponse(..), logout) where

import FancyApp.Types

data LogoutResponse = LogoutResponse

logout :: Command LogoutResponse
logout = {- the old implementation of buildHttpRequest for Logout -}