Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure : more than 1 variadic overload, more idiomatic way to do?

For some time, I have been asking myself whether there is a way to define a function with more than one variadic overload. Here is a sample function I wrote (I know there is no exception management plus maybe there is a better way to code it -actually I did not debug it - but I just focus here on the variadic aspect) :

(defn write-csv
  "Writes a csv from data that is already formatted for clojure.data.csv/write-csv or not.
   In the second case, the function guesses a header and writes it. Can handle three types of
   data : nested rows (example : {1 {:a 2 :b 3} 2 {:a 25 :b 17} ...), flattened data (like the one you use
   in clj-data-process-utils.data (example : ({:id 1 :a 2 :b 3} {:id 2 :a 25 :b 17} ...)) or already formatted
   data (example : [['ID' 'B' 'C'] [1 2 3] [2 25 17]]). Note that in the last case you have to provide a header if you want one.
   The guesses can be overriden by the :header arg. Optimized for Excel with default values.
   Input :
   - data : data to write as CSV
   - path : the filepath of the new CSV
   - (optional) sep : the separator to use, must be of type char [default : ;]
   - (optional) dec : the decimal separator to use, must be of type char [default : .]
   - (optional) newline : the newline character, see cljure.data.csv options, default here for windows [default : :cr+lf]
   - (optional) header : if you want to provide your own data, pass here a vector of columns names, guesses by default if data is not formatted [default : :guess]"
  [data path & {:keys [sep dec newline header] :or {sep \; dec \. newline :cr+lf header :guess}}]
  (let [f-data (cond (or (map? data) (seq? data))
                       (cond (vec? header)
                               (format-for-csv sep data header)
                             (= :guess header)
                               (->> (guess-header data)
                                    (format-for-csv sep data)))
                     (vec? data)
                       data)
        wrtr (io/writer path)]
    (csv/write-csv wrtr f-data :separator sep :newline newline)))

As you can see, we can optionally pass a header. I put it on optional keys but I would have preferred to have something like this in the first instance (even if this aritties map is ok for me) :

(defn write-csv 
  ([data path & {:keys [sep dec newline] :or {sep \; dec \. newline :cr+lf}}]
    ...)
  ([data header path & {:keys [sep dec newline] :or {sep \; dec \. newline :cr+lf}}]
    ...))

Of course it does not work because we can't have more than 1 variadic overload. I prefer it only because it is more clear for an end-user.

I so tought about two things :

  • using a second private function with apply...but it does not solve the problem in teh first isntance because I want two possible styles of inputs.
  • I looked about defmultibut I saw that it also takes the same arities for every submethods

Of course I can also split the function into two or distinguish two cases in the first arg (a vector with types [vector map] would mean that the user has passes not formatted data + a header) but is is worse for the user. I really want to offer these inputs possibilities.

Is there something I haven't notice in clojure functions or is it a deeper problem that we cannot solve ?

Thanks !

like image 977
Joseph Yourine Avatar asked Sep 11 '25 13:09

Joseph Yourine


1 Answers

To keep all the keyword args in a separate map is the best solution, i guess.. But there is also one relatively popular way to do it, if you really need (which i think is not): you can use one variadic signature in combination with arglists metadata:

user> (defn parse
        {:arglists '([data header? path & {:keys [sep dec newline]
                                           :or {sep :aaa
                                                dec :bbb
                                                newline :ccc}}])}
        [data header-or-path & args]
        (let [[header path {:keys [sep dec newline]
                            :or {sep :aaa dec :bbb newline :ccc}}]
              (if (even? (count args))
                [nil header-or-path args]
                [header-or-path (first args) (rest args)])]
          (println data header path sep dec newline)))
#'user/parse

user> (parse 1 2)
1 nil 2 :aaa :bbb :ccc
nil

user> (parse 1 2 3)
1 2 3 :aaa :bbb :ccc
nil

user> (parse 1 2 :sep :a :dec :b)
1 nil 2 :a :b :ccc
nil

user> (parse 1 2 3 :sep :a :dec :b)
1 2 3 :a :b :ccc
nil

The ide you (or the user of your lib) use will show the signature from :arglists, ignoring the real signature:

user/parse
 [data header? path & {:keys [sep dec newline], :or {sep :aaa, dec :bbb, newline :ccc}}]
  Not documented.
user/parse is defined in *cider-repl localhost*.

But again: this is too verbose and difficult to maintain (due to the code duplication), so you should use it wisely (by which i mean that no one should do it at all)

like image 171
leetwinski Avatar answered Sep 13 '25 06:09

leetwinski