beginners

2025-12-14T15:56:19.342649Z

Something I still struggle with (and I imagine others are/have been in the same boat) when trying to write Clojure code is how to deal with scenarios that imperative languages handle with a whole bunch of alternating do/check statements (see in thread for example). When I look at code like that in other languages, I'm at a loss for what the "Clojure way" of writing the same would be. Of course, I could just adapt one-to-one with nested if's and let's, but that feels pretty dirty

2025-12-14T15:56:56.962879Z

Here's an example (in python) of what I mean:

@get("/oauth/authorize")
async def authorize(
    request: Request,
    response_type: str,
    client_id: str,
    redirect_uri: str,
    scope: Optional[str] = None,
    state: Optional[str] = None,
    nonce: Optional[str] = None,
    code_challenge: Optional[str] = None,
    code_challenge_method: Optional[str] = None,
) -> Response:
    """OAuth authorization endpoint."""
    auth_request = AuthorizationRequest(
        response_type=response_type,
        client_id=client_id,
        redirect_uri=redirect_uri,
        scope=scope,
        state=state,
        nonce=nonce,
        code_challenge=code_challenge,
        code_challenge_method=code_challenge_method
    )
    
    # Validate authorization request
    valid, error = oauth_service.validate_authorization_request(auth_request)
    
    if not valid:
        error_params = {
            "error": error,
            "error_description": f"Authorization request validation failed: {error}"
        }
        if state:
            error_params["state"] = state
        
        error_url = f"{redirect_uri}?{urlencode(error_params)}"
        return Redirect(path=error_url, status_code=HTTP_302_FOUND)
    
    # Check if user is authenticated (simplified - in production use sessions)
    user_id = request.session.get("user_id") if hasattr(request, "session") else None
    
    if not user_id:
        # Redirect to login page with return URL
        login_params = {
            "return_to": request.url.path + "?" + str(request.url.query)
        }
        return Redirect(
            path=f"/login?{urlencode(login_params)}",
            status_code=HTTP_302_FOUND
        )
    
    # Create authorization code
    code = oauth_service.create_authorization_code(auth_request, user_id)
    
    # Build redirect URL
    response_params = {"code": code}
    if state:
        response_params["state"] = state
    
    redirect_url = f"{redirect_uri}?{urlencode(response_params)}"
    return Redirect(path=redirect_url, status_code=HTTP_302_FOUND)

2025-12-14T15:58:51.411659Z

You can see the pattern: do a thing, check the result. If invalid, return early, else do the next thing, etc. I feel like there's a cond-> or something in here somewhere, but I'm at a loss for how that would look concretely. I just don't have enough intuition built up to reframe the problem

p-himik 2025-12-14T16:01:47.481799Z

I wouldn't think twice and just use exceptions. That is, in this specific case. In other cases, I might use something else.

exitsandman 2025-12-14T16:39:05.626859Z

IMHO here it's very feasible to just do the simple thing

(let [auth-req ...]
  (if-let [err (oauth/validate auth-req)]
    
    (let [{:keys [user-id]} (:session request)]
      (if-not user-id
        
        ))))

👍 1
2025-12-14T17:23:59.445519Z

in situations like this, if it happens more than once, i reach for fmnoise/flow

p-himik 2025-12-14T17:26:42.433899Z

Which to me looks awfully similar to just throwing an exception. :D I guess I'm "too old" for sugar. :)

2025-12-14T17:35:01.507199Z

oh it is, just makes it a little easier to read for certain threading contexts

p-himik 2025-12-14T17:48:53.310569Z

My reasoning, in case anyone cares. Whenever I'm deciding whether to use something, the everpresent questions in my head are: • How debuggable is it? (So, so many good-looking libraries become immediate no-goes just because of this point.) • Is there any "magic" to it, anything implicit? • How much code is there? (Somewhat of a proxy for the above two things.) • Does it bring any other liabilities? Transitive dependencies being the obvious one, but also extra requirements of any nature, zero documentation, long-standing open issues, etc. So here, on the one hand I have "a little easier to read" and on the other I have: • A bit harder to debug • 200 loc of Clojure, 40 loc of Java • A Java-using library with just a project.clj in its root - no reasonable way to use it via Git deps • > creating/allocating resources which require manual state management as flet bindings is not good idea • Other people reading my code might not be familiar with it • At a glance, looks like something that's easy to overuse - exceptions are for exceptional paths, not for generic control flow

🙏 2
2025-12-14T18:23:27.779129Z

Insightful. Thanks all

2025-12-14T18:26:37.201849Z

none of that bothers me

2025-12-14T18:28:47.932619Z

i have found a half baked version of it in the 3 clojure job code bases i've worked on, sometimes multiple times lol

2025-12-14T18:32:13.389659Z

early exit control flow is useful, and an existing relatively small, complete solution allows for consistency without having to enforce adhoc conventions through code reviews or refactoring

exitsandman 2025-12-14T20:05:36.863179Z

one thing I want to note here is that imho unless-let defined as

(defmacro unless-let [[bind test] then else]
  (let [test# ~test]
    (if-not test#
      ~then
      (let [~bind test#]
        ~else))))
is very helpful (E: for how lightweight it is), because very often the else branch of an if-let is a less interesting catch-all case that is helpful to get out of the way first.

Mario G 2025-12-14T20:54:54.689699Z

Short answer is: there's no specific Clojure way here and it's fine. I agree with and I'd be OK with many of the solutions proposed, it's not that there must be only one way. Usually the local code base or team practices would lead to an approach or another. E.g. throw VS return a map that says KO inside (this would be my favourite BTW) VS any library that does a job. Most likely, it would be some sort of error handling, often let it throw and then something above would translate the rough error to an appropriate response for the specific situation. Which is an approach used also in other tech stacks. If the chain of things to check is too long (less trivial than first example by exitsandman above) usually cutting the chain to its own function makes sense and could make sense also in the Python example above or any other language. if authentication-KO then get-out else authorise and authorise does its own check. I'd probably do something like this in most languages, so you have smaller functions and not a single slab with if-s, you can test authorise on its own, etc.

☝️ 1
hrtmt brng 2025-12-15T05:56:03.410729Z

I use the following (examples on https://github.com/habruening/docnudb/blob/main/src/docnudb/backend/db.clj):

(defmacro let [[b v & more-bindings] & body]
  (cond (nil? b)
        `(do ~@body)
        (= b :escape)
        `(or ~v
             (let ~more-bindings ~@body))
        :else
        `(clojure.core/let [~b ~v]
           (let ~more-bindings ~@body))))
                      
(let [x (determine-x)
        :escape (when (something-wrong-with x) "early return, because x is wrong")
        y (determine-y x)
        :escape (when (something-wrong-with y) "early return, because y is wron")
        z (determine-z y)]
    (do-something-with z))