Building collections: Immutable Clojure vs mutable OOP

In traditional OOP, if you have some collection A and based on the elements of A you need to build another collection B, a common approach is instantiating B as an empty collection, iterating over A and at each iteration appending a new element to B. When working with Clojure I had to invite a new way of thinking about this sort of task.

To demonstrate the two approaches, I've thrown up equivalent solutions to the same task in both PHP and Clojure. The task defines a map whose keys are years and values are structures which contain 2 integers (liabilities and equity). The goal is to calculate assets (the sum of liabilities and equity) of each year and just put them in another structure: an array in PHP and a list in Clojure. The original structure, of course, needs to remain intact.

The following is the version in PHP which loops through one structure and mutates another.

<?php
class BalanceSheet
{
public function __construct(public int $liabilities, public int $equity)
{
}
}
class AssetsCalculator
{
public function calculateAssets(BalanceSheet $balanceSheet): int
{
return $balanceSheet->liabilities + $balanceSheet->equity;
}
}
$assetsCalculator = new AssetsCalculator();
/**
* The following is the different way to achieve the same result as in transform-demo.clj.
* In OOP, we first create an empty collection structure (an empty vector, list or array in PHP) and when we mutate it
* by adding elements to it.
*/
$assetList = [];
foreach (
[
'1950' => new BalanceSheet(7000, 15000),
'1951' => new BalanceSheet(14000, 100000),
'1953' => new BalanceSheet(45000, 350000),
] as $year => $sheet
) {
$assetList[] = $assetsCalculator->calculateAssets($sheet);
}
echo join(', ', $assetList) . PHP_EOL;
# 22000, 114000, 395000

Now, the interesting part. In Clojure, as data structure are immutable you cannot instantiate an empty structure and keep appending elements to it. Operations on a map yields a new map. This means we can freely do whatever transformations we want on the original map defined in the task. The following implementation in Clojure "replaces" the map values (BalanceSheet with liabilities and equity) by a simple integer (assets). The resulting map is passed to vals and a list of assets is returned.

(defrecord BalanceSheet [liabilities equity])
(defn calc-assets
[^BalanceSheet bs]
(+ (:liabilities bs) (:equity bs)))
(defn replace-vals-with-assets
"{:key1 BalanceSheet, ...} -> {:key1 (calc-assets BalanceSheet), ...}."
[sheets]
(reduce-kv (fn [m k v]
(assoc m k (calc-assets v))) {} sheets))
(let [sheets {:1950 (->BalanceSheet 7000 15000)
:1951 (->BalanceSheet 14000 100000)
:1953 (->BalanceSheet 45000 350000)}]
(-> sheets replace-vals-with-assets vals))
;; First, the call to (replace-vals-with-assets sheets) takes place and a new map is returned:
;; {:1950 22000, :1951 114000, :1953 395000}
;; Then a list of its values is made by calling (vals {:1950 22000, :1951 114000, :1953 395000}):
;; (22000 114000 395000).

The takeaway is this: in Clojure it is not necessary to "grow" a structure to reach its desired state. Instead, applying transformations on an existing structure can be enough.

Popular posts from this blog

AWS VPN Client on a guest VM

Cyberghost Vpn on Arch Linux

Go error handling and stack traces voted as the biggest challenge