tim
07/01/2020, 8:56 AMEither<A, List<Either<A, B>>
so I've created two extension functions to unpack it:
fun <L, R> List<Either<L, R>>.splitLeftAndRight(): Tuple2<List<L>, List<R>> {
val left = mutableListOf<L>()
val right = mutableListOf<R>()
forEach {
when (it) {
is Either.Left -> left += it.a
is Either.Right -> right += it.b
}
}
return Tuple2(left.toList(), right.toList())
}
fun <A, B, C> Either<A, List<Either<A, B>>>.flattenAndMap(f: (Tuple2<List<A>, List<B>>) -> C): Either<A, C> =
when (this) {
is Either.Left -> a.left()
is Either.Right -> {
f(b.splitLeftAndRight()).right()
}
}
And now in my comprehension becomes:
.map {
// something that returns Either<A, List<Either<A, B>>>
}
.flattenAndMap { (lefts, rights) ->
log.warn { "Filtering nested lefts: $lefts" }
rights
}
So my question here is, does this approach make sense, or is there something wrong with it/I've missed something in the FP toolbox that would make my life easier?
Thank you!simon.vergauwen
07/01/2020, 9:01 AMValidated
data type, which in combination with NonEmptyList
& Semigroup.nonEmptyList
can automically combine and collect errors.
val result = ValidatedNel<Error, Success>
which represents either NonEmptyList<Error>
or a single Success
value.
It however also doesn’t have the concept of having both Error
& Success
.
There is however a 3rd data type called Ior
, if Either
is an exclusive OR
than Ior
is an in-exclusive OR
.
In other words it can be Right
, Left
or Both
, using Ior<List<A>, List<B>>
you might also be able to easily express your problem.
bimap
would be spliftLeftAndRight
,
flatMap
would remain flatMap
,
handleErrorWith
to flatMap
Left
side,
etctim
07/01/2020, 9:14 AMEither<Failure<Auction>, Response>
. The response comprises a list of Bid/NoBid/Error
corresponding to each ad unit. I decided to categorise No Bid as a Failure to simplify and so I end up with something like this:
Either<Failure<Auction>, List<Either<Failure<Bid>, Bid>>>
And now part of my comprehension for processing the auction looks like this:
request
.shouldProcess()
.map {
it.bids.map { it.buildDemoBidAndRecord() }
}
.flattenAndMap { (lefts, rights) ->
if(lefts.isNotEmpty()) {
log.warn { "Lefts errors being filtered: $lefts" }
}
rights
}
.map {
val (responsesKind, recordsKind) = it.unzip()
Tuple2(responsesKind.fix(), recordsKind.fix())
}
.map { (bidResponses, bidRecords) ->
val auctionRecord = AuctionRecord.from(request, bidResponses, bidRecords)
PrebidModuleRepository
.insert(auctionRecord)
.handleError {
log.error("PrebidError: $it")
}
auctionRecord
}
.map {
if (it.responses.isEmpty()) {
log.warn { "No bids: $it" }
}
it
}
.map {
updateModuleLastRun(PrebidModule.Namespace)
.toTuple2With(it.responses)
}
tim
07/01/2020, 9:15 AMjulian
07/01/2020, 6:57 PMflattenAndMap
can be replaced with the simple Either.map
, passing a composition of f
and splitLeftAndRight
as the mapping lambda.tim
07/01/2020, 7:30 PM.map {
it.splitLeftAndRight()
}
.map { (lefts, rights) ->
if (lefts.isNotEmpty())
log.warn { "Lefts errors being filtered: $lefts" }
rights
}
is simplierjulian
07/01/2020, 8:18 PM.map {
it.splitLeftAndRight()
}
can be
.map(::splitLeftAndRight)
tim
07/01/2020, 8:22 PMjulian
07/01/2020, 8:23 PMtim
07/01/2020, 8:26 PMjulian
07/01/2020, 8:28 PMtim
07/01/2020, 8:29 PMjulian
07/01/2020, 8:33 PM.map(::f)
whether f
is a fun or an extension function.tim
07/01/2020, 8:35 PMmap(::doSomething)
fun doSomething(): Int { return 1 + 1 }
vs
map { // lambda that does the same thing as doSomething
1 + 1
}
julian
07/01/2020, 8:41 PMfun
it. If I need to provide different implementations of the lambda, like in unit tests, then fun
. If the lambda is more than one or two lines, fun
it. If what the lambda does isn't immediately obvious by merely glancing at it, fun
it - because that allows you to refer to it by a name that's descriptive of what it does.tim
07/01/2020, 8:42 PM