Bracket: a Tale of Partially Applied Functions

TL;DR

In this post, we describe how we can use partially applied functions as a design building block though the study of a practical example: the bracket function.

I’ll use the Haskell programming language to illustrate this post. Just keep in mind this could be applied to almost any language.

It all Begins with Code Reuse

HSpec is a BDD-style unit-test framework for Haskell. In that kind of tests, it is quite common to create and destroy resources such as database handles, complex data structures, an HTTP server, etc.

I was looking for a way to share a resource across several it clauses without having to duplicate its instantiate/tear-down process. I quickly found the around function and have been instantly confused by its type.

around :: (ActionWith a -> IO ()) -> SpecWith a -> SpecWith a

ActionWith and SpecWith being some alias types, let me de-sugar it for you.

around :: ((a -> IO ()) -> IO ()) -> SpecM a () -> SpecM a ()

I know, strange right?

The SpecM monad is a reader monad used internally by HSpec, this reader monad contains the newly injected resource among other things. We can omit that part for now.

If your mouth started foaming after “reader monad” (it shouldn’t), you can just picture it as a small read-only key-value database.

What’s really interesting here is this part:

((a -> IO ()) -> IO ()) -> [...]

What is this thing?

Well, to answer that, we first need to understand the bracket function.

The Bracket Function

Let’s have a look at its type:

IO a -> (a -> IO b) -> (a -> IO c) -> IO c

Let’s keep in mind its purpose: it’s a way to create and destruct a resource with side effects, preventing any leak after using it.

Despite being a bit intimidating, this signature actually makes sense.

As explained in this Haskel wiki page, the above type signature can be split into 3 parts:

  • IO a: the constructor. This function will return the resource a in a IO monad.
  • (a -> IO b): the destructor. This function will take as parameter the previously created a resource and will apply the necessary IO actions to destroy it.
  • (a -> IO c): the actual effect-full computation that will use the a resource.
  • IO c: the final result coming from the previous computation.

Understanding the Around Function

Let’s go back to the around function, it just makes sense now: it is a partially applied bracket function. A bracket function to which we already provided a constructor and a destructor.

around ::                         ((a -> IO ()) -> IO ()) -> SpecM a () -> SpecM a ()
bracket :: IO a -> (a -> IO b) ->  (a -> IO c ) -> IO c

Ah! Pretty awesome don’t you think?

Personally, I’m found of this small simple function. What a great simple way to express a resource life cycle!

A Practical Example with HSpec

Alright, now we understand the handle function signature, let’s see how we can use it in HSpec.

Let say we want to test our implementation of the great Star Wars names database.

[...]
 describe "getStarWarsNameDb" $ do
    it "contains Star Wars characters names" $ do
        hDb <- getStarWarsNameDb
        db <- readDb hDb
        ("Dark Vader" `isIn` db) `shouldNotBe` True
        ("JarJar Beans" `isIn` db) `shouldBe` True
        clear hDb
    it "contains Star Wars citations" $ do
        hDb <- getStarWarsNameDb
        db <- readDb hDb
        ("May the fourth be with you." `isIn` db) `shouldBe` True
        ("Do. Or do not. There will be a trial." `isIn` db) `shouldBe` True
        clear hDb
[...]

As you can see, we have some code duplication here, we want to get rid of both the construction and the destruction of the db database.

First, let’s create a partially applied bracket call to which we already provided both a constructor and a destructor:

withDb:: ((StarWarsDb) -> IO ()) -> IO ()
withDb = bracket getStarWrsNameDb clear

We then just need to adapt each it clause to be pattern matched against a function having a single argument: the database handle.

[...]
 around withDb describe "getStarWarsNameDb" $ do
    it "contains Star Wars characters names" $ \hDb -> do
        db <- readDb hDb
        ("Dark Vader" `isIn` db) `shouldNotBe` True
        ("JarJar Beans" `isIn` db) `shouldBe` True
    it "contains Star Wars citations" $ \hDb -> do
        db <- readDb hDb
        ("May the fourth be with you." `isIn` db) `shouldBe` True
        ("Do. Or do not. There will be a trial." `isIn` db) `shouldBe` True
[...]

Neat, right!

We could go even further thanks to the polymorphic nature of the around function:

[...]
withDb:: (((StarWarsDb, StarWarsDbContent) -> IO ()) -> IO ()
withDb = bracket getDbContent (\(hDb,_) -> clear hDb)
    with
      getDbContent = do
          hdb <- createDb
          db <- readDb hdb
          return (hdb, db)

[...]
 around withDb describe "getStarWarsNameDb" $ do
    it "contains Star Wars characters names" $ \(hdb, db) -> do
        ("Dark Vader" `isIn` db) `shouldNotBe` True
        ("JarJar Beans" `isIn` db) `shouldBe` True
    it "contains Star Wars citations" $ \(hdb, db) -> do
        ("May the fourth be with you." `isIn` db) `shouldBe` True
        ("Do. Or do not. There will be a trial." `isIn` db) `shouldBe` True
[...]

See, all we needed to do was to alter the withDb function and the pattern matching of the anonymous functions used in the in clause.

Wrap Up

Partially applied functions are an amazing tool. Thanks to them, we can add context little by little to a function call. Used properly, this can lead to some nice design tricks such as the bracket function.

Hope you enjoyed this ode to the bracket function :). We are constantly taking for granted some beautiful functions such as this one. I think it is important to both acknowledge and celebrate that, sometimes, software can be beautiful!