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
class – dynamically.
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.
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.
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.
So there.