Lassi
Lassi

Reputation: 3950

Why no multiple return values in Clojure

What's the rationale for the lack of multiple return value support in Clojure? (Clojure doesn't seem to have anything similar to Common Lisp's values/multiple-value-bind)

Is explicit destructuring considered more idiomatic in a functional programming style, does it have something to do with the JVM, or was it simply thought to be too much complexity for too little gain?

Upvotes: 4

Views: 2474

Answers (3)

glts
glts

Reputation: 22684

We can speculate about the why, but looking at the capabilities of Clojure in this area I think we can say it simply does not need multiple return values.

Out of the box, there are two basic ways of emulating multiple returns in Clojure: composite values for destructuring, and dynamically bound auxiliary returns.

Here is the destructuring style, where all results are returned eagerly and can be destructured by the caller as desired:

(defn foo
  "Returns a map with results at keys :ret and :aux."
  []
  {:ret :primary, :aux :auxiliary})

(let [{:keys [ret]} (foo)]
  (vector ret))                 ; => [:primary]
(let [{:keys [ret aux]} (foo)]
  (vector ret aux))             ; => [:primary :auxiliary]

Here is the dynamic var auxiliary return style, where optional results are opt-in (caller signals interest) and returned via side channel:

(def ^:dynamic *foo-aux* nil)

(defn foo
  "Returns result, and auxiliary result in *foo-aux* if bound."
  []
  (when (thread-bound? #'*foo-aux*)
    (set! *foo-aux* :auxiliary))
  :primary)

(vector (foo))                ; => [:primary]
(binding [*foo-aux* nil]
  (let [ret (foo)]
    (vector ret *foo-aux*)))  ; => [:primary :auxiliary]

Depending on the style, results are computed and returned eagerly, or explicitly asked for by the caller and thus computed on demand. Both styles allow callers to pick and choose results à la carte.

Upvotes: 2

Alex Hernandez
Alex Hernandez

Reputation: 29

I think the answer why is because it doesn't need a multiple value support and the reason is doesn't need one is because one can be easily written with macros. I work mostly in Common Lisp and know some Clojure but its seems that you are probably correct about it being more idiomatic. Clojure where has more included support of maps and vectors and destructuring them than Common Lisp does, so it is easier, safer, and probably more efficient to manually destructure rather than include a version of multiple-value-bind and values.

On a side note, I rarely use values and multiple-value-bind except when it makes more idiomatic sense that a function would "naturally" return a single value, like in the case of (mod 5 4), where only sometimes I want the second value.

Upvotes: 0

Alan Thompson
Alan Thompson

Reputation: 29958

It sounds like you are already familiar with basic Clojure destructing with vectors & maps:

(defn calc-sqr-vec [x]
  [ x  (* x x) ])         ; returns a vector of 2 values

(defn calc-sqr-map [x]
  { :x x  :y (* x x) })   ; returns a map of 2 entries

(let [ [x y] (calc-sqr-vec 3) ]
  (println (format "3^2 -> [%d,%d]" x y)))

(let [ {:keys [x y]} (calc-sqr-map 3) ]
  (println (format "3^2 -> [%d,%d]" x y)))

vec: 3^2 => [3,9]
map: 3^2 => [3,9]

where you wrap the two return values x & y in a single vector or map, and the caller pulls out the component values when desired.

I cannot answer the why part in relation to CL, but one big benefit compared to the Python-style multiple return values is the question of what to do when the user doesn't specify all the return values. For example:

q, r = divmod(22, 7)
q => 3
r => 1

q = divmod(22, 7)
q => (3,1)

So, in Python the same expression divmod(22, 7) generates different results that depend on the "receiving" part of the statement. This type of complexity is avoided by always returning the same, single value and allowing the caller the choice of when & how to pull out the desired bits (and ignore unwanted bits).


Update

It's interesting that this topic came today, since just yesterday I was working with functions that needed to return a bunch of separate values. I wrote a short macro to make it easier. The unit test shows it in action:

(dotest
  (let [some-fn (fn []
                  (let [a 1
                        b 2
                        c 3
                        d 4
                        e 5]
                    (label-value-map a b c d e))) ]
    (is= {:a 1 :b 2 :c 3 :d 4 :e 5} (some-fn))
    (let [ {:keys [a b c d e]} (some-fn) ]
      (is= [a b c d e] [1 2 3 4 5]))))

So using label-value-map and the plain-old {:keys [a b c d e]} destructuring you can transfer a bunch of scalar values from one place to another with less typing. :)

Upvotes: 5

Related Questions