Question

I want to write a function that concatenates vectors or matrices, which can take arbitrary inputs. To combine two vectors I've written the follow code. It also also matrices to be combined such that columns are lengthened.

(defn concats 
([x y] (vec (concat x y))))

Where I am stuck is extending the input to n vectors or matrices, and combining matrices to make longer rows.

Ex) (somefunction [[:a :b] [:c :d]] [[1 2] [3 4]] 2]

[[:a :b 1 2] [:c :d 3 4]]

The 2 in the input designates level to concatenate.

Was it helpful?

Solution

If you're not interested in "how it works", here's the solution right up front (note that level is zero-indexed, so what you've called the 1st level I'm calling the 0th level):

(defn into* [to & froms]
  (reduce into to froms))

(defn deep-into*
  [level & matrices]
  (-> (partial partial mapv)
      (iterate into*)
      (nth level)
      (apply matrices)))

The short answer for how it works is this: it iteratively builds up a function that will nest the call to into* at the correct level, and then applies it to the supplied matrices.

Regular old into, given a vector first argument, will concatenate the elements of the second argument onto the end of the vector. The into* function here is just the way I'm doing vector concatting on a variable number of vectors. It uses reduce to iteratively call into on some accumulated vector (which starts as to) and the successive vectors in the list froms. For example:

user> (into* [1 2] [3 4] [5 6])
> [1 2 3 4 5 6]

Now for deep-into*, I had to recognize a pattern. I started by hand-writing different expressions that would satisfy different "levels" of concatenation. For level 0, it's easy (I've extrapolated your example somewhat so that I can make it to level 2):

user> (into* [[[:a :b] [:c :d]]] [[[1 2] [3 4]]])
> [[[:a :b] [:c :d]] [[1 2] [3 4]]]

As for level 1, it's still pretty straightforward. I use mapv, which works just like map except that it returns a vector instead of a lazy sequence:

user> (mapv into* [[[:a :b] [:c :d]]] [[[1 2] [3 4]]])
> [[[:a :b] [:c :d] [1 2] [3 4]]]

Level 2 is a little more involved. This is where I start using partial. The partial function takes a function and a variable number of argument arguments (not a typo), and returns a new function that "assumes" the given arguments. If it helps, (partial f x) is the same as (fn [& args] (apply f x args)). It should be clearer from this example:

user> ((partial + 2) 5)
> 7
user> (map (partial + 2) [5 6 7]) ;why was six afraid of seven?
> (7 8 9)

So knowing that, and also knowing that I'll want to go one level deeper, it makes some sense that level 2 looks like this:

user> (mapv (partial mapv into*) [[[:a :b][:c :d]]] [[[1 2][3 4]]])
> [[[:a :b 1 2] [:c :d 3 4]]]

Here, it's mapping a function that's mapping into* down some collection. Which is kind of like saying: map the level 1 idea of (mapv into* ...) down the matrices. In order to generalize this to a function, you'd have to recognize the pattern here. I'm going to put them all next to each other:

(into* ...) ;level 0
(mapv into* ...) ;level 1
(mapv (partial mapv into*) ...) ;level 2

From here, I remembered that (partial f) is the same as f (think about it: you have a function and you're giving it no additional "assumed" arguments). And by extending that a little, (map f ...) is the same as ((partial map f) ...) So I'll re-write the above, slightly:

(into* ...) ;level 0
((partial mapv into*) ...) ;level 1
((partial mapv (partial mapv into*)) ...) ;level 2

Now an iterative pattern is becoming clearer. We're calling some function on ... (which is just our given matrices), and that function is an iterative build-up of calling (partial mapv ...) on into*, iterating for the number of levels. The (partial mapv ...) part can be functionalized as (partial partial mapv). This is a partial function that returns a partial function of mapving some supplied arguments. This outer partial isn't quite necessary because we know that the ... here will always be one thing. So we could just as easily write it as #(partial mapv %), but I so rarely get a chance to use (partial partial ...) and I think it looks pretty. As for the iteration, I use the pattern (nth (iterate f initial) n). Perhaps another example would make this pattern clear:

user> (nth (iterate inc 6) 5)
> 11

Without the (nth ...) part, it would loop forever, creating an infinite list of incrementing integers greater than or equal to 5. So now, the whole thing abstracted and calculated for level 2:

user> ((nth (iterate (partial partial mapv) into*) 2)
        [[[:a :b][:c :d]]] [[[1 2][3 4]]])
> [[[:a :b 1 2] [:c :d 3 4]]]

Then, using the -> macro I can factor out some of these nested parantheses. This macro takes a list of expressions and recursively nests each into the second position of the successive one. It doesn't add any functionality, but can certainly make things more readable:

user> ((-> (partial partial mapv)
           (iterate into*)
           (nth 2))
        [[[:a :b][:c :d]]] [[[1 2][3 4]]])
> [[[:a :b 1 2] [:c :d 3 4]]]

From here, generalizing to a function is pretty trivial--replace the 2 and the matrices with arguments. But because this takes a variable number of matrices, we will have to apply the iteratively-built function. The apply macro takes a function or macro, a variable number of arguments, and finally a collection. Essentially, it prepends the function or macro and the supplied arguments onto the final list, then evaluates the whole thing. For example:

user> (apply + [1 5 10]) ;same as (+ 1 5 10)
> 16

Happily, we can stick the needed apply at the end of the (-> ...). Here's my solution again, for the sake of symmetry:

(defn deep-into*
  [level & matrices]
  (-> (partial partial mapv)
      (iterate into*)
      (nth level)
      (apply matrices)))

OTHER TIPS

Using the concats function you listed in the question:

user=> (map concats [[:a :b] [:c :d]] [[1 2] [3 4]])
([:a :b 1 2] [:c :d 3 4])

this doesn't take into account the level as you listed, but it handles the input given

Taking arbitrary number of arguments needs a replacement concats function

(defn conc [a b & args] 
  (if (nil? (first args)) 
     (concat a b) 
     (recur (concat a b) (first args) (rest args))))

Here are two examples

user=> (map conc [[:a :b] [:c :d]] [[1 2] [3 4]] [["w" "x"] ["y" "z"]])
((:a :b 1 2 "w" "x") (:c :d 3 4 "y" "z"))
user=> (map conc [[:a :b] [:c :d] [:e :f]] [[1 2] [3 4] [5 6]] [["u" "v"] ["w" "x"] ["y" "z"]])
((:a :b 1 2 "u" "v") (:c :d 3 4 "w" "x") (:e :f 5 6 "y" "z"))

Here are two different solutions for a function which will return a vector that's the concatenation of an arbitrary number of input collections:

(defn concats [& colls]
  (reduce (fn [result coll]
            (into result coll))
          []
          colls))

(defn concats [& colls]
  (vec (apply concat colls)))

The [& arg-name] notation in the argument lists is how you specify that the function is "variadic" - meaning it can accept a variable number of arguments. The result is that colls (or whatever name you pick) will be a sequence of all the arguments in excess of the positional arguments.

Functions can have multiple arities in Clojure, so you can also do things like this:

(defn concats
  ([x]
     (vec x))
  ([x y]
     (vec (concat x y)))
  ([x y & colls]
     (vec (apply concat (list* x y colls)))))

However, only one of the overloads can be variadic, and its variadic part must come last (i.e. you can't do [& more n], only [n & more].

The Clojure.org page on special forms has more useful information on argument lists in Clojure (in the section on fn).

The function below correctly handles the example input/output you provided. Unfortunately I don't think I understand how you want the levels (and associated numeric input) to work well enough to generalize it as far as you're looking for.

(defn concats [x y]
  ;; only works with two inputs
  (vec (map-indexed (fn [i v] (into v (nth y i)))
                    x)))

(concats [[:a :b] [:c :d]] [[1 2] [3 4]]) ;=> [[:a :b 1 2] [:c :d 3 4]]

But maybe it will give you some ideas anyway, or if you can add more information (especially examples of how different levels should work) I'll see if I can be more help.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top