Cartesian product of functions in Clojure

Hi all,

I seem to be running into the same kind of situation often, where I feel the need for a certain operation, but can’t seem to find a handy way to perform it in Clojure. Theoretically, it feels as though a cartesian product of functions would do the trick.

This might seem like a very theoretical question, but I often (mostly when using threading macro’s) find myself in the situation where I have something that feels like a tuple of objects (I program in Python often), where I have an operation I want to perform only on one element of the tuple, or to apply operations to each of the elements.

In made up clojure this might be something like:

([inc, dec] [2 2])
->[3 2]

where two functions that act on integers are combined into a function that acts on a pair of integers.

Or more often I find myself wanting to create a [f, identity]. For example, as a toy-project I’m trying to do some accounting in Clojure a la hledger. I had just created a definition

(defn make-transaction [date description & postings]
   {:date date
    :description descriptions
    :postings (map (partial apply make-posting) postings)})

And created some test data:

(def data
  [["2021/01/01" "Buy an apple"
    ["expenses:groceries" 0.45]
    ["assets:checking" -0.45]]
   ["2021/01/15" "Buy a lemon"
    ["expenses:lemons" 0.30]
    ["assets:checking" -0.30]]
   ["2021/02/01" "Buy another apple"
    ["expenses:groceries" 0.45]
    ["assets:checking" -0.45]]
   ["2021/02/03" "Buy a red grapefruit"
    ["expenses:grapefruits" 0.80]
    ["assets:checking" -0.80]]])

And now I want to make local-date’s of the dates, instead of strings. I think the make-transaction shouldn’t do the parsing, so I want to hand it parsed dates. Being lazy, I’d love it if I could do something like

(map [#(local-date "yyyy/MM/dd" %) identity & identity ] data)

to only apply the function local date to each first entry and keep everything else. This would save me from having to add (local-date "yyyy/mm/dd ) four times.

So summarizing, I think I have three questions:

  1. How to apply a function only to a single element of a vector and keep the rest (is this what lenses do?)
  2. More generally, how to construct the cartesian product of functions. Or, how to achieve a similar result or avoid the use of cartesian products by being more clojuristic
  3. Do you see something about my thinking (e.g. the mention of the Python tuple) that indicates that I haven’t used Clojure enough :wink:

Thanks in advance for any thoughts!
Florian

1 Like

You can use destructuring and then your syntax is almost correct:

(mapv
  (fn [[date description & postings]]
    (apply vector (local-date "yyyy/MM/dd" date) description postings)
  data)

You can also use the backquote syntax, which is more compact, but possibly more obscure:

(mapv
  (fn [[date description & postings]]
    `[~(local-date "yyyy/MM/dd" date) ~description ~@postings]
  data)

Though now it looks almost exactly as your pseudo-syntax.

Now, it turns out that vectors are also tuples in Clojure, and that means they are indexed. So in fact you can simply do:

(mapv
  #(assoc % 0 (local-date "yyyy/MM/dd" %))
  data)

Now, you’re not the only person who ends up wanting something more for deep manipulation of data-structures (especially in a type preserving manner). Personally I keep to core functions, like my example demonstrate, there’s many simple ways with core functions. But if you’re intrigued, here’s a few libraries to check out:

And yes if you search for Clojure lens, most of them also fall into this use case.

You can easily do that with map (actually, assuming you wanted [3 1] as your output?):

(mapv (fn[f v] (f v)) [inc dec] [2 2])
;; => [3 1]

Since you can’t do reader macros à la CL, you need to make your own function for this, I suppose, but it’s certainly doable.

Edit: maybe something like this?

(defn c* [fns]
  (fn [values]
    (mapv (fn[f v] (f v)) fns values)))

(def my-fn-product (c* [inc identity dec]))

(my-fn-product [1 2 3])
;; => [2 2 2]

In fairness, if you do have a sufficiently nested data struct, something like specter or meander is a must-have. I had to do something like that at my previous job (it was the most natural way of representing the domain), and doing complex transformations became one-liners with specter, whereas with core functions, it would’ve been monstrous…

1 Like

clojure reader-macros

1 Like

I’m not sure I’m understanding the question, but you might be interested in juxt. Here’s an example:

((juxt inc dec) 1)
=> [2 0]
(map (juxt inc dec) [2 2])
=> ([3 1] [3 1])

TIL… Thanks for the link, @joinr !

I understood he wants to apply each fn in the vector to the corresponding entry in the arguments vector…

1 Like

Thanks for all the replies and suggestions! I guess it’s true what they say about the Clojure community being very helpful :slight_smile:
I’ve experimented with your suggestions and gained some new insights. If someone’s interested, here are my results. First I decided to change the format of the data a bit, to have the postings as a single vector, instead of as a variable number of items at the end:

(def data
  [["2021/01/01" "Buy an apple"
    [["expenses:groceries" 0.45]
     ["assets:checking" -0.45]]]
   ["2021/01/15" "Buy a lemon"
    [["expenses:lemons" 0.30]
     ["assets:checking" -0.30]]]
   ["2021/02/01" "Buy another apple"
    [["expenses:groceries" 0.45]
     ["assets:checking" -0.45]]]
   ["2021/02/03" "Buy a red grapefruit"
    [["expenses:grapegruits" 0.80]
     ["assets:checking" -0.80]]]])

And then I experimented a bit with the different suggestions, inside of a threading macro. First two are from @didibus.
Here I like the simple way to see that we’re adjusting the first item, and nothing else:

(->> data
     (mapv #(assoc % 0 (local-date "yyy/MM/dd" (nth % 0))))
     (mapv (partial apply make-transaction)))

Destructuring makes it easy to name the contents of your vector. Definitely something I should use more often:

(->> data
     (mapv (fn [[date description postings]]
             `[~(local-date "yyy/MM/dd" date) ~description ~postings]))
     (mapv (partial apply make-transaction)))

Then @mvarela 's suggestion to define a function for it:

(defn c* [fns]
  (fn [values]
    (mapv (fn [f x] (f x)) fns values)))

(->> data
     (mapv (c* [(partial local-date "yyyy/MM/dd") identity identity]))
     (mapv (partial apply make-transaction)))

I expanded on this suggestion a bit by introducting a cartesian map:

(defn cmap [fns coll]
  (mapv (c* fns) coll))

(->> data
     (cmap [(partial local-date "yyyy/MM/dd") identity identity])
     (mapv (partial apply make-transaction)))

(->> data
     (cmap [#(local-date "yyyy/MM/dd" %) identity identity])
     (mapv (partial apply make-transaction)))

To me this seems closest to what I was looking for, although I’m not sure yet if I find the cartesian map a natural enough operation for use in the wild.

The juxt function is indeed something slightly different from what I was looking for, but still thanks for the suggestion, because it looks like a useful function to know!

2 Likes

Since you changed your data-structure, you don’t need to use backquote to inline things, so you can use a normal vector instead:

(->> data
     (mapv (fn [[date description postings]]
             [(local-date "yyy/MM/dd" date) description postings]))
     (mapv (partial apply make-transaction)))

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.