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
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)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
I wouldn't think twice and just use exceptions. That is, in this specific case. In other cases, I might use something else.
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
)))) in situations like this, if it happens more than once, i reach for fmnoise/flow
Which to me looks awfully similar to just throwing an exception. :D I guess I'm "too old" for sugar. :)
oh it is, just makes it a little easier to read for certain threading contexts
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
Insightful. Thanks all
none of that bothers me
i have found a half baked version of it in the 3 clojure job code bases i've worked on, sometimes multiple times lol
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
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.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.
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))