beginners

Doug Harvey 2025-06-20T03:13:31.262649Z

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 keywords

2025-06-20T17:58:00.034249Z

symbols also work this way

user=> ('get '{get foo})
foo

2025-06-20T18:00:41.565229Z

user=> ('get (reify clojure.lang.ILookup (valAt [& args] :ok)))
:ok 

gaverhae 2025-06-23T12:17:53.219129Z

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.

michaelwhitford 2025-06-20T03:19:17.348139Z

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.

2025-06-20T03:31:51.420879Z

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?

exitsandman 2025-06-20T03:38:38.425949Z

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).

Nim Sadeh 2025-06-20T04:20:49.046609Z

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 check

2025-06-20T18:23:03.842859Z

is something swallowing exceptions? eg. an over eager try/catch without logging?

2025-06-20T18:23:29.757799Z

eg. are you using futures without calling deref on the return value?

Nim Sadeh 2025-06-20T18:23:52.972469Z

no my code is all sync

Nim Sadeh 2025-06-20T18:24:04.481189Z

no try-catch

Nim Sadeh 2025-06-20T18:24:32.285109Z

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

Nim Sadeh 2025-06-20T18:24:40.768339Z

Specifically for threads exiting fast

gaverhae 2025-06-23T12:29:37.570059Z

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.

➕ 1
2025-06-23T16:10:55.815459Z

@gaverhae yeah, good catch

user=> (if-let [{:keys [foo]} {}] (println foo))
nil
nil
very likely the cause of the bug here

Nim Sadeh 2025-06-23T16:15:07.066119Z

fetch-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

Nim Sadeh 2025-06-23T16:15:45.137099Z

@noisesmith in your example, foo is uninitialized, but in mine it's initialized I think? Also it seems like your print statement runs?

2025-06-23T16:16:47.757619Z

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

2025-06-23T16:17:26.755949Z

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

2025-06-23T16:19:26.296009Z

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

2025-06-23T16:22:13.308999Z

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

Nim Sadeh 2025-06-23T16:22:36.079849Z

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.

2025-06-23T16:27:48.577699Z

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

Nim Sadeh 2025-06-20T04:23:53.535949Z

Note that users/fetch-user-by-contact-id does not short circuit on id and will still run even if id is nil.

2025-06-20T04:27:32.194369Z

Chatgpt is nonsense

➕ 4
2025-06-20T04:28:23.219749Z

You haven't talked about how this end point is being called, but it is almost certainly a race condition

Nim Sadeh 2025-06-20T04:28:57.842139Z

This function is called by an async job queued with Goose using perform-async

2025-06-20T04:30:11.532409Z

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

2025-06-20T04:30:25.759859Z

Or contact or whatever

Nim Sadeh 2025-06-20T04:30:44.791319Z

No, it's not a database race condition

Nim Sadeh 2025-06-20T04:30:51.185249Z

The users in both cases were added weeks ago

Nim Sadeh 2025-06-20T04:31:17.360399Z

And they had like 10 successful texts, one unsuccessful, then 10 successful (approx. numbers to illustrate)

2025-06-20T04:33:08.971269Z

If you say so, but it sure sounds like some kind of race. Chatgpt's suggestion is just nonsense for sure though

Nim Sadeh 2025-06-20T04:34:02.078489Z

I figured as much but included it in case it would be helpful

2025-06-20T04:34:26.654969Z

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

Nim Sadeh 2025-06-20T04:37:19.723409Z

I confirmed that there are no writes to the user records around this time - for this or any other user

Nim Sadeh 2025-06-20T04:38:20.849799Z

> 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

Nim Sadeh 2025-06-20T04:41:11.624319Z

also fwiw ChatGPT also knows it's nonsense

🤡 1
Nim Sadeh 2025-06-20T04:46:42.179269Z

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.

Nim Sadeh 2025-06-20T04:53:10.385259Z

Another piece of the puzzle: send-message-to-contact logs something every time it's called - except in these flake cases