I'm pretty sure this is explained better elsewhere; please google around. Also sorry if this is hella pedantic.
Suppose you have some (abstract) type representing some resource that you want to use. A library exposing ways to aqcuire such resources might expose only a callback-based function to wrap usage of the resource to avoid leakage, like so:
-- ResourceID is something that names a resource. For example, it could be a
-- file descriptor, a socket, or a shared object. A Resource is the actual
-- handle with which we can use a resource.
withResource :: ResourceID -> (Resource -> IO a) -> IO a
-- Some function that uses an acquired resource
useResource :: ResourceID -> IO Int
useResource resID = withResource resID $ \resource -> do
-- Imagine that askForInt is some operation defined on resources:
-- askForInt :: Resource -> IO Int
value <- askForInt resource
value' <- askForInt resource
return $ value + value'
In the above, the lambda passed to withResource
forms a kind of scope over
the resource's usage, and it can only be used indirectly through a Handle
inside. In this way, withResource
can localize usage, and also do things like
making sure that resource acquisition is always paired with resource release.
This comes up often:
-- From System.IO
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
-- or, more clearly,
withFile' :: (FilePath, IOMode) -> (Handle -> IO r) -> IO r
withFile' = uncurry withFile
-- From Foreign.C.String
withCString :: String -> (CString -> IO a) -> IO a
-- From Foreign.ForeignPtr
withForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b
Hopefully the above examples seem very similar. (The fact that many of these
kinds of functions have IO
in their result types is because many practical
resources need to be managed in IO
, like files or memory buffers).
The question posed, then, is: given a hypothetical withResource
function as
described earlier, how can we use it with multiple resources at a time? The
simplest example is forming nested scopes when using 2 resources:
processTwo :: ResourceID -> ResourceID -> IO Int
processTwo resID resID' =
withResource resID $ \resource ->
withResource resID' $ \resource' -> do
value <- askForInt resource
value' <- askForInt resource'
return $ value + value'
The scope of the first resource extends over the 2nd usage in the argument to
the 2nd call to withResource
, but not vice versa, so the order goes like:
- Acquire resource 1
- Acquire resource 2
- Use resources 1 and 2
- Release resource 2
- Release resource 1
...which is the familiar stack-based RAII-style behavior like in C++.
What if we have more than 2 resources? Worse, what if we have some number of resources known only at runtime? Like a list of them?
-- Like a list of them.
withResources :: [ResourceID] -> ([Resource] -> IO a) -> IO a
withResources [] f = f []
withResources (resID:resIDs) f =
withResource resID $ \resource ->
withResources resIDs $ \resources ->
f (resource:resources)
The above just does recursively (on a list) what we did in the example with 2 resources: the resources are acquired in the order as they appear in the given list, the given callback is invoked, and the resources are released in reverse order.
Er, so it appears what what we're doing here is something like taking:
a -> (b -> m r) -> m r
and turning it into
f a -> (f b -> m r) -> m r
which in this case looks more like taking:
a -> m b
and turning it into
f a -> m (f b)
,given that we're transforming continuations?
Is there a general way to change Kleisli arrows in this way? It sorta looks like transformer lifting(???) but the result value is a monad in
f b
, notb
, andm
is not a transformer here; just something that composes withf
.While writing this I just realized that this is just
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
. Might as well leave this here as a reminder of my stupidity.