jared-nelsen
jared-nelsen

Reputation: 1231

Passing Empty Lists to Functions to Collect Results

I've been unable to find any doctrinal Clojure examples about what I am trying to understand and wanted some input on it.

Traditionally when I use recursion in imperative languages I often pass in an empty list to recursive functions in order to collect the results of some computation down through the stack.

As a baby example in Java:

public List<Integer> results = new ArrayList<>();

private List<Integer> add(List<Integer> list, int count){
    if(count > 10){
        return list;
    }

    count = count + 1;
    list.add(count);

    return add(list, count);
}

Which, if run with:

add(results, 0)

will result in a list of values 0 through 10.

Is this idiomatic in Clojure?

(defn t [list i]
    (conj list i)
    (if (<= i 10)
        (recur list (inc i))))

(t '() 0)

Secondly, why is it that when I evaluate:

(t '() 0)

do I get this error:

clojure.lang.Compiler$CompilerException: java.lang.ClassCastException: java.lang.Long (in module: java.base) cannot be cast to clojure.lang.IFn

Upvotes: 1

Views: 1632

Answers (3)

Ivan Grishaev
Ivan Grishaev

Reputation: 1679

Pay attention, conj behaves differently for vector and list types. For a vector, it appends an element to the end. For a list, it puts it in front of its head:

user=> (conj [1 2 3] 4)
[1 2 3 4]
user=> (conj '(1 2 3) 4)
(4 1 2 3)

So if anybody passes a vector instead of a list, he or she will face unpredictable behavior. Please note, in Clojure vectors are more common than lists (unlike in Scheme or Common list for example).

The concat function works fine for both types:

user=> (concat [1 2 3] [4])
(1 2 3 4)
user=> (concat '(1 2 3) [4])
(1 2 3 4)

Then, calling (conj list i) returns a new list without changing the previous one. This call should be placed into some other function call, say, inside recur or let. Otherwise, you do it in vain.

Upvotes: 0

GregA100k
GregA100k

Reputation: 1395

You certainly can pass an empty list in as a starting point for your result. The clojure idiom that applies here is that the function call

(conj list i)

returns a new list with i added to it, but it does not mutate list so in the t function, that line will not produce any results. To use the results of that function call, it should be passed as a parameter to the recur similar to the (inc i)

The second thing to note is that there is no else condition in the if statement so once the value of i is greater than 10, the if statement will return nil

(defn t1 [list i]
  (if (<= i 10)
    (recur (conj list i) (inc i))
    list))

Upvotes: 2

Leonel
Leonel

Reputation: 29217

Is this idiomatic in Clojure?

It's not.

In Clojure, data structures are immutable. Thus, once you call a function passing a list, the function cannot modify it. So, it's not only not idiomatic, it's impossible.

Instead, the idiomatic way to achieve what you want in Clojure is to call the recursive function to build a smaller list, and append into it.

(defn my-range [n]
  (if (< n 0)
    []       ; empty list
    (conj
     (my-range (dec n)) ; recursive call to build smaller list
     n)))     

Having said that, the snippet of Java you supplied is not great either; in general, here's a good convention:

  • If the method has a void return type, you may expect it to have side effects, and maybe modify one of its parameters
  • If the method has a return type, then you may expect the parameters not to be modified.

Giving the method a return type and also modifying a parameter at the same time makes for confusing code. A user of your code cannot know whether to use the return or to supply its own list to be modified.

Upvotes: 1

Related Questions