I'm splitting off the Managed
type from the mvc
library into its own stand-alone library. I've wanted to use this type outside of mvc
for some time now, because it's an incredibly useful Applicative
that I find myself reaching for in my own code whenever I need to acquire resources.
If you're not familiar with the Managed
type, it's simple:
-- The real implementation uses smart constructors
newtype Managed a =
Managed { with :: forall r . (a -> IO r) -> IO r }
-- It's a `Functor`/`Applicative`/`Monad`
instance Functor Managed where ...
instance Applicative Managed where ...
instance Monad Managed where ...
-- ... and also implements `MonadIO`
instance MonadIO Managed where ...
Here's an example of mixing the Managed
monad with pipes
to copy one file to another:
import Control.Monad.Managed
import System.IO
import Pipes
import qualified Pipes.Prelude as Pipes
main = runManaged $ do
hIn <- managed (withFile "in.txt" ReadMode)
hOut <- managed (withFile "out.txt" WriteMode)
liftIO $ runEffect $
Pipes.fromHandle hIn >-> Pipes.toHandle hOut
However, this is not much more concise than the equivalent callback-based version. The real value of the Managed
type is its Applicative
instance, which you can use to lift operations from values that it wraps.
Equational reasoning
My previous post on equational reasoning at scale describes how you can use Applicative
s to automatically extend Monoid
s while preserving the Monoid
operations. The Managed
Applicative
is no different and provides the following type class instance that automatically lifts Monoid
operations:
instance Monoid a => Monoid (Managed a)
Therefore, you can treat the Managed
Applicative
as yet another useful building block in your Monoid
tool box.
However, Applicative
s can do more than extend Monoid
s; they can extend Category
s, too. Given any Category
, if you extend it with an Applicative
you can automatically derive a new Category
. Here's the general solution:
import Control.Applicative
import Control.Category
import Prelude hiding ((.), id)
newtype Extend f c a b = Extend (f (c a b))
instance (Applicative f, Category c)
=> Category (Extend f c) where
id = Extend (pure id)
Extend f . Extend g = Extend (liftA2 (.) f g)
So let's take advantage of this fact to extend one of the pipes
categories with simple resource management. All we have to do is wrap the pull-based pipes
category in a bona-fide Category
instance:
import Pipes
newtype Pull m a b = Pull (Pipe a b m ())
instance Monad m => Category (Pull m) where
id = Pull cat
Pull p1 . Pull p2 = Pull (p1 <-< p2)
Now we can automatically define resource-managed pipes by Extend
ing them with the Managed
Applicative
:
import Control.Monad.Managed
import qualified Pipes.Prelude as Pipes
import System.IO
fromFile :: FilePath -> Extend Managed (Pull IO) () String
fromFile filePath = Extend $ do
handle <- managed (withFile filePath ReadMode)
return (Pull (Pipes.fromHandle handle))
toFile :: FilePath -> Extend Managed (Pull IO) String X
toFile filePath = Extend $ do
handle <- managed (withFile filePath WriteMode)
return (Pull (Pipes.toHandle handle))
All we need is a way to run Extend
ed pipes and then we're good to go:
runPipeline :: Extend Managed (Pull IO) () X -> IO ()
runPipeline (Extend mp) = runManaged $ do
Pull p <- mp
liftIO $ runEffect (return () >~ p)
If we compose and run these Extend
ed pipes they just "do the right thing":
main :: IO ()
main = runPipeline (fromFile "in.txt" >>> toFile "out.txt")
Let's check it out:
$ cat in.txt
1
2
3
$ ./example
$ cat out.txt
1
2
3
We can even reuse existing pipes, too:
reuse :: Monad m => Pipe a b m () -> Extend Managed (Pull m) a b
reuse = Extend . pure . Pull
main = runPipeline $
fromFile "in.txt" >>> reuse (Pipes.take 2) >>> toFile "out.txt"
... and reuse
does the right thing:
$ ./example
$ cat out.txt
1
2
What does it mean for reuse
to "do the right thing"? Well, we can specify the correctness conditions for reuse
as the following functor laws:
reuse (p1 >-> p2) = reuse p1 >>> reuse p2
reuse cat = id
These two laws enforce that reuse
is "well-behaved" in a rigorous sense.
This is just one example of how you can use the Managed
type to extend an existing Category
. As an exercise, try to take other categories and extend them this way and see what surprising new connectable components you can create.
Conclusion
Experts will recognize that Managed
is a special case of Codensity
or ContT
. The reason for defining a separate type is:
- simpler inferred types,
- additional type class instances, and:
- a more beginner-friendly name.
Managed
is closely related in spirit to the Resource
monad, which is now part of resourcet
. The main difference between the two is:
Resource
preserves the open and close operationsManaged
works for arbitrary callbacks, even unrelated to resources
This is why I view the them as complementary Monad
s.
Like all Applicative
s, the Managed
type is deceptively simple. This type does not do much in isolation, but it grows in power the more you compose it with other Applicative
s to generate new Applicative
s.
This comment has been removed by the author.
ReplyDelete