why does this work:
(:name {:name "doug"})
"doug"
but this doesn't?
("name" {"name" "doug"})
Execution error (ClassCastException) at user/eval38 (REPL:1).
class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
but if I reverse the arguments either works:
({"name" "doug"} "name")
"doug"
user=> ({:name "doug"} :name)
"doug"
I know the first one is a keyword as the key instead of a string, but I was surprised that the first style only works with keywordssymbols also work this way
user=> ('get '{get foo})
foouser=> ('get (reify clojure.lang.ILookup (valAt [& args] :ok)))
:ok The most direct answer is that keywords and maps implement IFn, and strings don't. I'd recommend using keywords-as-functions only when using a literal keyword, and using an explicit get in all other cases, because nil does not implement IFn.
A keyword and a map can both be used as a function to lookup a value by key. A string does not act as a function in the same way. When dealing with string keys use get, or the map as the function call for the lookup, as you saw when you reversed them, the map was the function, so it looked up the key and gave you the value.
There's kind of two reasons: 1. You can't actually make a String a function, because Strings are directly Java strings, and so Clojure cannot add the IFn interface to them needed to allow them to be used as a function. 2. You could debate, what would be the best function for a string? Is it really key lookup? Why not say string concatenation?
fwiw my two cents in terms of usage: keyword literals as functions are a big plus and I'd use them every time. Colls (vectors and sets are also callable in the same fashion) as functions are nice when passing a known-type instance into a HOF and using set literals as predicates, but I generally would not replace gets with calls to collections, especially since there are differences (`get` handles ILookup instances, nil and Java maps which aren't callable).
I've been debugging the following issue below. ChatGPT after a long conversation thinks it's a shadowing colliding with GC issue, I'm wondering if that makes sense. I have this function answer-text that responds to a text message from a webhook:
(defn answer-text
[producer database {{:keys [id] :as contact} :contact
:keys [message]}]
(if-let [{:keys [id] :as user} (users/fetch-user-by-contact-id database id)]
;; handle message logic
...
;; else
(send-message-to-contact
"Sorry, but we could not find your user profile. Are you sure you're a registered user?"
contact)))
The issue: a couple times (2 out of hundreds of invocations today) now I have see the else clause fire when it shouldn't in a flaky manner (the next request from the same contact succeeds). All my SQL queries, including users/fetch-user-by-contact-id, have logging, but in the requests that fail, the logging for this SQL statement doesn't appear, suggesting the function wasn't run. I confirmed that the log still prints even if the id parameter is null. This suggests that the function producing the value in the if-let statement was evaluated to as falsy before it even ran.
I have scanned my codebase for other possible invocations of send-message-to-contact, but they are both in this function (the other in the truthy part of the if statement and involves an LLM call). It's a fact that the else clause fired as the texts containing this exact string were sent in both cases.
I've deployed code to test this theory by not destructuring user, but since the error is flaky it will take days before I can confidently say that it fixed the issue, so I'm hoping to get a sanity checkis something swallowing exceptions? eg. an over eager try/catch without logging?
eg. are you using futures without calling deref on the return value?
no my code is all sync
no try-catch
I suspect that if the worker thread in goose exits too fast then the logs (mulog) don't have time to flush, which is a recorded issue on the GH for Mulog
Specifically for threads exiting fast
It's really difficult to guess without the code of users/fetch-user-by-contact-id. Is it possible that you have the wrong expectations about if-let? When used with destructuring like that, it's easy to expect it to short-circuit on the variables you define, but it in fact short-circuits on the overall result of the right-hand-side.
@gaverhae yeah, good catch
user=> (if-let [{:keys [foo]} {}] (println foo))
nil
nil
very likely the cause of the bug herefetch-user-by-contact-id doesn't short circuit
(defn fetch-user-by-contact-id
[db contact-id]
(sql/get-by-id (db)
:users
contact-id
:contact_id
{}))
where the sql function is from next.jdbc@noisesmith in your example, foo is uninitialized, but in mine it's initialized I think? Also it seems like your print statement runs?
yes, the point here is that id can be nil, and the fetch-user-by-contact-id would run and ask for the nil id
and no, your case will be just like this one, id will have the value of id in the map (potentially nil or false) and the block will run regardless
that isn't guaranteed to be the cause of the specific bug you are asking about, but if there's any way the map doesn't contain :id it's still a bug
also, there's no "uninitialized" value in my example, I don't think there's such thing as uninitialized values in clojure. the value of :id in my map is nil, since the map is non-nil the code runs and the nil value is used
But one of the mysteries is that I didn't see logs for the sql execution in the service logs. All my SQL statements are logged and this is no exception, it logs when it succeeds. If I found a log showing that it tried to execute with id nil or false, I would have easily found the cause to be a missing ID and I could contact my SMS vendor. Unfortunately I don't have great access to my managed SQL instance to see logs from far enough in the past to really confirm whether it ran or not. I'm currently operating with the theory that it did run with nil value, but that the log didn't print out because the thread was too short lived. I added a sleep to it but haven't seen the issue replicate yet.
it could be that the :keys inside if-let isn't your real issue, but it's a common problem.
another potential issue is the shadowing of id - you might want to disambiguate by using contact-id and/or user-id
Note that users/fetch-user-by-contact-id does not short circuit on id and will still run even if id is nil.
Chatgpt is nonsense
You haven't talked about how this end point is being called, but it is almost certainly a race condition
This function is called by an async job queued with Goose using perform-async
So something like whatever adds the user to the database is in the middle of running and hasn't completed yet when the first execution happens and has completed by the second
Or contact or whatever
No, it's not a database race condition
The users in both cases were added weeks ago
And they had like 10 successful texts, one unsuccessful, then 10 successful (approx. numbers to illustrate)
If you say so, but it sure sounds like some kind of race. Chatgpt's suggestion is just nonsense for sure though
I figured as much but included it in case it would be helpful
Could be the webhook request getting dropped mid deploy, or failing to get from any proxy you have in front of your service to the service, etc
I confirmed that there are no writes to the user records around this time - for this or any other user
> Could be the webhook request getting dropped mid deploy, or failing to get from any proxy you have in front of your service to the service, etc
That could happen, but the webhook is being executed. It's running the else statement, but not executing the fetch-user-by-contact-id function required to evaluate the if-let
also fwiw ChatGPT also knows it's nonsense
Also, a race would not have prevented the execution of the fetch function, it would have just returned unexpected results which I would have seen and debugged. But the absence of a log on the fetch function - only on these requests that else - suggests that it's not a database race condition.
Another piece of the puzzle:
send-message-to-contact logs something every time it's called - except in these flake cases