Skip to content

Instantly share code, notes, and snippets.

@holyjak
Last active April 10, 2019 20:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save holyjak/91c902f5983228b1ae42fae0e548b20c to your computer and use it in GitHub Desktop.
Save holyjak/91c902f5983228b1ae42fae0e548b20c to your computer and use it in GitHub Desktop.
Benefits of Clojure's use of generic data structures over Java's object graphs

WIP - collecting examples that show the benefit of using generic data structures over types and object graphs

Case 1

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.

Groovy

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)
    }
}

Clojure

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))

Comments

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment