0

So I recently learned that I cannot modify parameters in a Clojure function. I have a big function that takes in a list, then does about 5 different steps to modify it and then returns the output.

But what I'm doing now is

(defn modify [my-list other params]
  (if (nil? my-list)
      my-list
      (let [running my-list]
       (def running (filter #(> (count %) 1) my-list)) 
       (def running (adjust-values running))
   ; a whole bunch of other code with conditions that depend on other parameters and that modify the running list
       (if (< (count other) (count my-list))
           (def running (something))
           (def running (somethingelse)))
       (def my-map (convert-list-to-map my-list))
       (loop [] .... ) loop through map to do some operation
       (def my-list (convert-map-to-list my-map)
     running
)))

This doesn't seem correct, but I basically tried writing the code as I'd do in Python cuz I wasn't sure how else to do it. How is it done in Clojure?

0

2 Answers 2

4

Instead of using def inside the modify function, you can have a let with multiple bindings. Actually, def is typically only used at the top-level to define things once and is not meant to be a mechanism to allow for mutability. Here is an example of a let with multiple bindings, which is similar to introducing local variables except that you cannot change them:

(defn modify2 [my-list other params]
  (if (nil? my-list)
      my-list
      (let [a (filter #(> (count %) 1) my-list)
            b (adjust-values a)
            c (adjust-values1 other b)
            d (adjust-values2 params c)]
        d)))

Here we introduce new names a, b, c and d for each partial result of the final computation. But it is OK to just have a single binding that gets rebound on each line, that is, you could have a single binding running that gets rebound:

(defn modify2 [my-list other params]
  (if (nil? my-list)
      my-list
      (let [running (filter #(> (count %) 1) my-list)
            running (adjust-values running)
            running (adjust-values1 other running)
            running (adjust-values2 params running)]
        running)))

Which one you prefer is a matter of style and taste, there are up and downsides with either approach. The let form to introduce new bindings is a powerful construct, but for this specific example where we have a pipeline of steps, we can use the ->> macro that will generate the code for us. So we would instead write

(defn modify3 [my-list other params]
  (if (nil? my-list)
      my-list
      (->> my-list
           (filter #(> (count %) 1))
           adjust-values
           (adjust-values1 other)
           (adjust-values2 params))))

It takes the first macro argument and then passes it in as the last parameter to the function call on the following line. Then the result of that line goes in as a last parameter to the line that follows and so on. If a function call just takes a single argument as is the case for adjust-values in the example above, we don't need to surround it with parentheses. See also the similar -> macro.

To see which code is generated by the ->>, we can use macroexpand:

(macroexpand '(->> my-list
                   (filter #(> (count %) 1))
                   adjust-values
                   (adjust-values1 other)
                   (adjust-values2 params)))
;; => (adjust-values2 params (adjust-values1 other (adjust-values (filter (fn* [p1__7109#] (> (count p1__7109#) 1)) my-list))))

Added: Summary

If your computation has a pipeline structure, the -> and ->> macros can be used to express that computation concisely. However, if your computation has a general shape where, you will want to use let to associate symbols with results of sub-expressions, so that you can use those symbols in subsequent expressions inside the let form.

Sign up to request clarification or add additional context in comments.

2 Comments

Actually you might have misunderstood but the comment about "a bunch of other code and conditions" is more complex than simply calling a function, i edited my post.
@user3710760 Well, for arbitrary computations, let is what you want to be using. Whether you rebind the same symbol e.g. running or introduce new symbols to refer to each sub result is up to you. Don't use def for this.
3

Yes, this is the way Clojure was designed: as a functional language with immutable data (of course with fallbacks to what the host offers if you want or need it).

So if you want to modify data in consecutive steps, then you can either chain the calls (looks nicer with the threading macros). E.g.

(->> my-list 
     (filter #(> (count %) 1)) 
     (adjust-values))

This is the same as:

(adjust-values 
  (filter #(> (count %) 1) 
          my-list))

If you prefer to do that in steps (e.g. you want to print intermediate results or you need them), you can have multiple bindings in the let. E.g.

(let [running my-list
      filttered (filter #(> (count %) 1) running)
      adjusted (adjust-values filtered)
      running ((if (< (count other) (count adjusted)) something somethingelse))
      my-map (convert-list-to-map my-list)
      transformed-map (loop [] .... )
      result (convert-map-to-list transformed-map)]
  result)

This returns the adjusted values and holds on to all the things in between (this does nothing right now with the intermediate results, just an example).

And aside: never ever def inside other forms unless you know what you are doing; def define top level vars in a namespace - it's not a way to define mutable variables you can bang on iteratively like you might be used to from other languages).

4 Comments

you mean top level vars that should not be modified? what if i have a while loop that cycles through the values and keeps updating them, how do I define it?
Only def things on top level of the namespace (e.g. like defn).
@user3710760 I can only suggest to read up on how clojure and functional programming in general do things. I know how it feels to grasp for the things you know from imterative programming, but things are different here. Check out how loop or reduce work. You will always get a result from your functions and you can continue to work from this result if you hold on to it in a let.
@user3710760 I have adjusted the let example to show how to go along the x' = f x path

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.