weavejester / medley

A lightweight library of useful Clojure functions
Eclipse Public License 1.0
865 stars 66 forks source link

Add flip function #83

Closed irigarae closed 9 months ago

irigarae commented 9 months ago

Some function I've used for a while, and maybe it could be useful to have available in the library for everybody is flip: some function that takes extra arguments as partial but moves the argument to the first position. Inspired by the name in other languages. I've mainly used it to mix well some functions that lend themselves to ->> or partial with the ones that work with ->.

(= (map #(assoc % :x 3) xs)
   (map (flip assoc :x 3) xs))
#_=> true
(map #(dissoc % :id :secret) xs)
(map (flip dissoc :id :secret) xs)
(map #(str/split % #"," -1) xs)
(map (flip str/split #"," -1) xs)

If we generalize it we can accept multiple arguments also in the generated function instead of just one. Inspired by the source code of clojure.core/partial: the function flip can take many arguments and only rearrange the first one in front of f, while leaving the rest behaving as in partial. Suggested definition based on partial:

(defn flip
  "Takes a function f and fewer than the normal arguments to f, and
  returns a fn that takes an additional arg. When called, the returned
  function calls f with the additional arg as first argument."
  [f & args]
  (fn [x] (apply f x args)))

;; examples with mathematical function
(mapv (flip / 3) (range 0 10 3)) #_=> [0 1 2 3]
(mapv (flip - 3) (range 0 10 3)) #_=> [-3 0 3 6]
;; but works with things that have a significant first argument like
update assoc dissoc str/split re-find ; etc.

Leaving it here for discussion (maybe this amount of flexibility is not needed, or the returned function should not accept 0-arity like partial, etc.). I can make a pull request if preferred.

weavejester commented 9 months ago

This PR seems related to #63, but in the opposite direction.

I don't quite understand the purpose of this function, though. In the examples you give, the flip version is less concise. Do you have an example that would show the benefits of this function?

irigarae commented 9 months ago

In general I've seen flip used as a function to switch the order of two arguments of a function (actually lodash is mentioned in the #63 that you mentioned, but I've seen it in haskell).

(defn flip [f y] (fn [x] (f x y)))
;; which can always be replaced as a lambda
(flip / 2) #_=> #(/ % 2)
;; it's complementary to partial in a way
(partial / 2) #_=> #(/ 2 %)

It can work whenever the function you are calling requires the variable argument in first position; if the argument was in last position you can always use (partial f ,,,). These are random examples, but as I say the function flip can always be substituted by just a lambda, but it's the same for partial, however I feel it's good to give it a name.

;; divide by
(mapv (flip / 3) (range 0 10 3)) #_=> [0 1 2 3]
;; substract
(mapv (flip - 3) (range 0 10 3)) #_=> [-3 0 3 6]
;; split
(mapv (flip str/split #",") ["a,b" "c,d,e,,," "f,g" "" "h"]) #_=> [["a" "b"] ["c" "d" "e"] ["f" "g"] [""] ["h"]]

However even if you still want to generate an operation that operates on a first argument, there are many functions that actually have more arguments, so being limited to 2 arguments doesn't make sense. It makes sense to have something like this in clojure:

(defn flip [f & args]
  (fn [x] (apply f x args)))

;; we can use more arguments
(let [split-comma (flip str/split #"," -1)]
  (split-comma "a,b,c,,,")) #_=> ["a" "b" "c" "" "" ""]
(let [ensure-orphan (flip assoc :parent :none)]
  (mapv ensure-orphan [{:id 3 :parent 7} {:id 3 :parent 2}])) #_=> [{:id 3, :parent :none} {:id 3, :parent :none}]
(mapv (flip / 7 5) (range 6)) #_=> [0 1/35 2/35 3/35 4/35 1/7]

It's giving it a name to the concept of actually having a function that is only missing the first argument (which is usually the one that is modified for these types of functions), but indeed one could always just write a lambda, e.g., #(math/pow % 3) instead of (flip math/pow 3).

Regarding #63, this suggestion is not intended specifically to play nice with these update-in and family. However it's the missing piece together with comp and partial, this could be a way of writing the same thing (although as I say it's a side effect, not the main design idea):

(let [m {:users [{:id 1 :orders #{{:items [{:price 1} {:price 4} {:price 2}]}}}
                 {:id 2 :orders #{}}]}]
  (= (->> (partial str "$")
          (flip update :price)
          (partial mapv)
          (flip update :items)
          (partial map)
          (comp set)
          (flip update :orders)
          (partial mapv)
          (update m :users))
     {:users [{:id 1, :orders #{{:items [{:price "$1"} {:price "$4"} {:price "$2"}]}}}
              {:id 2, :orders #{}}]}))

In any case, the suggested one was after looking at all the flexibility and default cases defined by partial. But it could be simply ignored, since it might be confusing. I will simplify the given definition in the first post.

weavejester commented 9 months ago

Because flip can always be substituted for a more concise lambda, I don't think this is a function that would be suitable for inclusion in Medley. A function like #(/ % 2) is also more understandable than (flip / 2), as the reader doesn't need to look up the definition of flip.

There could be some benefits when lambdas are nested, but this seems enough of an edge condition that I don't think there's enough of an advantage to its inclusion in this library.

irigarae commented 9 months ago

More concise is subjective, I think it goes in the same direction of giving names to specific operations, e.g., map filter reduce iterate cycle can all be done in a loop in Clojure, but giving them distinctive names allows to identify on the spot and have a good mental model of what the code is going to do. Same with partial, comp, and the suggested flip.

I understand it’s a small function and it even takes more characters than having the lambda 😂 I close this issue since the intention was just to bring it up in case it seemed useful for the library.