Looking at the implementation of postwalk-replace in clojure.walk made me wonder why the author didn't use #(smap % %) or #(get smap % %) instead of (fn [x] (if (contains? smap x) (smap x) x))
Probably no good reason. It's easy to write such code when you're in a kind of flow.
Also nil and false values can get tricky with this style. Wouldnāt affect this version but sometimes easier to code it this way so itās not a factor
Iām in the middle of refactoring some code into subdirectories. and after the refactoring Iām getting a java NullPointerException which I donāt understand, and I get no stacktrace to help, Unfortunately the code involves the spec API which I donāt really understand.
1. is it normal that we get no stacktrace when we get a NullPointerException?
2. whatās some advise of figuring out whatās happening?
Iāve put a bunch of print statements into my code and iāve almost poinpointed the place where it is happening. And I suspect it is happening in a local function which clojure has given a cryptic name to. although iām not 100% sure.
#function[genus.genus-spec/spec-to-rte/fn--18016]
Because, indeed my function spec-to-rte declares several local functions using letfn but I donāt know which one is fn-18016 ..
why doesnt the name mangling use the name given in letfn to create the name of the function? Or perhaps it does and my problem is elsewhere?
finally found my problem, just by looking deeper and deeper. maybe thereās no easy way. I just had to do the work.
> is it normal that we get no stacktrace when we get a NullPointerException?
Kinda. Most likely, that's just due to how the JVM is configured to work by default. You can change it by providing a -XX:-OmitStackTraceInFastThrow flag to java. So prepend -J to it if you use clj.
> whatās some advise of figuring out whatās happening?
Refactoring piece by piece, where each piece is as small as reasonably possible, with checks in between.
> Because, indeed my function spec-to-rte declares several local functions using letfn but I donāt know which one is fn-18016 ..
Functions created with letfn have a name, and that name will be in their string representation.
fn--... tells us that it's not one of those functions at the top level of letfn but rather some anonymous function - look for (fn [...] ...) or #(...).
> Functions created with letfn have a name, and that name will be in their string representation.
thatās good news.
ahh that makes sense. I found where Iām creating some other anonymous functions ⦠perhaps I should try to name these in the macro expansion.
(defmacro casep
"Like case, but uses the given test function rather than = to test
whether the given object corresponds to any of the given choices."
[test obj & pairs]
(loop [pairs pairs
default (fn [] nil)
acc ()]
(cond (empty? pairs)
`(-casep-helper ~test ~obj ~default ~@(reverse acc))
(empty? (rest pairs))
(recur ()
`(fn [] ~(first pairs))
acc)
:else
(let [key (first pairs)
value (second pairs)]
(recur (rest (rest pairs))
default
(cons `['~key (fn [] ~value)] acc)
)))))Yeah, and fortunately the (fn some-name [...] ...) form is possible. :)
found my issue. Iāve changed several of my name space names (because of directory refactoring). genus-spec has become genus.genus-spec . Somewhere Iāve hard coded the name space name in a call to (find-ns 'genus-spec) which of course now returns nil
Lo-and-behold, when i pass nil as first argument of ns-resolve we get this NullPointerException.
BTW, iām always a bit confused about clojure macros. Do I need to quote the (fn [] nil) which is the initial value of default in the loop? because it will be interpolated into the back-tick later ???
otherwise iām imbedding a live function object into a macro expansion.
That's correct, it needs to be syntax-quoted.
Same how you already do it in (recur () (fn [] ~(first pairs)) acc)`.
Macros must produce a data structure that's a valid Clojure form.
Quotes and syntax quotes are ways to create suitable data structures.
But you could make do without them at the (...) level at all, by constructing lists manually: (list fn [] nil)`.
Going further, even fn` can be replaced with (symbol "clojure.core" "fn"). Not that anyone should want to write or read stuff like that when quotes and syntax quotes exist, but it might help with grokking these things.
> Not that anyone should want to write or read stuff like that when quotes and syntax quotes exist
Just to expand on this a bit - I meant specifically the example code in my message above, not in general.
Sometimes you do want to use list and/or symbol functions explicitly, especially in more complex macros.
I also find myself using template instead of back-tick because it gives more flexibility,
> Yeah, and fortunately the (fn some-name [...] ...) form is possible. :)
This reminds me of my gripe that clojure lacks homoiconicity.
If I want to add the name in the fn form, the following fails.
(defmacro casep
"Like case, but uses the given test function rather than = to test
whether the given object corresponds to any of the given choices."
[test obj & pairs]
(loop [pairs pairs
default `(fn casep-nil-default [] nil) ;; TODO name this fn
acc ()]
(cond (empty? pairs)
`(-casep-helper ~test ~obj ~default ~@(reverse acc))
(empty? (rest pairs))
(recur ()
`(fn casep-specified-default [] ~(first pairs)) ;; TODO name this fn
acc)
:else
(let [key (first pairs)
value (second pairs)]
(recur (rest (rest pairs))
default
(cons `['~key (fn [] ~value)] acc) ;; TODO name this fn
)))))
because backquote thinks casep-specified-default and casep-nil-default are vars. Instead I have to use ~'casep-specified-default etc.
(defmacro casep
"Like case, but uses the given test function rather than = to test
whether the given object corresponds to any of the given choices."
[test obj & pairs]
(loop [pairs pairs
default `(fn ~'casep-nil-default [] nil) ;; TODO name this fn
acc ()]
(cond (empty? pairs)
`(-casep-helper ~test ~obj ~default ~@(reverse acc))
(empty? (rest pairs))
(recur ()
`(fn ~'casep-specified-default [] ~(first pairs)) ;; TODO name this fn
acc)
:else
(let [key (first pairs)
value (second pairs)]
(recur (rest (rest pairs))
default
(cons `['~key (fn [] ~value)] acc) ;; TODO name this fn
)))))How is it related to homoiconicity though?
> A language is homoiconic if a program written in it can be manipulated as data using the language.
That's still perfectly the case here. It's just that the behavior of syntax quotes requires you to circumvent it in some specific cases.
> because backquote thinks casep-specified-default and casep-nil-default are vars.
No, syntax quotes have no idea and no care about vars. And macros don't care about vars as well. Vars come into picture much later.
The only thing that's important here is symbols. A syntax quote adds namespaces to symbols, that's it, as documented here: https://clojure.org/reference/reader#syntax-quote
For the syntax quote to behave differently, it would have to either be omniscient or become significantly less general.
Clojure golf question:
I want to update-in a field if and only if:
a. The path does not already have some value in it
b. Whatever value I generate does have some value
So far I have:
(defn get-program [etype field]
:the-program ;; can be null
)
(let [m {} ;; some map
etype "users" ;; some k
field "email" ;; some v
]
(update-in m [:etype->programs etype :fields]
(fn [field-programs]
(if (contains? field-programs field)
field-programs
(if-some [p (get-program etype field)]
(assoc field-programs field p)
field-programs
)))))
Would you write this differently?I'm not sure this code is doing what you want it to do, so before answering, I have a question : are you sure about this (contains? field-programs etype) ? Isn't it to be field?
To me it reads as if you want to assoc in field if and only if that field is not yet set in field-program. Am I wrong?
Sorry about that, updated!
I want to say something along the lines of:
assoc p in field if and only if:
⢠field is not yet set
⢠and p is not nil
use fnil as the function to update?
fnil would still assoc if p is nil
Also wouldn't work, you'd have to use or. Like if associating nil wasn't a problem, you could just:
(update-in m [:etype->programs etype :fields field] #(or % (get-program etype field)))It sounds like you want default values. Have you considered merge?
I would perhaps just do the naive thing of using get-in to see if the value is there and if not, execute the function and only then use assoc-in to set the new value, to prevent creating too much garbage by updating for nothing
Smthing like this? You do make one unecessary op if field exists.
I would use a not-found sentinel with get-in to check for nil or false fields, unless that's not what stopa is expecting in those fields
That could work! Thanks team
I like to collapse nested conditions when they share the same fallback values like this:
(let [m {} ;; some map
etype "users" ;; some k
field "email" ;; some v
]
(update-in m [:etype->programs etype :fields]
#(or (when-not (contains? % field)
(when-some [p (get-program etype field)]
(assoc % field p)))
%)))