ivanmorgillo
11/19/2021, 4:58 PMSatyam Agarwal
11/20/2021, 3:56 PMtypealias ErrorOr<A> = Either<Throwable, A>
suspend inline fun <reified A> Db.useTxn(noinline f: suspend (Handle) -> ErrorOr<A>): ErrorOr<A> = withContext(IO) {
Either
.catch { this@useTxn.open().begin() }
.flatMap { handle -> bracketCase(acquire = { handle }, use = f, release = { h, e -> h.closeTxn(e) }) }
}
suspend fun Handle.closeTxn(e: ExitCase): ErrorOr<Unit> {
return Either.catch {
guarantee(
{
when (e) {
is ExitCase.Completed -> this.commit().close()
is ExitCase.Cancelled -> this.rollback().close()
is ExitCase.Failure -> this.rollback().close()
}
},
{ this.close() }
)
}
}
Problem with this code is that it will never come to ExitCase.Failure as arrow-kt checks on what exception is thrown and then handles result :
val res = try {
use(acquired)
} catch (e: CancellationException) {
runReleaseAndRethrow(e) { release(acquired, ExitCase.Cancelled(e)) }
} catch (t: Throwable) {
runReleaseAndRethrow(t.nonFatalOrThrow()) { release(acquired, ExitCase.Failure(t.nonFatalOrThrow())) }
}
We changed from bracketCase
to Resource
with throwing exception inside use , but to mitigate the above flaw, with bracketCase
it will roughly look like this :
suspend inline fun <reified A> Db.useTxn(noinline f: suspend (Handle) -> ErrorOr<A>): ErrorOr<A> = withContext(IO) {
Either
.catch { this@useTxn.open().begin() }
.flatMap { handle ->
Either.catch {
bracketCase(
acquire = { handle },
use = { h -> f(h).fold({ error -> throw error }, ::identity) },
release = { h, e -> h.closeConn(e) }
)
}
}
}
This problem occured when I migrated from IO<A>
to suspend () -> Either<Throwable, A>
We have fixed in our code base like this, but *It would have been awesome we got Either based apis for bracketCase and Resource. *
Just wanted to highlight how can we easily mess up things 😄 🙈SecretX
11/20/2021, 8:54 PMOption<Pair<A, Option<B>>>
to Option<Pair<A, B>>
using arrow? This is the code were I want to apply that
fun playerDisconnect(player: Player) {
activeDungeons.remove(player.uniqueId) // returns a String?
?.let { dungeonRepo.getDungeon(it) } // returns an Option<Dungeon>
?.tap {
dungeonRepo.getLastLevel(it) // I need to somehow zip the "Dungeon" with this Option<Level> (returned by the getLastLevel), and use both, or none of the two, if there's no last level
// filter the result using levelSpawnsManager#isLevelComplete, passing the Level I just got as argument
// then run these methods after
levelSpawnsManager.cancelDungeonSpawns(dungeon)
levelSpawnsManager.resetCompletedLevels(dungeon)
player.runLogoffCommands(dungeon)
}
}
Stewart Stewart
11/21/2021, 9:24 PMeitherAB.handleError
I might want to handle A
with a B
under certain conditions or for certain subtypes of A
.
instead, I’d have to handleErrorWith
and have a catch-all case re-wrapping in Left.Alvaro Blazquez checa
11/23/2021, 9:09 AMValidated.applicative(validationSemigroup).tupled(Valid("foo"), Valid("bar")).fix()
Marko Novakovic
11/23/2021, 1:37 PMeither
block offers nice syntax but it’s `suspend`ed. is there non-`suspend`ed version or I have to use flatMap
and map
?Ivan Lorenz
11/23/2021, 4:57 PMfun RedisConnection.saveRedisSession(session: Session): Save<Unit>
How can we achieve that? I understand that this is a impossible approach because extension functions are resolved statically. Then, how to mock o test double them? Or the real problem is we should not need to mock functions used by other functions? Then what to test? 😄 Sorry for the mess but switching paradigms is a tough brain task. Thank you very much in advance.ipolyzos
11/23/2021, 4:59 PMMilse113
11/25/2021, 9:14 PMjean
11/26/2021, 7:52 AMfun fetchById(id: String?) = Either.catch(::onApiError) {
id ?: return Either.Left(InOrderError.ApiError)
...
}
is there an nicer way to guard the nullability of Id in this case? I found left()
but it doesn’t let me pass a type of errorShalom Halbert
11/27/2021, 6:09 PM<ul><li>Parse a string into an integer</li><li>Calculate the reciprocal</li><li>Convert the reciprocal into a string</li></ul>
Shalom Halbert
11/27/2021, 10:25 PMEither<Exception, Any>
with left
equal to a caught Exception
is equal to an expected value?Shalom Halbert
11/28/2021, 3:55 AMfun doSomething() = IllegalAccessException("message").left()
@Test
fun sample() {
doSomething().shouldBeLeftThrowableWithMessage<IllegalAccessException>("message")
}
dnowak
11/29/2021, 12:34 AMGemy
11/29/2021, 11:32 AMTies
12/01/2021, 9:33 AMhttps://www.youtube.com/watch?v=Wojgv2MeMGU▾
Filip Piechowski
12/02/2021, 12:18 PMPhilipp Mayer
12/03/2021, 11:00 AMStewart Stewart
12/03/2021, 9:46 PMcatch
that only handles specific exceptions and throws the rest?Emil Kantis
12/03/2021, 10:03 PMEither.leftOrNull()
? For instance if I have a List<Either<A,B>>
and want to map that into a List<A>
?thanh
12/04/2021, 2:14 PM<https://github.com/arrow-kt/arrow/blob/main/CONTRIBUTING.md#steps>
are outdated.
cd arrow-libs/core
./gradlew build
doesn't work any morepakoito
12/06/2021, 11:43 AMipolyzos
12/06/2021, 2:35 PMDaniel Berg
12/07/2021, 2:00 AMthan_
12/07/2021, 2:30 PMSchedule.identity<Boolean>().delay { Duration.minutes(15) }.whileOutput(::identity)
Unfortunately this executes instantly. Is there a way to create this scheduler (other that putting kotlinx.coroutines.delay in the repeat function)?simon.vergauwen
12/07/2021, 3:03 PMibcoleman
12/07/2021, 8:41 PMVitali Plagov
12/09/2021, 2:39 PMbuild.gradle
has this:
plugins {
id "org.jetbrains.kotlin.kapt" version "1.6.0"
}
dependencies {
implementation "io.arrow-kt:arrow-core:1.0.1"
implementation "io.arrow-kt:arrow-optics:1.0.1"
kapt "io.arrow-kt:arrow-meta:1.0.1"
}
Then, all data classes are annotated with @optics
and have a companion object
// showing a simple data class for brevity reasons
@optics
data class Mapping(
val type: MappingType,
val values: List<String>
) {
companion object
}
Next, in my class when I’m trying to do this:
val updatedMapping = Mapping.
no fields (type
or values
) are shown after .
Am I missing something?simon.vergauwen
12/15/2021, 9:36 AMShalom Halbert
12/15/2021, 12:11 PMShalom Halbert
12/15/2021, 12:11 PMtavish pegram
12/15/2021, 6:29 PMTried to refactor this in a way that
Respects a consistent level of abstraction in each function
Keeps the "top-level" abstraction declarative and for the most part hiding the sad path (though I did leave in a single mapErrors fn in each call
Hides the "glue" or "adapter" functions that make all the different types fit together.
I'm currently wrestling with the idea that
Arrow and FP is good and provides super generic good tools like Either, map, flatMap, etc. that we can use to implement pretty much anything in a Good Way.
Those same tools are "low level" and distracting when reading the code
So what I'm messing around with is trying to keep the main bits of behavior in each file/class at a high level of abstraction (of course, what "high-level abstraction" means depends on the context. In this example its high level to talk about HTTP responses and JSON. In a Payment service, that would be considered low level). We do this by adding a bunch of private extension functions that hide the "glue" which is normally needed to fit all these different types together while giving them expressive and clear names (approaching a DSL, yay!), and keeping the functions small enough that they are easy to update and reason about in isolation (hopefully, its what I was going for).
Another way of thinking about it is that we are taking these great super generic lego pieces and fitting them together into larger (though still modular) "solutions" that we paint with concrete function names before finally composing those concrete pieces together at the "top"
class AffirmClient(
private val httpClient: HttpClient,
// @Value("\${affirm.au.private_key}")
private val affirmAuPrivateKey: String,
// @Value("\${affirm.au.public_key}")
private val affirmAuPublicKey: String,
// @Value("\${affirm.au.hostname}")
private val affirmAuApiUrl: String,
) {
private val logger = KotlinLogging.logger { }
suspend fun authorizeTransaction(
partnerPaymentMethodId: String,
metadata: Metadata,
): Either<AuthedError, AuthorizeTransactionResponse> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions",
request = AuthorizeTransactionRequest(
transactionId = partnerPaymentMethodId,
orderId = metadata.orderId,
),
auth = getAuthHeader(),
)
}.mapLeft {
AuthedError.AuthFailureError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.authorizeTransaction response: ${it.body}" }
if (it.code == 200) {
AuthorizeTransactionResponse.fromHTTPBody(it.body).right()
} else {
AuthedError.AuthFailureError(message = it.body.optString("message").toString()).left()
}
}
suspend fun captureTransaction(
authorizationToken: String
): Either<CapturedError, CaptureTransactionResponse> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions/$authorizationToken/capture",
auth = getAuthHeader(),
)
}.mapLeft {
CapturedError.CaptureActionError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.captureTransaction response: ${it.body}" }
if (it.code == 200) {
CaptureTransactionResponse.fromHTTPBody(it.body).right()
} else {
CapturedError.CaptureActionError(
message = it.body.optString("message").toString()
).left()
}
}
suspend fun refundTransaction(
confirmationId: String,
amount: Int,
): Either<RefundError, RefundTransactionResponse> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/refund",
request = RefundTransactionRequest(amount = amount),
auth = getAuthHeader(),
)
}.mapLeft {
RefundError.RefundFailureError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.refundTransaction response: ${it.body}" }
if (it.code == 200) {
RefundTransactionResponse.fromHTTPBody(it.body).right()
} else {
RefundError.RefundFailureError(
message = it.body.optString("message").toString()
).left()
}
}
suspend fun voidTransaction(
confirmationId: String
): Either<VoidError, Unit> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/void",
auth = getAuthHeader(),
)
}.mapLeft {
VoidError.VoidFailureError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.voidTransaction response: ${it.body}" }
if (it.code == 200) {
Unit.right()
} else {
VoidError.VoidFailureError(
message = it.body.optString("message").toString()
).left()
}
}
private fun getAuthHeader(): HttpHeader =
HttpHeader(key = affirmAuPublicKey, value = affirmAuPrivateKey)
}
Afterclass AffirmClient(
private val httpClient: HttpClient,
private val affirmAuPrivateKey: String,
private val affirmAuPublicKey: String,
private val affirmAuApiUrl: String,
) {
@Loggable
suspend fun authorizeTransaction(
partnerPaymentMethodId: String,
metadata: Metadata,
): Either<AuthedError, AuthorizeTransactionResponse> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions",
request = AuthorizeTransactionRequest(
transactionId = partnerPaymentMethodId,
orderId = metadata.orderId,
),
auth = getAuthHeader(),
)
.mapAuthErrors()
.parseAuthorizeResponse()
@Loggable
suspend fun captureTransaction(
authorizationToken: String
): Either<CapturedError, CaptureTransactionResponse> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions/$authorizationToken/capture",
auth = getAuthHeader(),
)
.mapCaptureErrors()
.parseCaptureResponse()
@Loggable
suspend fun refundTransaction(
confirmationId: String,
amount: Int,
): Either<RefundError, RefundTransactionResponse> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/refund",
request = RefundTransactionRequest(amount = amount),
auth = getAuthHeader(),
)
.mapRefundErrors()
.parseRefundResponse()
@Loggable
suspend fun voidTransaction(
confirmationId: String
): Either<VoidError, Unit> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/void",
auth = getAuthHeader(),
)
.mapVoidErrors()
.parseVoidResponse()
private fun getAuthHeader(): HttpHeader =
HttpHeader(key = affirmAuPublicKey, value = affirmAuPrivateKey)
}
@Loggable
private fun <http://HttpClient.post|HttpClient.post>(
url: String,
request: HttpRequest? = null,
auth: HttpHeader? = null,
): Either<Throwable, HttpResponse> =
Either.catch {
this.postJson(
url = url,
request = request,
auth = auth,
)
}
private fun <B> Either<Throwable, B>.mapAuthErrors() =
this.mapLeft { AuthedError.AuthFailureError(it.message) }
private fun Either<AuthedError, HttpResponse>.parseAuthorizeResponse() = this.flatMap {
when (it.code == 200) {
true ->
AuthorizeTransactionResponse
.fromHTTPBody(it.body)
.right()
false -> AuthedError.AuthFailureError(
message = it.body.optString("message").toString()
).left()
}
}
private fun <B> Either<Throwable, B>.mapCaptureErrors() =
this.mapLeft { CapturedError.CaptureActionError(it.message) }
private fun Either<CapturedError, HttpResponse>.parseCaptureResponse() = this.flatMap {
when (it.code == 200) {
true ->
CaptureTransactionResponse
.fromHTTPBody(it.body)
.right()
false -> CapturedError.CaptureActionError(
message = it.body.optString("message").toString()
).left()
}
}
private fun <B> Either<Throwable, B>.mapRefundErrors() =
this.mapLeft { RefundError.RefundFailureError(it.message) }
private fun Either<RefundError, HttpResponse>.parseRefundResponse() = this.flatMap {
when (it.code == 200) {
true ->
RefundTransactionResponse
.fromHTTPBody(it.body)
.right()
false -> RefundError.RefundFailureError(
message = it.body.optString("message").toString()
).left()
}
}
private fun <B> Either<Throwable, B>.mapVoidErrors() =
this.mapLeft { VoidError.VoidFailureError(it.message) }
private fun Either<VoidError, HttpResponse>.parseVoidResponse() = this.flatMap {
when (it.code == 200) {
true -> Unit.right()
false -> VoidError.VoidFailureError(
message = it.body.optString("message").toString()
).left()
}
}
Shalom Halbert
12/16/2021, 9:50 PM