plumatic / plumbing

Prismatic's Clojure(Script) utility belt

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

like-some?

ccfontes opened this issue · comments

Because I use your plumbing already in my benrikuro and then just defcopy the forms I like, it would be nice if some of other forms I use myself would have a chance to be integrated in plumbing.

;;;;;;;;;;;;;;;; Somewhere from the Clojure community ;;;;;;;;;;;;;;;;

(defmacro defcopy
  "Defines a copy of a var: a new var with the same root binding (if
   any) and similar metadata. The metadata of the copy is its initial
   metadata (as provided by def) merged into the metadata of the original.
   source: same as defalias from clojure 1.2 and downwards."
  ([name orig]
  `(do
     (alter-meta!
      (if (.hasRoot (var ~orig))
        (def ~name (.getRawRoot (var ~orig)))
        (def ~name))
      ;; When copying metadata, disregard {:macro false}.
      ;; Workaround for http://www.assembla.com/spaces/clojure/tickets/273
      #(conj (dissoc % :macro)
             (apply dissoc (meta (var ~orig)) (remove #{:macro} (keys %)))))
     (var ~name)))
  ([name orig doc]
   (list `defcopy (with-meta name (assoc (meta name) :doc doc)) orig)))
(defn ffilter ; can be find-first too
  "Returns the first item of coll for which (pred item) returns logical true.
   Consumes sequences up to the first match, will consume the entire sequence
   and return nil if no match is found."
  [pred coll] (first (filter pred coll)))
(defn map-dregs [f & colls] ; by TimMc
  "Like map but when there is a different count between colls, applies input fn
   to the coll values until the biggest coll is empty."
  ((fn map* [f colls]
     (lazy-seq
       (when (some seq colls)
         (cons (apply f (map first (filter seq colls)))
               (map* f (map rest colls))))))
   f colls))
(defn update
  "Updates the value in map m at k with the function f.
  Like update-in, but for updating a single top-level key.
  Any additional args will be passed to f after the value."
  ([m k f] (assoc m k (f (get m k))))
  ([m k f x1] (assoc m k (f (get m k) x1)))
  ([m k f x1 x2] (assoc m k (f (get m k) x1 x2)))
  ([m k f x1 x2 & xs] (assoc m k (apply f (get m k) x1 x2 xs))))
(defn update-in*
  "Updates a value in a nested associative structure, where ks is a sequence of keys and f is a
  function that will take the old value and any supplied args and return the new value, and returns
  a new nested structure. If any levels do not exist, hash-maps will be created. This implementation
  was adapted from clojure.core, but the behavior is more correct if keys is empty and unchanged
  values are not re-assoc'd."
  [m keys f & args]
  (if-let [[k & ks] (seq keys)]
    (let [old (get m k)
          new (apply update-in* old ks f args)]
      (if (identical? old new)
        m
        (assoc m k new)))
     (apply f m args)))

(defn update-each
  "Update the values for each of the given keys in a map where f is a function that takes each
  previous value and the supplied args and returns a new value. Like update-in*, unchanged values
  are not re-assoc'd."
  [m keys f & args]
  (reduce (fn [m key]
                 (apply update-in* m [key] f args))
                   m keys))
(defn update-vals ; by Jay Fields
  "Updates all the values of a map"
  [m f & args]
  (reduce (fn [r [k v]] (assoc r k (apply f v args)))
          {} m))
(require '[clojure.reflect :refer [reflect]] '[clojure.pprint :refer [print-table]])

(defn get-members [some-type]
  (->> (-> some-type reflect :members)
         (filter :exception-types)
         (sort-by :name)
         print-table))
(defn call-method
  "Calls a private or protected method.

   params is a vector of classes which correspond to the arguments to
   the method e

   obj is nil for static methods, the instance object otherwise.

   The method-name is given a symbol or a keyword (something Named)."
  [klass method-name params obj & args]
  (-> klass (.getDeclaredMethod (name method-name)
                                (into-array Class params))
      (doto (.setAccessible true))
      (.invoke obj (into-array Object args))))
(defn get-field
  "Access to private or protected field. field-name is a symbol or
  keyword."
  [klass field-name obj]
  (-> klass (.getDeclaredField (name field-name))
      (doto (.setAccessible true))
      (.get obj)))
(defn unlazy
  "Same as map/filter/reduce, but preserves the input data type."
  [core-f f coll]
  (into (empty coll) (core-f f coll)))

;;;;;;;;;;;;;;;; my own ;;;;;;;;;;;;;;;;

(defn update-multi
  "Updates multiple keys of a map with multiple fns using a map of key/fn pairs."
  [m fn-m]
  (merge
    m
    (into {} (map (fn [[k f]] [k (f (k m))]) fn-m))))
(defn str->stream [string] (-> string .getBytes clojure.java.io/input-stream))
(def nilify (constantly nil))
(defn or->
  "Same as -> but defaults to the initial value if the result is falsey."
  [x & args]
  (or (eval `(-> ~x ~@args)) x))

Thank you for taking the time.

Thanks for your interest in contributing.

We're pretty mindful of keeping plumbing.core small -- just to those things we wish were in clojure.core, more or less. That's a pretty high bar; internally, we have a plumbing.core-incubator namespace, and only things that marinade in there for a few months and see a lot of usage are considered for inclusion.

Are there any of these that you find particularly useful and think meet this standard? FWIW, update is already in there.

That's cool. Then I'm going to use the incubator namespace if it can reduce my codebase. Btw, can you provide me a link to that namespace? (it's embarrassing but I can't find it haha)
The ones I would really like to see in clojure.core:

  • defcopy - useful for aliasing a form while keeping its metadata. I use it to keep the docstring for aliases.
  • ffilter or find-first - this one doesn't need introduction.
  • update-each - useful for updating several map keys with same fn.
  • update-multi - useful for updating several map keys with several fns.
  • or-> - too often do I find myself using the original value when -> returns nil (but can be used for false too).
  • unlazy - this one is just so useful, when you want to revert from the lazy-seq to the input type when doing map/reduce/filter.
  • map-dregs would be cool too for clojure.core, but I'm not sure how frequently it would be used.

update-multi and unlazy are names I came up with. They could be named differently.

Well defcopy wont work in cljs and current impl of or-> also.

Cool, thanks. core-incubator is an internal thing currently -- it's by definition unstable, so not sure it's something we actually want to expose publicly.

defcopy: we're currently using potemkin/import-vars for this.

find-first: a deliberate omission -- we decided it was not shorter/clearer enough to merit inclusion.

update-each: how often do you want this rather than map-keys?

update-multi: could see this as useful, but I'm not sure how often. If anywhere, it would probably go in plumbing.map -- will have to think about it.

or->: when do you find yourself using this? I don't think I've really seen this pattern (and using eval like this is a big no-no IMO).

unlazy: I think mapv fills most of this niche for us, and I think being explicit is often a good thing. I think this also reverses the order for lists?

potemkin/import-vars looks neat. defcopy allows me to additionally rename vars, which import-vars doesn't (it seems). I do rename them a lot btw :) I even make copies of the var in the same ns, for different naming tastes.

I use map-keys and update-each. The former updates all keys with one fn, the latter updates values of selected keys with one fn, so they are quite different. Maybe the naming is ambiguous. An example: (update-each {:a 1 :b 2 :c 4} [:a :b] inc) => {:a 2, :c 4, :b 3}

update-multi: I use it 2 times here and it's just 128 LOC. It may be some design fault on my part, though haha.

or->: I'm only using it in the same source file, even though I felt the need for it in the past many times. The example is too simple, though. I think of it as defaulting to the original value when the result is false. Maybe the name is not the best either. I found some issues with the fn (besides eval of course) so better ignore it for the moment. Sorry about this one. It's not mature enough.

unlazy: yes, it reverses lists, well spotted! I guess I never tried it with lists haha. Being explicit is sound. Which fn do you use for maps?

So it leaves us with defcopy, update-each and update-multi. Do you want to leave defcopy out also?

Thanks for the details. I think we'll skip worrying about defcopy for now, until we do something with potemkin. For update-multi, after looking at these examples, I think just a -> with multiple calls to update is clearer, and nearly as short. Do you have concrete examples of where you use update-each?

Sorry for being a stickler about this stuff -- but I've spent way too much time over the years cleaning up huge namespaces of my utility functions that seemed like a good idea at the time but in hindsight weren't really net wins.

Yes, one could do that with ->. My point there was to save 1 line. Maybe it's not as readable if each key must go in a different line anyway, but if it's a small one liner, I think it's a win, like this one in that file: (update-multi {:allFeaturesArray seq :partOfSpeech #(split % #",")}). I would argue that if it's a 1 liner, readability may increase, when comparing to :keys scattered through lines + ->.

I found just one example of update-each in my codebases:

(defn sync-timecodes [unsynced-sub synced-sub]
  (let [time-offset (- (-> synced-sub :events first :Start)
                       (-> unsynced-sub :events first :Start))]
    (update-in unsynced-sub [:events]
      (partial map
        #(update-each % [:Start :End] + time-offset)))))

Both :Start and :End would naturally be updated with the same fn when shifting time by an offset.

Don't worry, I'm already learning a lot with this ping pong and it was a pleasure :)

It's clear by now there isn't an obvious need of any of the remaining forms, so I'm closing this.

For future reference, you may want to see defcopy, update-each and update-multi.

Thanks for the discussion. It was really useful for me.

Thanks for understanding -- we'll keep these in mind for future updates.