PurelyFunctional.tv Newsletter 348: Tip: cleanup in finally

Issue 348 - October 14, 2019 · Archives · Subscribe

Clojure Tip 💡

cleanup in finally

There's a common pattern in code, where you need to create a resource, use it, then clean up the resource.

(let [resource (create-resource)]
  (use-resource resource)
  (cleanup-resource resource))

You can imagine the resource is anything that might need some cleanup. It could be opening a file, reading from it, then closing the reader. Or a database connection. Or a thread pool.

After a while, we may realize that the function use-resource could throw an exception. We want to catch that exception, so we do this:

(let [resource (create-resource)]
  (try
    (use-resource resource)
    (cleanup-resource resource)
    (catch Exception e
      (handle-exception e))))

This looks good, but unfortunately there's a problem. If use-resource throws an Exception, it will skip the cleanup. We could copy the cleanup-resource call also inside the catch:

(let [resource (create-resource)]
  (try
    (use-resource resource)
    (cleanup-resource resource)
    (catch Exception e
      (handle-exception e)
      (cleanup-resource resource))))

This will work, but there's a better way built into Clojure and the JVM. The try expression allows you to add a finally clause that is always run when exiting the try expression. It's run regardless of how the try expression is exited. If the body is successful, the finally clause is run. And if any of the catch statements, or none of them, are run, the finally is also run.

This is what it looks like:

(let [resource (create-resource)]
  (try
    (use-resource resource)
    (catch Exception e
      (handle-exception e))
    (finally
      (cleanup-resource resource))))

Another nice thing is that you can use the finally clause even without a catch. That is, you can ignore exceptions (let them slip through) but still have safe cleanup of resources.

You might be wondering about what happens with the value of a try expression if you use a finally. Well, there's no need to worry. The value of a try is unaffected by having a finally. The value of the last expression in the try's body is the value of the try. Here's an example:

(defn fetch-user [userid]
  (let [db (db/connect)]
    (try
      (db/fetch db "user" userid)
      (finally
        (db/disconnect db)))))

db/fetch returns the results of a query. It's the last expression in the try, so the result of the query is returned from the function fetch-user (or it throws an exception, of course).

One more thing: if you are opening a file or a stream, you could write this:

(let [writer (io/writer "my-file.txt")]
  (try
    (doseq [x (range 100)]
      (.write writer x)
      (.write writer "\n"))
    (finally
      (.close writer))))

That write the numbers 0 through 99 to a file using the try/finally pattern we just went over. However, Clojure comes with a macro that does that specific version of the pattern. If the cleanup you do in a finally is just to call the .close method, you should use with-open. Here's what it looks like:

(with-open [writer (io/writer "my-file.txt")]
  (doseq [x (range 100)]
    (.write writer x)
    (.write writer "\n")))

The try, finally, and .close are handled for you. If you're interested in how the with-open macro can be written, I've got a lesson on it called Macro example: with-open.

Book update 📖

Chapter 4 of Grokking Simplicity is out! Chapter 4 is about refactoring your actions to extract out calculations. By making code calculations, you make it easier to test and reuse.

You can buy the book and use the coupon code TSSIMPLICITY for 50% off.

I've been getting such positive messages about the book. Thank you so much for reading it. It's hard work and those nice messages help me keep going.

PurelyFunctional.tv Update 💥

A couple of weeks ago, I added a long-missing feature to PurelyFunctional.tv. Before, if you bought a Team License to a course, the only way to share it with your team was to download it and share the files. That wasn't very convenient.

Now, you can share your Team License purchases with other users on the site, so they can view them online. You can add people to your team by their email addresses. Just visit your dashboard and enter their emails. They'll then have access to the courses you've bought with a Team License.

Currently recording 🎥

This week, there's a new, much anticipated lesson. This one is all about Clojure Spec integration. In this lesson, we go over how to access the automatically created generators for Specs and how to replace them with custom generators when needed.

Go check it out here: Testing with Spec: custom generators

Up next? How to test functions with Spec. We'll be exploring the limits of the testing we can do in Spec. Spoiler alert: it's quite powerful.

BTW, Magnus Kvalevåg has a cool presentation on stateful property based testing at Clojutre.

Clojure Challenge 🤔

Last week's challenge

The challenge in Issue 347 was to write a function to efficiently remove a value from a vector, given its index in the vector.

You can check out the submissions here.

I tried a variety of implementations and did some primitive benchmarking of them. I need a more thorough system to know which one is the fastest.

Alan Thompson points out that Tupelo has this function already.

This week's challenge

parallel execution

If you need to run an action on all elements of a collection, you can use [run!](http ://clojuredocs.org/clojure.core/run!).

(run! println (range 100))

This will run all of the printlns in sequence. But there's no built-in function to run all of those functions in parallel. That's your task this week.

Write a function runp! that takes a function and a sequence of arguments. It will run the function on different threads and wait for them all to finish. It returns nil, just like run!.

(defn runp! [f coll]

Please be mindful of the number of threads it creates. For instance, if I create 4000 threads on my machine, it crashes the OS. The typical solution is to use a thread pool.

As usual, please send me your implementations. I'll share them all in next week's issue. If you send me one, but you don't want me to share it publicly, please let me know.

Rock on!
Eric Normand