Pattern matching on records in Clojure - clojure

Is this supported right now? The only information I could find was the example from the wiki (https://github.com/clojure/core.match/wiki/Deftype-and-defrecord-matching)
which produces an error:
CompilerException java.lang.AssertionError: Invalid list syntax (Red.
(Red. a x b) y c) in (Black. (Red. (Red. a x b) y c) z d). Valid
syntax: [[:default :guard] [:or :default] [:default :only] [:default
:seq] [:default :when] [:default :as] [:default :<<] [:default
:clojure.core.match/vector]]

This has not been implemented yet, but since records behave as maps you can do:
(defrecord ab [a b])
user.ab
user> (let [x (->ab 1 1)]
(match [x]
[{:a _ :b 2}] :a0
[{:a 1 :b 1}] :a1
[{:c 3 :d _ :e 4}] :a2
:else nil))
:a1
You can also match on the type of the record, but it is a bit inelegant.
user> (let [x (->ab 1 1)
aba user.ab]
(match [(type x) x]
[aba {:a _ :b 2}] :a0
[aba {:a 1 :b 1}] :a1
[aba {:c 3 :d _ :e 4}] :a2
:else nil))
:a1
https://github.com/clojure/core.match/wiki/Basic-usage#map-patterns

Related

Clojure sum maps with strings and numbers

Hi l have 2 maps like these (with the possibility of having multiple maps)
(def map1 {:a {:b 1} :c ["dog"]})
(def map2 {:a {:b 2} :c ["cat"]})
l need to have this as a return {:a {:b 3} :c ["dog" "cat"]}
How can l do this?
Solution using generic functions
You could use generic functions - let's call it here o+ (operator +) - by making use of Clojure's multiple dispatch ability. By multiple dispatch you avoid if - else or cond clauses - for distinguishing the argument types/classes - thereby the code stays extensible (further cases can be added without having to change existing code). The function at the end of the defmulti definition is the dispatch function. In the defmethod form - right before the actual function arguments list (vector), you list the dispatch case for which then Clojure looks when deciding which method to use.
(defmulti o+ (fn [& args] (mapv class args)))
(defmethod o+ [Number Number] [x y] (+ x y)) ;; Numbers get added
(defmethod o+ [clojure.lang.PersistentVector clojure.lang.PersistentVector]
[x y] (into (empty x) (concat x y))) ;; Vectors concatentated
(defmethod o+ [clojure.lang.PersistentArrayMap clojure.lang.PersistentArrayMap]
[x y] (merge-with o+ x y)) ;; Maps merged-with #'o+ (recursive definition!)
Test
You can fuse then two maps with the (now recursively defined) o+ operator:
(def map1 {:a {:b 1} :c ["dog"]})
(def map2 {:a {:b 2} :c ["cat"]})
(o+ map1 map2) ;; or originally: (merge-with o+ map1 map2)
;; => {:a {:b 3}, :c ["dog" "cat"]}
Even deeper nested maps are covered
Due to the recursive character of this definition - this works also with more deeply nested maps as long as they have the same structure - and use only the defined class cases (otherwise one could add further cases):
(def mapA {:a {:b 2 :c {:d 1 :e ["a"] :f 3}} :g ["b"]})
(def mapB {:a {:b 3 :c {:d 4 :e ["b" "e"] :f 5}} :g ["c"]})
(o+ mapA mapB)
;;=> {:a {:b 5, :c {:d 5, :e ["a" "b" "e"], :f 8}}, :g ["b" "c"]}
Use reduce to add more maps at once
And as long as you can apply o+ on two objects, you can process an arbitrary number of maps using reduce:
(def mapA {:a {:b 2 :c {:d 1 :e ["a"] :f 3}} :g ["b"]})
(def mapB {:a {:b 3 :c {:d 4 :e ["b" "e"] :f 5}} :g ["c"]})
(def mapC {:a {:b 1 :c {:d 3 :e ["c"] :f 1}} :g ["a"]})
(reduce o+ [mapA mapB mapC]) ;; this vector could contain much more maps!
;; => {:a {:b 6, :c {:d 8, :e ["a" "b" "e" "c"], :f 9}}, :g ["b" "c" "a"]}
;; or we could define in addition:
(defmethod o+ :default [& args] (reduce o+ args))
;; which makes `o+` to a variadic function (function which can be
;; called with as many arguments you want)
;; then, whenever we add more than two arguments, it will be activated:
(o+ mapA mapB mapC)
;; => {:a {:b 6, :c {:d 8, :e ["a" "b" "e" "c"], :f 9}}, :g ["b" "c" "a"]}
;; and this also works:
(o+ 1 2 3 4 5 6)
;; => 21
(o+ ["a"] ["b" "c"] ["d"])
;; => ["a" "b" "c" "d"]
You can use merge-with with a custom function to resolve conflicts when merging maps.
(defn my-merge [a b]
(merge-with (fn [a b]
(if (map? a)
(merge-with + a b)
(into a b)))
a b))
Or defined recursively:
(defn my-merge [a b]
(cond (vector? a) (into a b)
(number? a) (+ a b)
(map? a) (merge-with my-merge a b)
:else (throw (ex-info "Unsupported values" {:values [a b]}))))
Here's an idea. I didn't test this, and I'm quite sure a more clever solution will come... especially now that I said that this is an answer (the someone-said-something-wrong-on-the-internet effect) :)
(reduce
(fn [accum item]
(let [b-accum (get-in accum [:a :b])
b-item (get-in item [:a :b])
new-b (+ b-accum b-item)
new-c (vec (concat (:c accum) (:c item))]
{:a {:b new-b} :c new-c}))
[map1 map2])
(require '[net.cgrand.xforms :as x])
(let [map1 {:a {:b 1} :c ["dog"]}
map2 {:a {:b 2} :c ["cat"]}]
(->> [map1 map2]
(into {}
(x/multiplex
{:a (comp (map :a) (map :b) (x/reduce +) (x/into [:b]) (x/into {}))
:c (comp (mapcat :c) (x/reduce conj))}))))

How to select keys in nested maps in Clojure?

Let's say I have a map (m) like this:
(def m {:a 1 :b 2 :c {:d 3 :e 4} :e { ... } ....})
I'd like to create a new map only containing :a, :b and :d from m, i.e. the result should be:
{:a 1 :b 2 :d 3}
I know that I can use select-keys to easily get :a and :b:
(select-keys m [:a :b])
But what's a good way to also get :d? I'm looking for something like this:
(select-keys* m [:a :b [:c :d]])
Does such a function exists in Clojure or what's the recommended approach?
In pure Clojure I would do it like this:
(defn select-keys* [m paths]
(into {} (map (fn [p]
[(last p) (get-in m p)]))
paths))
(select-keys* m [[:a] [:b] [:c :d]]) ;;=> {:a 1, :b 2, :d 3}
I prefer keeping the type of a path regular, so a sequence of keys for all paths. In clojure.spec this would read as
(s/def ::nested-map (s/map-of keyword?
(s/or :num number? :map ::nested-map)))
(s/def ::path (s/coll-of keyword?))
(s/fdef select-keys*
:args (s/cat :m ::nested-map
:paths (s/coll-of ::path)))
As an alternative you can use destructing on a function, for example:
(def m {:a 1 :b 2 :c {:d 3 :e 4}})
(defn get-m
[{a :a b :b {d :d} :c}]
{:a 1 :b b :d d})
(get-m m) ; => {:a 1, :b 2, :d 3}
You can use clojure.walk.
(require '[clojure.walk :as w])
(defn nested-select-keys
[map keyseq]
(w/postwalk (fn [x]
(if (map? x)
(select-keys x keyseq)
(identity x))) map))
(nested-select-keys {:a 1 :b {:c 2 :d 3}} [:a :b :c])
; => {:a 1, :b {:c 2}}
I'm not aware of such a function being part of Clojure. You'll probably have to write it yourself. I've came up with this :
(defn select-keys* [m v]
(reduce
(fn [aggregate next]
(let [key-value (if (vector? next)
[(last next)
(get-in m next)]
[next
(get m next)])]
(apply assoc aggregate key-value)))
{}
v))
Require paths to be vectors so you can use peek (much faster than last). Reduce over the paths like this:
(defn select-keys* [m paths]
(reduce (fn [r p] (assoc r (peek p) (get-in m p))) {} paths))
(select-keys* m [[:a] [:b] [:c :d]]) ;;=> {:a 1, :b 2, :d 3}
Of course, this assumes that all your terminal keys are unique.

zipmap with multi-value keys

The following:
(zipmap '(:a :b :c :c) '(1 2 3 4))
evals to: {:c 4, :b 2, :a 1}
I would like to get:
{:c '(3 4) :b '(2) :a '(1)}
instead.
How should I define my own zipmap that takes two lists and returns a map with multiple values for keys?
This will do
(defn zippy [l1 l2]
(apply merge-with concat (map (fn [a b]{a (list b)}) l1 l2)))
;;; ⇒ #'user/zippy
(zippy '(:a :b :c :c) '(1 2 3 4))
;;; ⇒ {:c (3 4), :b (2), :a (1)}

How do I conj to a clojure vector conditionally

Is there a cleaner way to do something like the following in clojure?
(defn this [x] (* 2 x))
(defn that [x] (inc x))
(defn the-other [x] (-> x this that))
(defn make-vector [thing]
(let [base (vector (this (:a thing))
(that (:b thing)))]
(if-let [optional (:c thing)]
(conj base (the-other optional))
base)))
(make-vector {:a 1, :b 2}) ;=> [2 3]
(make-vector {:a 1, :b 2, :c 3}) ;=> [2 3 7]
By "cleaner" I mean something closer to this:
(defn non-working-make-vector [thing]
(vector (this (:a thing))
(that (:b thing))
(if (:c thing) (the-other (:c thing)))))
(non-working-make-vector {:a 1, :b 2} ;=> [2 3 nil] no nil, please!
(non-working-make-vector {:a 1, :b 2, :c 3} ;=> [2 3 7]
Note that I might want to call some arbitrary function (e.g. this, that, the-other) on any of the keys in thing and place the result in the returned vector. The important thing is that if the key doesn't exist in the map it should not put a nil in the vector.
This is similar to this question but the output is a vector rather than a map so I can't use merge.
(defn this [x] (* 2 x))
(defn that [x] (inc x))
(defn the-other [x] (-> x this that))
(def k-f-map {:a this
:b that
:c the-other})
(def m1 {:a 1 :b 2})
(def m2 {:a 1 :b 2 :c 3})
(defn make-vector [k-f-map m]
(reduce (fn [res [fk fv]]
(if (fk m)
(conj res (fv (fk m)))
res))
[] k-f-map))
(make-vector k-f-map m1)
-> [2 3]
(make-vector k-f-map m2)
-> [2 3 7]
;;; replace [:a :b :c] with a vector of arbitrary functions
;;; of your choice, or perhaps accept a seqable of functions
;;; as an extra argument
(defn make-vector [thing]
(into [] (keep #(% thing) [:a :b :c])))
;;; from the REPL:
(make-vector {:a 1 :b 2})
; => [1 2]
(make-vector {:a 1 :b 2 :c 3})
; => [1 2 3]
Note that keep only throws out nil; false will be included in the output.
or using cond->?
your make-vector function in cond-> version:
(defn make-vector [thing]
(cond-> [(this (:a thing))
(that (:b thing))]
(:c thing) (conj (the-other (:c thing)))))
you can have more conditions or change :a and :b to be optional as well.

In Clojure can I call a function that expects keys with a map?

Let's say I have
(defn test [ & {:keys [a b c]}]
(println a)
(println b)
(println c))
What I want is to call test with a map {:a 1 :b 2 :c 3}.
This works:
(apply test [:a 1 :b 2 :c 3])
These do not:
(apply test {:a 1 :b 2 :c 3})
(apply test (seq {:a 1 :b 2 :c 3}))
EDIT
So you can of course define the function like this also:
(defn test [{:keys [a b c]}] ; No &
(println a)
(println b)
(println c))
And then you can pass a map to it:
(test {:a 1 :b 2 :c 3})
1
2
3
When learning clojure I had missed this was possible. Nevertheless if you ever come across a function defined by me or somebody like me then knowing how to pass a map to it could still be useful ;)
user> (apply list (mapcat seq {:a 1 :b [2 3 4]}))
(:a 1 :b [2 3 4])
Any good reason not to define it like this in the first place?
(defn my-test [{:keys [a b c]}] ;; so without the &
(println a)
(println b)
(println c))
and then call it like this?
(my-test {:a 10 :b 20 :c 30})
which outputs:
10
20
30
nil
This works, but is inelegant:
(apply test (flatten (seq {:a 1 :b 2 :c 3})))
The reason (apply test (seq {:a 1 :b 2 :c 3})) doesn't work is that (seq {:a 1 :b 2 :c 3}) returns [[:a 1] [:b 2] [:c 3]], flatten takes care of this.
Better solutions?