This confused me for a long time too! But figuring it out gave me a helpful technique for understanding Haskell library types.
First I'll start with my middleware being undefined:
myMiddleware :: Middleware
myMiddleware = undefined
So what is Middleware
? The key is to look at the definition of the type:
type Middleware = Application -> Application
Let's start at the first layer (or level of abstraction) by having the middleware take an Application and return an Application. We don't know how to modify an application, so we'll return exactly what's passed in for now.
myMiddleware :: Application -> Application
myMiddleware theOriginalApp = theOriginalApp
But what is an Application? Again, let's turn to Hackage:
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
An Application is a function! We may not know exactly what each part is supposed to do or be, but we can find out. Let's replace Application
in our type signature with the function type:
myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
-> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
myMiddleware theOriginalApp = theOriginalApp
Now we can see this type should allow us to access a Request
! But how do we use it?
We can expand theOriginalApp
in the function definition into a lambda expression that matches the return type:
myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
-> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
myMiddleware theOriginalApp = (\req sendResponse -> undefined)
We can do whatever we want with the request now:
myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
-> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
myMiddleware theOriginalApp = (\req sendResponse ->
let myModifiedRequest = addSomeHeadersIfMissing req in
undefined)
Now what about that undefined
? Well, we're trying to match our lambda to the type of that return function, which takes a Request and a function (that we don't care about) and returns an IO ResponseReceived
.
So, we need something that can use myModifiedRequest
and return an IO ResponseReceived
. Luckily our type signature indicates that theOriginalApp
has the right type! To make it fit, we only need to give it the sendResponse
function too.
myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
-> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
myMiddleware theOriginalApp = (\req sendResponse ->
let myModifiedRequest = addSomeHeadersIfMissing req in
theOriginalApp myModifiedRequest sendResponse)
And that's it, that will work! We can improve the readability by simplifying the type annotation back to Middleware
, and getting rid of the lambda. (We can also eta-reduce and remove the sendResponse
term from both the arguments and the definition, but I think it's clearer if it stays.)
The result:
myMiddleware :: Middleware
myMiddleware theOriginalApp req sendResponse =
let myModifiedRequest = addSomeHeadersIfMissing req in
theOriginalApp myModifiedRequest sendResponse