Are there best practises with using Either? Lets s...
# arrow
u
Are there best practises with using Either? Lets say I have a factory class User.create(form): Either<List<Problem>, User> - lets also assume I have a private User.userType() used by the create() to convert the form enums to a kotlin class enum - should the userType() also return an either ? I get a bit confused as to whether I should use Eithers for the validation process in the factory because then things get really messy. I guess my question is where I should and shouldn't use an Either. I have realised that using it everywhere can make things really bloated
r
You should use
Either
type on your public interface. I mean, will you ever build an instance of
UserType
alone? If yes, then the factory of the type must return an
Either
. If the public interface is the
User
type, then the
Either
must go only of the
UserFactory
. The DDD purist would say you need
Either
only on your aggregate. Btw, use
EitherNel
instead of
Either
with a list. Try to have a look also at the smart constructor pattern.
u
cool, someone else who does ddd w/ functional programming - a rare breed! Isnt an Either a replacement for the try/catch ? so if a validation condition fails just return a left. The problem is the aggregation of these eithers because kotlin doesnt have a scala for-comprehesive like feature, handling multiple eithers from many entities gets very confusing when compared to the humble try/catch
r
Uhm, you must use the
bind
method of arrow. Please, check this article for further details: https://blog.rockthejvm.com/functional-error-handling-in-kotlin-part-2/
Moreover, I have a Github repo where I'm applying all of these concepts: https://github.com/rcardin/functional-event-sourcing-in-kotlin
Let me know if you need more information
Kotlin is not Scala, you're right. It has its patterns
u
Just my two cents... The key issue in error handling is what to do in response: Do you want to inform some user or client system, so they can change their input? Do you want to recover or retry? Or is there nothing you can do except changing your program or manually dealing with your hardware or networking? IMHO, typed errors are the way to go for contracts on your interfaces. Here, typed errors form part of your API, so it's much better to have them show in the types than to be hidden inside your code in some try/catch statement. On the other hand, bugs in your code typically should lead to exceptions being thrown (fail fast), to be picked up by some top-level exception handler that informs ops or dev, so they can fix the application. Somewhere in between are problems with external systems you interface with. Here, you have to judge if you want to thrown an exception because you need to adapt your code or infrastructure, of if a typed error is the way to go, because some staff member may be able to, say, fix incorrect data in the neighbour system, or you might implement a selective retry mechanism, depending on the error type
u
@Riccardo Cardin Brilliant contributions there, all very useful. Double thanks for the git event sourcing, we've just rolled out a project with scala and akka so a kotlin variant is most welcome. Do you have any recommendations on handling bind() with a Either<List<Problem>, Unit> - It seems not to work if the return type is a list
@Ulrich Schuster I am familiar with and prefer typed errors - it was just a bit confusing since I am not familiar with the arrow work flow (coming from a scala background)
r
Can you post an example of code that doesn't compile?
u
@Riccardo Cardin a little something I quickly whipped up:
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
interface Prob
sealed interface HouseRoomProblem: Prob {
data object HasNoName: HouseRoomProblem
}
object HouseRoom {
data class Room(val name: String)
fun *create*(name: String): Either<HouseRoomProblem, Room> = _either_ *{*
ensure(name._*isNotEmpty*_()) *{*
HouseRoomProblem.HasNoName
}
*Room*(name)
}
}
object House {
data class House(val room: HouseRoom)
fun *create*(): Either<List<HouseRoomProblem>, House> = _either_ *{*
*House*(
room = HouseRoom.*create*("room1").bind()
)
}
}
r
The code has some errors 🙂 Here is the fixed version:
Copy code
interface Prob
sealed interface HouseRoomProblem: Prob {
    data object HasNoName: HouseRoomProblem
}
object HouseRoom {
    data class Room(val name: String)
    fun create(name: String): Either<HouseRoomProblem, Room> = either {
        ensure(name.isNotEmpty()) {
            HouseRoomProblem.HasNoName
        }
        Room(name)
    }
}
object House {
    data class House(val room: HouseRoom.Room)
    fun create(): Either<HouseRoomProblem, House> = either {
        House(
            room = HouseRoom.create("room1").bind()
        )
    }
}
The problem was not in the
List<HouseRoomProblem>
, but with the fact that you don’t use/create the list at all 😅
u
Yes, that is what I am asking. Using bind() with a List<HouseProblem> return type doesn't work, which may posit an issue in an aggregate where I want to collect errors and return a list. I should have added a comment
I guess in such a case a bind() would be the wrong go to solution
r
Tomorrow, I'll give you the solution using the list. Sorry, I didn't get your intentions at first
u
thanks
r
Something like this:
Copy code
object House {
    data class House(val room: HouseRoom.Room)
    fun create(): Either<List<HouseRoomProblem>, House> = either {
        House(
            room = HouseRoom.create("room1").mapLeft { listOf(it) }.bind()
        )
    }
}
Otherwise, you should use the
zipOrAccumulate
function if you want to accumulate all the validation errors.
🙌 1
u
that looks good. You suggested using EitherNel instead of Either<List<A>, B> earlier on, what is your rationale for that ? They both look pretty much the same to me, are there any benefits with EitherNel ?
r
You know, if you have a
Left
instance, you always have a non-empty list of errors. So, you can use the
EitherNel
, which uses a
NonEmptyList
to accumulate errors.
👍 1
It’s the same in Scala + Cats
👍 1
u
Thanks once again. Is it possible to use the EitherNel with the either dsl ? I want to make use of bind() and ensure() but the either dsl conflicts with EitherNel
r
Please have a look at the project I shared with you. Here is an example:
Copy code
context(NonZero<T>)
fun <T : Number> T.nonZero(fieldName: String): EitherNel<ZeroFieldError, T> =
    either {
        ensure(nonZero()) { nonEmptyListOf(ZeroFieldError(fieldName)) }
        this@nonZero
    }
1
bind()
works like a charm with
EitherNel
since this last type is only a type alias for
Either<NonEmptyList<E>, A>
Maybe it’s time to study a little bit of arrow 😛
u
thanks, there aren't many resources out there with regards to arrow hence why I've been struggling. I will review your code in more details. Cheers!