Good day all, I'm looking for feedback on two bi...
# arrow
t
Good day all, I'm looking for feedback on two bits now that I'm a few weeks into my FP journey (which I love more every day 🙏 😍 arrow). Firstly I'm finding myself turning everything into a comprehension ... is that optimal or is it over zealous? Secondly, I've been toying with extension functions when I encounter different/nested combinations like Either<Either...>. In my current use case I have
Either<A, List<Either<A, B>>
so I've created two extension functions to unpack it:
Copy code
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:
Copy code
.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!
s
I’m not sure what your use-case is for having so many nested `Either`s. I’m guessing you’re doing some kind of validation and want to combine results? There is also a
Validated
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, etc
👍🏾 1
t
You're close to what I'm trying to do. I'll try and explain my use case as that might help. I'm building a header bidding (digital advertising) system and I'm processing an 'auction' for ad units on a website. Each auction comprises 1+ ad units, and for each ad unit I need to bid/no bid but if there is an error I also want to log it for troubleshooting but then exclude it from the auction response. So when processing the auction I have
Either<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:
Copy code
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)
        }
I should note this is the most complicated FP I've ever done, so I'm not sensitive to feedback and starting again 🙂 Unfortunately I don't know any FP ninjas which is why I'm here asking for feedback!
💪🏾 1
j
@tim I think your
flattenAndMap
can be replaced with the simple
Either.map
, passing a composition of
f
and
splitLeftAndRight
as the mapping lambda.
t
ahh yes good point:
Copy code
.map { 
  it.splitLeftAndRight()
}
.map { (lefts, rights) ->  
  if (lefts.isNotEmpty())
    log.warn { "Lefts errors being filtered: $lefts" }
  rights
}
is simplier
👍🏾 1
j
Also, I think
Copy code
.map { 
  it.splitLeftAndRight()
}
can be
Copy code
.map(::splitLeftAndRight)
t
I'm on the fence with receiver notation as I find it a bit less readable ... is there a strong argument for it or just preference?
j
I'm an FP newbie myself, so don't take my opinion as representative. But I think shorter is better. Especially in this case where receiver notation is, I think, the more idiomatic form in the Kotlin language specifically.
t
I'm currently flip-flopping between the two and changed the receivers I had today to lambdas!! 🙂 I find in a comprehension, where I am using several lambdas, its less readable ... but maybe I should prefer moving everything into a fun and use receiver notation rather than lambas?! 🤷‍♂️
j
The reading difficulty, I think, may be due to not having completely made the cognitive transition to the idiomatic form. But once you start seeing it more frequently in code that you write, it naturally becomes just as readable. Perhaps even more readable, because it takes less space and is visually an easily identifiable pattern. But if your team has an established convention, it's probably more valuable to follow that, whatever it is.
t
ahh fair point ... the more I use kotlin the more i prefer idiomatic conventions, whereas when I first started with Kotlin I had no preference
j
I'm not sure what you mean about having to switch your code between funs and receivers. With the code samples you provided, I think you can do
.map(::f)
whether
f
is a fun or an extension function.
t
perhaps I'm using the wrong terminology but I referring to funs vs lambdas:
Copy code
map(::doSomething)

fun doSomething(): Int { return 1 + 1 }

vs 

map { // lambda that does the same thing as doSomething
   1 + 1
}
j
Oh, I see. I misunderstood. The way I go about deciding about this sort of thing is: I start with code in a lambda for the purpose of thinking through what it needs to do. Then I switch mental modes to considering secondary factors besides logic: If the code needs to be reused elsewhere, I
fun
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.
t
Sounds look a solid heuristic!
👍🏾 1