WIP - collecting examples that show the benefit of using generic data structures over types and object graphs
Task: Combine data usage amounts for individual subscribers together. If a subscriber appears multiple times,
their amounts should be summed. SubscribersWithUsage.subscriberUsage
is essentially a
map of <user id> -> (map of <category> -> (map of <amount name> -> amount))
. Most subscribers appear
only once in the input but some do multiple times, in which case we sum all their usage amounts.
private static void addUsagePerSubscription(SubscribersWithUsage total, SubscribersWithUsage single) {
def saved = total.subscriberUsage
def toAdd = single.subscriberUsage
for (subscriptionEntry in toAdd.entrySet()) {
def subscriptionId = subscriptionEntry.key
def subscriptionUsageToAdd = subscriptionEntry.value
def savedForSubscription = saved.getOrDefault(subscriptionId, new HashMap<>())
for (subscriptionUsageEntry in subscriptionUsageToAdd.entrySet()) {
def profileCategory = subscriptionUsageEntry.key
def usageToAdd = subscriptionUsageEntry.value
def savedUsage = savedForSubscription.getOrDefault(profileCategory, new SubscriberUsage())
savedForSubscription.put(profileCategory, savedUsage + usageToAdd) ; + is an overriden operator -> method
}
saved.put(subscriptionId, savedForSubscription)
}
}
In Clojure we represent this as actual maps:
{:subscriberUsage {"userX" {:nighttime-usage {:amount-with-vat 11, ...}}})
and we merge them as follows:
(defn deep-merge-with
"Recursively merges maps. If keys are not maps, merge the values with (f val1 val2)"
[f & vals]
(if (every? map? vals)
(apply merge-with deep-merge-with f vals)
(apply f vals)))
(defn merge-usages [usages]
(apply
(partial deep-merge-with +)
usages))
Why the Groovy solution using esentially just a single "core function" (actually a statement), for
, the Clojure one uses quite a few - apply
, merge-with
, partial
. It defines a generic helper function, a (recursive) variation of a core function, one that could easily be used at many places.
The core of the Clojure solution is much smaller and it is easier to read, essentially it is "deep merge usages with +". Even though you don't know the custom function deep-merge-with
, you know the standard merge-with
and can easily guess what this one does. On the other hand, the Groovy solution is "safer" because it will only work for a 2-level deep map whose leaves override +
and will fail if you pass it anything unexpected.
You could write both quite differently but they way they are, I think they are quite "canonical" for the given language.