Reputation: 1348
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
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