2
votes

I would like to use Clojure hashmaps to define generalized vectors. In this picture, the hashmap {:x 3 :y 2 :z (- 3)} represents the symbolic expression 3x + 2y - 3z.

I would like a function gen+ which acts as an addition operation on hashmaps, and which satisfies the following constraints:

  • Applying gen+ to two hashmaps with overlapping keys returns a hashmap with those key values summed. e.g.,

(gen+ {:x 1} {:x 2})

;= {:x 3}

  • Applying gen+ to two hashmaps with distinct keys returns a hashmap with those key-values combined. e.g.,

(gen+ {:x 1} {:y 2})

;= {:x 1 :y 2}

  • The empty hashmap {} is an additive identity for the gen+ function. e.g.,

(gen+ {:x 1} {})

:= {:x 1}

  • By the above constraints, any hashmap with zero entries is also an additive identity. For example, {:z 0}. Owing to this redundancy, the gen+ function should always return hashmaps without any 0 values. e.g.,

(gen+ {:x 1 :y 0} {:z 0})

;= {:x 1}

  • Clojure interprets missing keys as having value nil, rather than 0, as with ({:x 3} :y) ;= nil. Consequently, gen+ should treat nil values the same way as 0. e.g.,

(gen+ {:x 1 :y 0} {:x nil :y nil})

{:x 1}

  • My Question: How can we write a function gen+ which satisfies the above constraints? Once this is in place, is it possible to overload the + operator with the gen+ function, enabling elementary addition with hashmaps?

It should be apparent why this formalism treats hashmaps as generalized vectors. Clojure interprets a vector like [x0 x1 x2] almost the same as the hashmap {:0 x0 :1 x1 :2 x2}, with a slight difference that I don't quite understand. Consequently, if we apply gen+ to two vectors, then it should be effectively the same as + applied to them. This empowers us to easily work with sparse vectors, as well as add vectors of different sizes. e.g.,

(gen+ [0 0 0 4] [0 0 0 0 0 0 0 0 0 9])

;= {:4 4 :9 9}

Here's what I don't understand about hashmaps and vectors. If I call a hashmap as a function, I need to apply a key argument like :2. On the other hand, if I call a vector as a function, I need to apply an index argument like 2. e.g.,

({:2 2} :2)

;= 2

([0 1 2] 2]

;= 2

Even if you can't help with the gen+ function, could you please explain why hashmaps and vectors behave differently when called as functions?

2
Just like M Smith, I wouldn't advise overloading +, but here's some dark magic anyways.Omri Bernstein
If this is a school project, props to your school for accepting clojure.muhuk
@muhuk: I'm a professional mathematician who works primarily with functional analysis (infinite-dimensional linear algebra). These tools apply when spaces/types of interest are linear spaces, points/terms are vectors, and maps/functions are linear operators. Being able to work with hashmaps as vectors means that we can deploy the technology of modern mathematics, and not have to reinvent the wheel. This point of view is particularly useful in MapReduce architectures, where the basic objects of interest are key/value pairs, just as with hashmaps.Tom LaGatta
But I too think schools should be open to a wide variety of programming languages, particularly functional ones :)Tom LaGatta

2 Answers

4
votes

The answer to you first question is to use merge-with See http://clojuredocs.org/clojure_core/clojure.core/merge-with

From the docs:

Returns a map that consists of the rest of the maps conj-ed onto the first. If a key occurs in more than one map, the mapping(s) from the latter (left-to-right) will be combined with the mapping in the result by calling (f val-in-result val-in-latter).

You can then write a function to combine the merged values (maybe +). Then toss the nil and 0 values.

Overloading + is not really a good idea in Clojure as it isn't really overloading but replacing.

The difference between a map and vector is a like an array while a map is more a dictionary of key value pairs. Both structures are also functions of them members. For a vector/array whose members are access by offset, it makes sense for it to accept an offset. For a map/dictionary whose members as accessed by key, it makes sense for it to accept a key.

2
votes

This will first remove the nil and 0 values and then add the maps with merge-with, as suggested by M Smith:

(defn filter-vals
  [pred m]
  (into {} (filter (fn [[k v]] (pred v))
                   m)))

(defn gen+
  [m1 m2]
  (letfn [(keep-val? [val]
            (and (not (nil? val))
                 (not (zero? val))))]
    (merge-with +
                (filter-vals keep-val? m1)
                (filter-vals keep-val? m2))))

You need to filter out any nils before you start adding - otherwise you'll eventually end up trying to add nil to a number, which is an error. However, it's possible for the gen+ I outlined to return an entry with a 0 value (consider (gen+ {:x 1} {:x -1})).

If that's possible based on your inputs, and needs to be avoided, you need to add another filter after the merge:

(defn gen+
  [m1 m2]
  (letfn [(keep-val? [val]
            (and (not (nil? val))
                 (not (zero? val))))]
    (->> (merge-with +
                     (filter-vals keep-val? m1)
                     (filter-vals keep-val? m2))
         (filter-vals keep-val?))))

And, finally, here's a version that can handle a variable number of input maps:

(defn gen+
  [& maps]
  (letfn [(keep-val? [val]
            (and (not (nil? val))
                 (not (zero? val))))]
    (->> (apply merge-with
                +
                (map #(filter-vals keep-val? %) maps))
         (filter-vals keep-val?))))

So, for instance:

(gen+ {:x 1} {:x 3} {:x 4 :y 5}) ;=> {:x 8, :y 5}

Regarding the difference between maps and vectors, think of it this way: maps are functions from keys to values, whereas vectors are functions from indices to values. In both cases if you have a "key" (which for a vector is the index) you can use that to look up the associated value.

I agree with the answers and comments you've gotten so far about overloading clojure.core/+ (I don't recommend it); that said, here's one way to sort of do it, with plenty of caveats:

(ns overload.example
  (:refer-clojure :exclude [+])
  (:require [clojure.core :as clj]))

(in-ns 'overload.example)

(defn gen+ ...)

(defn + [& xs]
  (if (every? number? xs)
    (reduce clj/+ 0 xs)
    (apply gen+ xs)))