Search code examples
clojureinterface

override keys and vals


given the type:

(defn as-pairs [m]
  (when-not (empty? m)
    (seq (persistent! (reduce (fn [s [k vs]]
                                (reduce (fn [s v] (conj! s (clojure.lang.MapEntry/create k v))) s vs))
                              (transient []) m)))))

(deftype Rel [m]
  clojure.lang.Seqable
  (seq [this] (as-pairs m))
  clojure.lang.ILookup
  (valAt [this k] (get m k))
  (valAt [this k default] (get m k default))
  clojure.lang.IPersistentMap
  (assoc [this k v] (Rel. (update m k #(conj (or % #{}) v))))
  (assocEx [this k v] (throw (Exception.)))
  (without [this k] (Rel. (dissoc m k))))

(defn relation [& pairs]
  (reduce (fn [r [k v]] (assoc r k v)) (Rel. (hash-map)) pairs))

Is it possible to override the calls to (keys ...) and (vals ...)? As an example, the current implementation

>(def r (relation [1 2] [1 3] [2 4]))
> r
{1 3, 1 2, 2 4}
> (get r 1)
#{3 2}
> (seq r)
([1 3] [1 2] [2 4])
> (keys r)
(1 1 2)
> (vals r)
> (3 2 4)

For example I would like keys to return something more along the lines of (seq (set (keys r)) i.e exclude duplicates. I can see that in

static public class KeySeq extends ASeq{
    final ISeq seq;
    final Iterable iterable;

    static public KeySeq create(ISeq seq){
        if(seq == null)
            return null;
        return new KeySeq(seq, null);
    }
...

It seems that keys and vals is dependent on the implementation of seq for clojure.lang.Seqable. it just takes the first/second values from the clojure.lang.MapEntry pairs returned by (seq r).

Is there any way around this?


Solution

  • No, it is not possible to override this behavior of clojure.core/keys and clojure.core/vals. As you noted, those Clojure functions call the corresponding static methods in clojure.lang.RT, which in turn call the createFromMap static methods in the KeySeq and ValSeq classes of clojure.lang.APersistentMap. All these implementations use IPersistentMap directly to get their sequence, so they cannot be overridden independently of what you've already written.

    One thing you could do in this case is provide your own keys function to replace the one from clojure.core:

    (ns example.core
      (:refer-clojure :exclude [keys])
      (:require [clojure.core :as clj]))
    
    (defn keys [m]
      (distinct (clj/keys m)))
    

    Example:

    (ns example.usage
      (:refer-clojure :exclude [keys])
      (:require [example.core :refer [keys]]))
    
    (keys {:foo :bar :baz :qux})
    ;;=> (:foo :baz)
    

    One thing to note is that it is part of the contract of clojure.core/keys to return a sequence that matches up with seq, so, for instance, (= m (zipmap (keys m) (vals m))) holds in general. This example.core/keys function satisfies that property for regular Clojure maps, but not for the multimaps you're defining. It's up to you to decide whether that tweak of semantics is a good idea or not.