Translation for Common Lisp idiom

Is there a good description for how to define lambda lists with keyword arguments in clojure? Does the concept exist? I know about (fn [x & others] ...) But what about keyword arguments?

I don’t find information about this in the function documentation.

If you mean function calls like

(foo x :bar 1)

It’s in the destructuring segment of the documentation since these binding forms work for any binding, not just lambdas
https://clojure.org/guides/destructuring#_associative_destructuring
You can pass them either as & args or as a map.
Go with whichever you find more readable unless you care about performance. Then prefer the map form.

Great, and it seems the bizarre feature I was hoping for actually works.
The default values are allowed to reference each other in the same way that it works in Common Lisp :slight_smile:

In this example debug defaults to the value of val and verbose defaults to the value of debug. Even if I call the function with :verbose or with :debug

clojure-rte.core> (defn configure [val & {:keys [debug verbose]
                                          :or {debug val, verbose debug}}]
                    (list val debug verbose))
#'clojure-rte.core/configure
clojure-rte.core> (configure 1)
(1 1 1)
clojure-rte.core> (configure 1 :verbose 3)
(1 1 3)
clojure-rte.core> (configure 1 :debug 3)
(1 3 3)
clojure-rte.core> 
2 Likes

How can I make a recursive call to a function like configure passing the exact same keys as I already have, and how can I make a recursive call to configure but overriding some of the keys?

This is one of the reasons I dislike [& {:keys []}] notation.
You can use the same destructuring form with a map, then write something like:

(defn configure
  [val {:keys [debug verbose]
        :or {debug val, verbose debug}
        :as opts}]
  (configure (inc val) opts))

Or some other contrived example.
That way opts is easy to carry around and/or modify as you see fit in your recursion, just assoc other values into it if you want to modify it.
I hope that answers your question.

1 Like

Sorry bsless, did you omit the & intentionally to indicate that you’d rather make the assoc list a required argument. I just want to understand your intent.

I omitted it but it doesn’t have to be a required argument, you can always write something like:

(defn configure
  ([val]
   (configure val {:debug true}))
  ([val {:keys [debug verbose]
         :or {debug val, verbose debug}
         :as opts}]
   (configure (inc val) opts)))

if you want to enforce default opts for variadic args. I don’t know if CL supports this.
I mainly omitted it because it makes the recursion annoying because you’ll have to write all of the arguments explicitly, even if you don’t change them, while if it’s a map you can just pass it unchanged.

The following recursive call works:

clojure-rte.core> (defn configure
                    [val & {:keys [debug verbose]
                          :or {debug val, verbose debug}
                          :as opts}]
                    (if (> val 0)
                      (cons (list val debug verbose)
                            (apply configure (- val 1) opts))
                      nil))
#'clojure-rte.core/configure
clojure-rte.core> (configure 3)
((3 3 3) (2 2 2) (1 1 1))
clojure-rte.core> 

But not if I try to add a keyword override at the recursive call site.

clojure-rte.core> (defn configure
                    [val & {:keys [debug verbose]
                          :or {debug val, verbose debug}
                          :as opts}]
                    (if (> val 0)
                      (cons (list val debug verbose)
                            (apply configure (- val 1) :debug (+ val 4) opts))
                      nil))
#'clojure-rte.core/configure
clojure-rte.core> (configure 3)

It thinks there is no value given for :debug, which is strange. I get the following error:

  Show: Project-Only All 
  Hide: Clojure Java REPL Tooling Duplicates  (11 frames hidden)

1. Unhandled java.lang.IllegalArgumentException
   No value supplied for key: [:debug 7]

    PersistentHashMap.java:   77  clojure.lang.PersistentHashMap/create
                      REPL:  842  clojure-rte.core/configure
                      REPL:  842  clojure-rte.core/configure
               RestFn.java:  139  clojure.lang.RestFn/applyTo
                  core.clj:  671  clojure.core/apply
                  core.clj:  660  clojure.core/apply
                      REPL:  848  clojure-rte.core/configure
                      REPL:  842  clojure-rte.core/configure
               RestFn.java:  139  clojure.lang.RestFn/applyTo
                  core.clj:  671  clojure.core/apply
                  core.clj:  660  clojure.core/apply
                      REPL:  848  clojure-rte.core/configure
                      REPL:  842  clojure-rte.core/configure
               RestFn.java:  410  clojure.lang.RestFn/invoke
                      REPL:  854  clojure-rte.core/eval9116
                      REPL:  854  clojure-rte.core/eval9116
             Compiler.java: 7176  clojure.lang.Compiler/eval
             Compiler.java: 7131  clojure.lang.Compiler/eval
                  core.clj: 3214  clojure.core/eval
                  core.clj: 3210  clojure.core/eval
                  main.clj:  414  clojure.main/repl/read-eval-print/fn
                  main.clj:  414  clojure.main/repl/read-eval-print
                  main.clj:  435  clojure.main/repl/fn
                  main.clj:  435  clojure.main/repl
                  main.clj:  345  clojure.main/repl
               RestFn.java: 1523  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   79  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:   55  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:  142  nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
                  AFn.java:   22  clojure.lang.AFn/run
               session.clj:  171  nrepl.middleware.session/session-exec/main-loop/fn
               session.clj:  170  nrepl.middleware.session/session-exec/main-loop
                  AFn.java:   22  clojure.lang.AFn/run
               Thread.java:  832  java.lang.Thread/run

Turns out you don’t have to write the arguments explicitly if you use apply. However, not sure how to override values…

apply is more to unpack arguments which come back in a sequence but should be passed to a function, as in

(f a1 a2 a3) == (apply f [a1 a2 a3])

Forgive me, I know I’m thinking in Common Lisp while learning clojure.

Isn’t that exactly what the recursive call is with opts?

(configure 3 :debug 4 :verbose 5) == (apply configure 3 opts)

bsless, the reason I’m asking in the first place is because I actually did implement the function in my application as two required arguments, the second of which is as assoc list. But I’m translating this from a CL application which uses &rest and &key arguments.

My application seems to work, but it occurs to me that my manipulation of the assoc list when I call the function seems like I’m doing with that the language could be doing for me.

The reason can be seen here:

user=> (defn configure
                    [val & {:keys [debug verbose]
                          :or {debug val, verbose debug}
                          :as opts}]
(println opts))
#'user/configure
user=> (configure 1 :debug 2 :verbose 3)
{:verbose 3, :debug 2}
nil
user=> (apply println {:verbose 3 :debug 2})
[:verbose 3] [:debug 2]
nil

opts is a hash map in configure but when you use apply, seq is applied to the hash map which produces a sequence of pairs (of key/value) which is not the same as passing those arguments in the first place.

This is why this style of function is discouraged except for strictly “user-level” where a human types the function calls directly. It’s why passing arguments as a single hash map is generally preferred, even for “user-level” code – i.e., without the & – and then you can simply call the function recursively passing the hash map directly (without apply).

If you really still want to use the & form, you can invoke the recursive call with apply if you flatten out the hash map:

user=> (apply println (into [] cat {:verbose 3 :debug 2}))
:verbose 3 :debug 2

into [] cat turns a hash map into a flat sequence of keys and values.

3 Likes

Just a warning, it is not safe to do :or {debug val, verbose debug} as you’re doing here. Maps are inherently unordered and there is no guarantee provided that debug will be bound before verbose's value is evaluated. If you need to do this, you should provide an inner let that establishes debug and verbose that can rely on let's ordering guarantees.

You may also want to be a little suspicious about having the :or rely on val being bound already, although that is definitely true of the current implementation, and unlikely that that assumption would be violated in the future.

In general, I think it’s safest to make :or defaults only constant values, and leave anything trickier for a let that can ensure the ordering of evaluation.

3 Likes