Fork me on GitHub
#beginners
<
2020-11-11
>
Jim Newton13:11:44

isn't there a funcall function in clojure? I know it's not absolutely necessary, but sometimes it can make the code more readable. For example, this call to reduce returns a function that needs to be called with no arguments.

(reduce   (fn
            ([]
             default-f)
            ([_ [keys f]]
             (if (exists [x keys] (test value x))
               (reduced f)
               default-f)))
          default-f
          pairs)
It works if I just wrap this in a second set of parens.
((reduce  (fn
            ([]
             default-f)
            ([_ [keys f]]
             (if (exists [x keys] (test value x))
               (reduced f)
               default-f)))
          default-f
          pairs))
But I think it is more readable if I use funcall to emphasize what's happening.
(funcall (reduce (fn
                     ([]
                      default-f)
                     ([_ [keys f]]
                      (if (exists [x keys] (test value x))
                        (reduced f)
                        default-f)))
                   default-f
                   pairs))

delaguardo13:11:09

try .invoke

(.invoke (reduce
            (fn [f k]
              (fn []
                (str (f) (k))))
            [(fn [] "q") (fn [] "w") (fn [] "e")]))

✔️ 6
andy.fingerhut14:11:33

Another way to emphasize this is to use let to bind the return value of the (reduce ...) expression to some name, then in the body of the letcall that function, e.g.:

(let [f (reduce …)]
  (f))

Jim Newton14:11:04

@U0CMVHBL2 yes that's a good idea as well. I ended up just implementing a funcall function in my local utils library. Then after that I ended up removing the code altogehter, I think it is a bit clearer using loop/recur,

(defn -casep-helper [test value default-f & pairs]
  (loop [pairs pairs]
    (cond (empty? pairs)
          (default-f)

          :else
          (let [[keys f] (first pairs)]
            (if (exists [x keys] (test value x))
              (f)
              (recur (rest pairs)))))))

Jim Newton14:11:58

@U0CMVHBL2, yes as i mentioned earlier, you're right. I can just wrap an extra set of parens around it and it works perfectly well. I just find that the explicit funcall makes it easier to understand what's happening.

Jim Newton14:11:34

I originally had an error in this function and it made an infinite loop. Can you spot the error?

(defn -casep-helper [test value default-f & pairs]
  (loop [pair pairs]
    (cond (empty? pairs)
          (default-f)
          :else
          (let [[keys f] (first pairs)]
            (if (exists [x keys] (test value x))
              (f)
              (recur (rest pairs)))))))
The error was what motivated me to try to rewrite it using reduce/`reduced`.

andy.fingerhut14:11:21

It is not obvious to me from a minute of reading where that function would cause an infinite loop, unless the infinite loop is in default-f or f

Daniel Stephens21:11:27

you're using pairs inside the loop, whereas you probably should be using the (misnamed since it's still a seq) pair I think.

(loop [[pair & more] pairs]
  (if pair
    (do ... (recur more))
    ...))
So an infinite loop happens because recuring with (rest pairs) presumably always has some values left, since it's what you passed into the function, not the loop

Jim Newton09:11:49

The infinite loop is that I meant to use (loop [pairs pairs] ...) but instead i used (loop [pair pairs] ...) ... That's when I said to myself, what I really want to do is iterate over a sequence until some condition, which is exactly what reduce/`reduced` is for.

Jim Newton09:11:54

but the conversion to reduce didn't aid in readability, in my opinion for two reasons. 1. in order to prevent the default-f function from being called unnecessarily was tricky, and 2. handling the empty sequence, reduce calls the given function with 0 arguments in case of an empty sequence, so I had to provide a multiple-arity function, which further obfuscates the code. In the end, I think that loop/`recur` is probably clearer.

Jim Newton09:11:12

w.r.t. the following suggestion

(loop [[pair & more] pairs]
  (if pair
    (do ... (recur more))
    ...))
I really don't like that coding style, even though I know it is very often used in the clojure community. One reason I don't like it is that it works accidentally. The code would fail if there is every a pair such as (false not-false) in the list of pairs . I avoid this accidental dependence in my code when I can help it, even if that makes for someone non-ideomatic clojure coding style.

Daniel Stephens10:11:52

(loop [[pair & more] [ [false true] ]]
  (if pair
    (do (println pair) (recur more))
    nil))
[false true]
=> nil
A pair of false and true is still truthy. Admittedly the code doesn't hit everything if your list of pairs can contain nils. Either way I would prefer reduce to loop when it's feasible, I was just trying to help point out where the infinite loop came in and what the form of loop bindings is 🤷

Daniel Stephens10:11:09

Maybe this would work for you

Daniel Stephens10:11:12

(defn -casep-helper [test value default-f & pairs]
  @(reduce
     (fn [agg [keys f]]
       (if (exists [x keys] (test value x))
         (reduced (delay (f)))
         agg))
     (delay (default-f))
     pairs))

Jim Newton11:11:09

about the @, that's an interesting thought to avoid having to do a funcall.

Jim Newton11:11:06

@ULNRSUK8C, you're right about a pair of [false non-false] still being truthy. I guess I was thinking about adding the pair destructuring also. such as

(loop [[[key value] & pairs] pairs]
   (if key ...
I could try something like this:
(loop [[[key value :as pair] & pairs] pairs]
   (if pair ...
But I must admit that bothers me because for me, it still works accidentally. OTOH, i'm, not completely comfortable using :as in because it works sometimes but not others, and I don't remember the cases where it's legal. For example, I know the following is illegal.
(defn foo [a & as :as all]
   ...)
and this is illegal as well
(defmacro foo [a & as :as all]
   ...)

Daniel Stephens12:11:07

(defn foo [& [a & as :as all]]
  (println a)
  (println as)
  (println all))
=> #'user/foo
(foo 1 2 3 4)
1
(2 3 4)
(1 2 3 4)
=> nil
you have to have :as inside a destructure form I believe, so this works

Daniel Stephens12:11:53

otherwise :as couldn't know whether to refer to a and as or just as or nothing

Daniel Stephens12:11:55

I'm not completely sure what you mean when you say it works accidentally, if you can't rely on pairs being a list of pairs then I'd suggest either the call point is wrong or you need another function that does validation first.

Jim Newton13:11:27

I guess the following isn't so bad.

(apply   (reduce (fn
                     ([]
                      default-f)
                     ([_ [keys f]]
                      (if (exists [x keys] (test value x))
                        (reduced f)
                        default-f)))
                   default-f
                   pairs)
            ())

noisesmith19:11:48

reduce won't use the 0 arg arity of that function - supplying default-f as the init arg already accomplishes the right thing

3
noisesmith19:11:12

and you can eliminate both the "apply" and the "()" - just the parens suffice

andy.fingerhut14:11:13

Clojure is a Lisp-1, not a Lisp-2 like Common Lisp, so you can use functions in call position, or even any other expression that, when evaluated, returns a function, e.g.

user=> ((constantly true) 5)
true

andy.fingerhut14:11:45

In this, Clojure is more like Scheme than Common Lisp. I don't recall whether Scheme has funcall, but I would guess no.

Nassin16:11:45

or like Javascript 😉

Mark Wardle22:11:38

Hi. I have a clojure test (deftest) running fine at the REPL, but that fails when I run using deps.edn (clj -M:test) with cognitect test runner, because “class not found”. Fixed if I manually compile AOT before running the test. So… is it possible to automate AOT for tests without resorting to a Makefile? Any pointers gratefully received! Thank you

seancorfield23:11:06

@mark354 Yes, you can add "-e" "(compile,'the.namespace)" into your :main-opts for running the tests, ahead of the "-m" option for the test runner. See https://github.com/seancorfield/cfml-interop/blob/develop/deps.edn#L11-L13 for an example of this.

Mark Wardle07:11:34

Thanks! It works. Much appreciated.