boolean·knot

technical blog

21 December 2015

Encapsulation and Clojure

Encapsulation is a mainstay of object orientated programming, but in Clojure it’s often avoided. Why does Clojure steer clear of a concept that many programming languages consider to be best practice?

Why shouldn’t we use encapsulation?

In object orientated programming, the internal state of an object is hidden behind methods. This protects the object from being put into an invalid state, and allows the object can be treated like a black box; we care only about the interface it presents to the world.

So this is good, right?

Unfortunately, the advantages of encapsulation come with a steep price. If we hide the underlying data structures, then we lose access to their respective API. We need to reimplement any functionality we want to retain.

For instance, let’s say we want to represent HTTP headers in an object orientated language like Ruby. We might start off with a class like:

class Headers
  def initialize
    @env = {}
  end

  def [](key)
    @env[key]
  end

  def []=(key, value)
    @env[key] = value
  end
end

Here we’ve written a getter and setter function for the underlying @env instance variable. All this code is just boilerplate to get around the encapsulation of @env, but it’s a necessary starting point to produce an class with custom behaviour.

Clojure discourages encapsulation of data, so we can avoid this initial boilerplate. In Clojure, we can just use a raw map.

A world without encapsulation

Let’s expand the Ruby example a little. We’ll add in some new functionality to ensure that when a header is added, the header name is both a string and only contains valid characters:

class Headers
  def initialize
    @env = {}
  end

  def [](key)
    @env[key]
  end

  def []=(key, value)
    raise IndexError unless header_name?(key)
    @env[key] = value
  end

  private

  def header_name?(s)
    s.is_a?(String) && s =~ /^[A-Za-z0-9!#$%&'*+._`|~^-]+$/
  end
end

In Clojure, data structures are immutable, so unlike Ruby we don’t need to check whether an update is valid. A map is either a valid header map when it is created, and will always be valid, or else it was never valid to begin with.

This means that we can achieve equivalent functionality by writing a predicate function that will return true if a map is a valid header map, or false otherwise:

(defn header-name? [s]
  (and (string? s) (re-matches #"[A-Za-z0-9!#$%&'*+._`|~^-]+" s)))

(defn headers? [m]
  (and (map? m) (every? header-name? (keys m))))

You may point out here that running headers? requires iterating through every key in the map, and therefore may not be as efficient as the Ruby implementation, where each key is checked only when it’s set. But if performance does become an issue in Clojure, we can solve it with a memoization strategy.

Ruby is not a language known for boilerplate, but because it’s an object orientated language, it cannot entirely avoid the repetition caused by encapsulation. In Clojure we can avoid this redundancy completely.