Roman Liutikov
Roman Liutikov

Reputation: 1348

Function composition and type annotations in Haskell

Total noob here. I've seen an example of doing HTTP Get request using do block, but I want to do it via composition.

Like this:

get url = getResponseBody . simpleHTTP $ getRequest url

Is it proper? And what is the right type annotation for this function?

This one is ok:

get :: String -> IO String
get url = getResponseBody =<< simpleHTTP (getRequest url)

But I want to compose and not to bind. What it a better/right way?

Upvotes: 1

Views: 290

Answers (1)

bheklilr
bheklilr

Reputation: 54058

If we look at the types:

getRequest :: String -> Request_String
simpleHTTP :: Request ty -> IO (Result (Response ty))
getResponseBody :: Result (Response ty) -> IO ty
type Request_String = Request String

So for the purposes of this question we can specialize the types to

simpleHTTP :: Request_String -> IO (Result (Response String))
getResponseBody :: Result (Response String) -> IO String

It's pretty clear to see that we can just do

simpleHTTP (getRequest url) :: IO (Result (Response String))

However, in order to compose this with getResponseBody we are required to use monadic combinators. There are all sorts of math-y reasons for this, but for simplicity's sake just think of it as function composition not being powerful enough to compose actions together, only functions. To compose actions, monads were introduced and they come with their own composition operators instead. Compare the types of these two composition operators:

(.)   ::            (b ->   c) -> (a ->   b) -> (a ->   c)
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)  -- Imported from Control.Monad

They're very similar, aren't they? The only difference is the addition of the m with the Monad constraint. This is the composition operator for monads, it is more restrictive and powerful than the normal . operator, which means it can do more, such as sequencing and executing IO actions, but it also means that it works with fewer types. It can be used as

get = simpleHTTP <=< simpleHTTP . getRequest

However, there is a more generalized version of composition defined in Control.Category that can handle both:

import Prelude hiding ((.), id)  -- Hide the less general versions in Prelude
import Control.Category
import Control.Arrow

get = runKleisli (Kleisli getResponseBody . Kleisli simpleHTTP) . getRequest

But this requires that you wrap your monadic functions in the Kleisli newtype wrapper and unwrap it with runKleisli. You can also skip Control.Category and just use Control.Arrow:

get = runKleisli (Kleisli getResponseBody <<< Kleisli simpleHTTP) <<< getRequest

You can even swap this around to be

get = getRequest >>> runKleisli (Kleisli simpleHTTP >>> Kleisli getResponseBody)

if you want it to read left-to-right instead of right-to-left.

In the end, there are several ways to write equivalent code, but the most familiar and readable to most people, if you are adamant on pointfree style (it isn't necessary to be idiomatic), would be to use the usual . and <=< since it is the shortest implementation and uses more recognizable operators. Control.Category and Control.Arrow are very useful when building something custom, but for the built-in types there are usually already operators defined that make more sense.

Upvotes: 5

Related Questions