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