A lot of discussions on software design end up focusing on dependencies and coupling. In short, hell is dependencies and the couplings it produces. It’s a tricky problem because its hard to look at some program text and see all of its dependencies; some of them require intelligence to recognize.
In Ruby, we don’t have very good ways to declare a class’s dependencies and no ways ways to declare its couplings. We can describe a project’s dependencies with a Gemfile or a file’s dependencies with requires. The trick is that these specifications often explode in complexity. Requiring Ruby’s thread library brings in some thread-safe data structures like queues and condition variables. Requiring ActiveRecord brings in a world of dependencies and causes a number of behavioral changes to Ruby that some consider impolite.
In some tinkerings with Clojure this weekend, I was struck how the
ns function is more effective at both declaring dependency and coupling and in restricting the possible distress those qualities may bring. Consider this snippet from my weekend project:
(:require [compojure.route :as route]
[compojure.handler :as handler]
[ring.middleware.params :as params]))
I have pedantic quibbles with
ns, but I like what’s happening here. This file can only use the functions in
hrq.core with no namespace qualifications. This file can only use the functions from
ring.middleware.params when they are qualified with the proper prefix. So now I have a very good idea of what code this particular file depends on and where I should look to find behavior that this file is subject to.
To a lesser extent, I have a good guess about what state this file depends on. If there are dynamically scoped variables (pardon me if those are the wrong Clojure/Lisp words) in the dependencies declared for this file, I would need to care about them. If those files are pure behavior (i.e. referentially transparent pure functions), I have nothing to worry about.
Clojure isn’t perfect in this regard; it does allow mutations and state changes outside of functions. It’s not strictly referentially transparent like Haskell is. The tradeoff is worthwhile, in my opinion. Admit some possible coupling in exchange for ease of building typical programs.
I’m not sure that Clojure is inherently superior to Ruby in this regard. It’s possibly a momentary cultural advantage, a reaction by those who were burned by expansive, implicit dependencies in Ruby and other languages. That said, it’s a good example of Clojure’s considered separation of concerns solving problems that are quite thorny in other languages.