The Clean Code Blog

by Robert C. Martin (Uncle Bob)

Functional Classes in Clojure

19 January 2023

My previous blog seemed only to continue the confusion regarding classes in Functional Programming. Indeed, many people got quite irate. So perhaps a bit of code will help.

Trigger Warning:

  • Object Oriented Terminology.
  • Dynamically Typed Language.
  • Mixed Metaphors.
  • Distracting Animations.

To all the adherents of the Statically Typed Functional Programming religion: I know that you believe that Static Typing is an essential aspect of Functional Programming and that no mere dynamically typed language could ever begin to approach the heights and glory of The One True and Holy TYPED Functional Apotheotic Paradigm. But we lowly programmers quivering down here at the base of Orthanc can only hope to meekly subsist on the dregs that fall from on high.

(R.I.P. Kirstie Alley

OK, so, once again…

A class is an intentionally named abstraction that consists of a set of narrowly cohesive functions that operate over an internally defined data structure.

We do not need the class keyword. Nor do we need polymorphic dispatch. Nor do we need inheritance. A class is just a description, whether in full or in part, of an object.

For example – it’s time we talked about clouds (which I have looked at from both sides now; and do, in fact, understand pretty well).

So… Here come your father’s parentheses!

(ns spacewar.game-logic.clouds
  (:require [clojure.spec.alpha :as s]
            [spacewar.geometry :as geo]
            [spacewar.game-logic.config :as glc]))

(s/def ::x number?)
(s/def ::y number?)
(s/def ::concentration number?)

(s/def ::cloud (s/keys :req-un [::x ::y ::concentration]))
(s/def ::clouds (s/coll-of ::cloud))

(defn valid-cloud? [cloud]
  (let [valid (s/valid? ::cloud cloud)]
    (when (not valid)
      (println (s/explain-str ::cloud cloud)))
    valid))

(defn make-cloud
  ([]
   (make-cloud 0 0 0))
  ([x y concentration]
  {:x x
   :y y
   :concentration concentration}))

(defn harvest-dilithium [ms ship cloud]
  (let [ship-pos [(:x ship) (:y ship)]
        cloud-pos [(:x cloud) (:y cloud)]]
    (if (< (geo/distance ship-pos cloud-pos) glc/dilithium-harvest-range)
      (let [max-harvest (* ms glc/dilithium-harvest-rate)
            need (- glc/ship-dilithium (:dilithium ship))
            cloud-content (:concentration cloud)
            harvest (min max-harvest cloud-content need)
            ship (update ship :dilithium + harvest)
            cloud (update cloud :concentration - harvest)]
        [ship cloud])
      [ship cloud])))

(defn update-dilithium-harvest [ms world]
  (let [{:keys [clouds ship]} world]
    (loop [clouds clouds ship ship harvested-clouds []]
      (if (empty? clouds)
        (assoc world :ship ship :clouds harvested-clouds)
        (let [[ship cloud] (harvest-dilithium ms ship (first clouds))]
          (recur (rest clouds) ship (conj harvested-clouds cloud)))))))

(defn update-clouds-age [ms world]
  (let [clouds (:clouds world)
        decay (Math/pow glc/cloud-decay-rate ms)
        clouds (map #(update % :concentration * decay) clouds)
        clouds (filter #(> (:concentration %) 1) clouds)
        clouds (doall clouds)]
    (assoc world :clouds clouds)))

(defn update-clouds [ms world]
  (->> world
       (update-clouds-age ms)
       (update-dilithium-harvest ms)))

Some years back I wrote a nice little spacewar game in Clojure. You can play it here. While playing, if you manage to blow up a Klingon, a sparkling cloud of Dilithium Crystals will remain behind, quickly dissipating. If you can guide your ship into the midst of that cloud, you will harvest some of that Dilithium and replenish your stores.

The code you see above is the class that represents the Dilithium Cloud.

The first thing to notice is that I defined the TYPE of the cloud classdynamically.

A cloud is an object with an x and y coordinate, and a concentration; all of which must be numbers. I also created a little type checking function named valid-cloud? that is used by my unit tests (not shown) to make sure the TYPE is not violated by any of the methods.

Next comes make-cloud the constructor of the cloud class.

via GIPHY

There are two overloads of the constructor. The first takes no arguments and simply creates a cloud at (0,0) with no Dilithium in it. The second takes three arguments and loads the instance variables of the class.

via GIPHY

There are two primary methods of the cloud class: update-clouds-age and update-dilithium-harvest. The update-clouds-age method finds all the cloud instances in the world object and decreases their concentration by the decay factor – which is a function of the number of milliseconds (ms) since the last time they were updated. The update-dilithium-harvest method finds all the cloud objects that are within the ship object’s harvesting range and transfers Dilithium from those cloud objects to the ship object.

Now, you might notice that these methods are not the traditional style of method you would find in a Java program. For one thing, they deal with a list of cloud objects rather than an individual cloud object. Secondly, there’s nothing polymorphic about them. Third, they are functional, because they return a new world object with new cloud objects and, in the case of update-dilithium-harvest, a new ship object.

So are these really methods of the cloud class? Sure! Why not? They are a set of narrowly cohesive functions that manipulate an internal data structure within an intentionally named abstraction.

For all intents and purposes cloud is a °°°°°° °°°°°°° class.

via GIPHY

So there.