I'm using the RaiseDSL via context receivers. I'm ...
# arrow
d
I'm using the RaiseDSL via context receivers. I'm initiating the Raise scope via a top-level function
withApiError
which I call in my controller methods. Besides setting up
Raise<ApiError>
context, I'm also setting other common contexts. E.g. I inject an
Account
via
withAccount
function. This function can naturally raise an error (e.g. if the account does not exist), therefore it itself needs the
Raise<ApiError>
context. So my typical controller method looks like
Copy code
withApiError {
  withAccount {
    // do some stuff which needs an account and can raise an error
  }
}
So far so good. The catch (I would say catch 22 🙂) is that in the
withApiError
function I would like to log some stuff which itself needs some account info. Obviouly, this can be used only if the account was successfully fetched. My question is how to best model this. Obviously I can do something like
Copy code
withApiError { // this is just for the account
  withAccount {
    withApiErrorAndAccount { // inject full error context with access to account
      // do some stuff which needs an account and can raise an error
    }
  }
}
Is there a more elegant way how to do that? Perhaps some clever tricks with nullability of receivers?
a
I don't think there's an easy way to do this; you can maybe separate the code a bit more:
Copy code
val account = withApiError {
  ...
}
wihApiErrorAndAccount(account) {

  ...
}
but at the end of the day you have two different
ApiError
contexts that need to be injected separately
d
I was perhaps thinking of the concept of optional contexts. Therfore my not about nullability tricks. Perhaps this is not related to arrow after all, and is more specific to context receivers/parameters... Do you know whether such an option (optional context) was ever discussed?
a
it was, and the current proposal: 1. allows having two overloads, one with no context parameters, one with some of them 2. does not allow optional context parameters with a default value (figuriing out how to resolve those would be chaos)
👍 1
y
Maybe
withAccount
should inject a new
Raise
context that does the extra logging. Hence`withApiError` augments the pre-existing context
d
Eventually, I ended up creating a wrapper class for the logger with a mutable nullable field which is set only once the account context is available.
Copy code
class TraceLogger(var accountName: String? = null) {
        fun log() = traceClient.log(accountName)
    }

    fun <R, E : ApiError> withApiError(traceLogger: TraceLogger, block: Raise<E>.() -> R): R =
        recover(block) { error ->
            traceLogger.log()
            ...
        }

    fun <R, E : ApiError> withApiTraceError(
        block: context(Account, Raise<E>) () -> R,
    ): R {
        val traceLogger = TraceLogger() {
        return withApiError(traceLogger) {
            withAccount {
                traceLogger.accountName = name
                block(this@withAccount, this@withApiError)
            }
        }
    }
I'm quite happy with this solution because it exactly mimics the actual data flow.