Schrödinger's functions: not the pure functions your are looking for
What make a pure function pure? What make a stable system stable?
Reading Abstract Clojure fills me with angst.
The article ends up with the following design:
I see that diagram and my reptilian brain just wants to run away and hide: this is no different from what my Java looked like ten years ago.
Is this design the best we can do for “large” applications? And if this is the case, why did I bother with Clojure?
Pure functions
To make your code more maintainable, a useful principle is to build your application mostly from pure functions, minimizing side-effecting code.
Pure functions are amazing as they are easier to understand than side-effecting code, hence easier to change.
Two reasons:
- Their context is limited to their parameters: no need to worry about global variables or the database state.
- They are deterministic: for a given set of parameters, they always always always always return the same result. Always.
An important side effect of these reasons is that, while you are trying to understand a pure function, you can stop at any layer down the call stack knowing no surprise is lurking further down, making it easier to understand the function.
From impure to pure code
As pure functions are important, let’s see how to build them.
We start with a prototypical web controller that calls the database to do its job:
(ns app.server
(:require [app.db :as db]))
(defn get-article [data-source request]
(let [id (get-in request [:path-params :id])
article (db/get-article-by-id data-source id)] ;; <<-- PURE EVIL!!! Or should I say IMPURE EVIL?
{:status 200
:body article}))
The function get-article
is not pure as it is not deterministic. Two consecutive calls to get-article
can return completely different responses:
- Nothing if the article does not exist yet.
- A version of the article.
- Another version of the article, if there has been some write between the two calls.
- An error if the database is down.
- May never finish the execution, if you forgot to set your timeouts.
To smite this evil, Abstract Clojure tells us to add one level of indirection, by passing a function or protocol instead of the data-source
, so that the get-article
function is decoupled from the database
namespace/package:
(defprotocol ArticleRepository
(create [_ article])
(get-by-id [_ id])
,,,)
(defn get-article [article-repository request]
(let [id (get-in request [:path-params :id])
article (article-repository/get-by-id article-repository id)] ;; <<-- Beautiful!
{:status 200
:body article}))
And with this, our get-article
function becomes pure. Victory!
Or not?
Schrödinger’s functions
That last version of get-article
makes me doubt.
If a tree falls in a forest and no one is around to hear it, does it make a sound?
Sorry, I meant:
If a pure function is passed an impure function at runtime, does it make it impure?
get-article
can be passed a pure or an impure function, so is get-article
a Schrödinger’s function? One that is sometimes pure while others is impure, and to find out you need to open the box and look into the implementation details of the article-repository
passed as a parameter?
Before digging more into philosophy or quantum mechanics, let’s see if a statically typed language can shed some light:
find some **obvious** Haskell IO monad example and paste here
If that simple piece of monadic Haskell did not make sense, let me explain with some good old Java:
public interface ArticleRepository {
String createArticle(Article article);
Article getById(String id);
}
class ArticleController {
public HttpResponse getArticle(ArticleRepository articleRepository, HttpRequest request) {
String id = request.getParams().get("id");
Article article = articleRepository.getById(id);
return HttpResponse.withStatus(200).withBody(article);
}
}
So far, our non-idiomatic non-monadic Java does not help, but before complaining about Java’s crippled type system, let’s try to implement a DB based ArticleRepository:
class DatabaseArticleRepository implements ArticleRepository {
private DataSource dataSource;
public Article getById(String id) {
try (Connection connection = dataSource.getConnection()){
try (PreparedStatement stmt = connection.prepareStatement("select * from article where id=?")) {
stmt.setString(1, id);
ResultSet resultSet = stmt.executeQuery();
if (resultSet.next()) {
return new Article().withContent(resultSet.getString("content"));
} else
return null;
}
}
}
}
But this does not compile:
DatabaseArticleRepository: unreported exception java.sql.SQLException; must be caught or declared to be thrown.
Same as Haskell’s IO, Java’s checked exceptions tag a function/method as not safe. But with IO you are only allowed to “declare to be thrown” (unless you want to be Unsafe), so the only option is to rethrow the exception:
class DatabaseArticleRepository implements ArticleRepository {
private DataSource dataSource;
public Article getById(String id) throws SQLException { // Throwing exception now.
,,,
}
}
Now the compiler complains with:
getById(java.lang.String) in DatabaseArticleController cannot implement getById(java.lang.String) in ArticleRepository overridden method does not throw java.sql.SQLException
Gosh! Let’s add it to the ArticleRepository:
public interface ArticleRepository {
Article getById(String id) throws SQLException; // More throwing.
}
And recompile:
ArticleController: unreported exception java.sql.SQLException; must be caught or declared to be thrown
And finally, we need to bubble it up to the top of the stack:
class ArticleController {
public HttpResponse getArticle(...) throws SQLException { // Yet more throwing
...
}
}
Java checked exceptions, just like Haskell IO, just like impure functions, are contagious. Like a virus, any function that comes in contact with them is infected.
So there are no Schrödinger’s functions. A function that is passed an impure function at runtime, is an impure function (as long as it runs the impure function).
Our final version of the get-article
function, even if it depends on an interface/protocol, is as impure as our initial one.
Try/catch! RuntimeException!
But if a function catches a checked exception, and swallows or rethrows it as a runtime exception, does it become a pure function?
You can pretend that it is a pure function, and I can pretend to know what I am talking about, but pretending does not make it real.
Also, we are using checked exceptions as an approximation. You can read the same argument but with F# and Haskell.
Unstable
Now that we know that depending on an interface/protocol does not make our functions pure, what does it mean for the stability of our design?
Depend in the direction of stability – Stable Dependencies Principle
This is a good principle to follow. Abstract Clojure defines stability as:
In Clojure, we can consider a function to be stable if it is referentially transparent (== pure).
If we accept this definition, our final design looks like:
So we are back to a completely unstable system, but with a lot more moving parts that the initial design.
More things, same stability? Does not seems like a winning design.
Maybe this is not the stability definition that you are looking for.
So what now? We will leave the questions on how to convert impure functions into pure functions and what stability is for another day.