How much ok is it to use Any in general applicatio...
# codingconventions
g
How much ok is it to use Any in general application code to represent a union? Use-case: return either a dto or a sealed class. End-goal is to simplify the function and chop it in smaller pieces but probably this will no simplify it at all 😛 Thanks in advance for any answers!!
c
Have you considered using a monad for this, such as Arrow’s Either?
g
nice idea!. Should i bring the whole lib in, or should i just implement my mini either? probably the latter..
c
i toyed with the same, and ultimately brought in Arrow Core as it provides all kinds of goodness and addresses lots of nuances such as coroutines etc. Plus, it’s well tested so I don’t have to maintain my own stuff there 😉
g
Nc, thanks for help !
j
You can also use a sealed class to model unions if you're ok with wrapping
c
that’s the same thing, at least for Either - it’s a sealed class wrapper over a union of left and right.
j
Exactly, hence why it's probably not needed to change the whole paradigm since it means wrapping either way
c
Not sure I follow. Wrapping (monad) is a different paradigm than currently in use (in this case).
j
ArrowKt is a different way of programming than the standard Kotlin, functional-oriented, on other levels. So integrating the library usually would imply adopting the spirit as well (that's what I meant)
c
Arrow is a library, you can choose what to use from it. Either is simply a wrapper class that you can use w/o adopting full FP concepts.
j
Of course, you can do that. What I mean is that it might not be super beneficial to add this library just for
Either
, as opposed to writing your own. So in general I would expect people to add it when they really want to switch paradigms
c
Using something that is proven and tested is generally better than writing your own, unless you plan to invest in everything around that (and that aligns with your dev goals, etc). All the interactions with exceptions, coroutines, etc are significant; DSLs that support the concept are non-trivial to conceptualize, implement, etc. People can add a library for whatever reasons meet their current needs.
j
I think we agree but we are not talking about the same thing. My point was that if you really just want to model a union with a wrapper without FP concepts nor functional methods on it, there is no point to bring a library just to avoid writing a 4-line sealed class. Now you're mentioning complex interactions with exceptions and non-trivial DSL. In that case I totally agree it is better to use a tested library like Arrow. Maybe it was a bit too strong on my part to say you would change the rest of your code to functional paradigm - you can use functional helpers for this, like we do for collections, and keep the rest of the code as-is
c
fair enough, thank you for clarifying. I recently went through this exercise and contemplated writing my own wrapper, ultimately deciding not to as it seems trivial at the outset (as you noted it’s a sealed class heirarchy), but then the scope starts to grow as you look at transforming exceptions into results, coroutine integration, DSLs/extensions to help with extracting values, etc.
j
as you look at transforming exceptions into results
That's what I mean by changing paradigm 🙂
c
yes - introducing the wrapper for return values changes the paradigm and has ripple effects.
r
one could also be inclined to ask why not utilize at least the basic functionalities, I can see at least a few places where I'd say approaching it with a proper
Either
is quite superior. First off one get
map
,
mapLeft
and
flatMap
, which by themselves are quite powerful and useful constructs then there's also the either-continuation (requires it to be a coroutine though) that is also quite useful for giving the code a more natural flow:
Copy code
suspend fun foo(): Either<A, B> = either {
    val someEither = someEither() // Either<A, B>
    val a = someEither.bind() // B if someEither is right, otherwise it stops computation and returns the left side

    a.doSomething()
}
Lastly
Either
is also a really good solution to the frustating `null`/`Optional`/whatever talk, where if one analyze them all of them is wrong! (yes, I know the community disagree with me there). All of them doesn't tell anything and are bad ways to represent nothing, because they don't communicate what is meant by nothing. having
Either<DomainError, A>
and then having domain-error be something like:
Copy code
sealed interface DomainError

data class NoSuchElement(val id): DomainError

object ServiceUnavailable: DomainError

// ... etc
will communicate a lot better, and be many fold more true to the spirit of DDD and my experience is it's worth (by a lot!) the additional effort because it gives an explicit understanding and clear code analysis for other developers
d
See how kotlin.Result uses Any, but wraps it in a value class, then you might not need any wrappers...
Or less, at least..
I think the error case is wrapped, but hopefully happens less often
r
See how kotlin.Result uses Any, but wraps it in a value class, then you might not need any wrappers...
... using
Any
like that is kind of a big turn down if you ask me, especially in a strong type system that allows stronger types. Using
Any
there would be a major design mistake... fortunately Jetbrains did not do that mistake,
kotlin.Result
does not! use
Any
but generics: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/ that said, it looks like a slightly weird design, a bit like a less useful/powerful interpretation of the
Either
monad that tries to encompass the common design mistake of using exceptions for domain logic. We do have the language power to express a proper distinction between domain errors end system/programming errors. Let's keep exceptions exceptional and only use them when things are truely wrong and design our domain errors - which are expected - instead :)
r
@dave08 curious... Can't really argue with that - wondering why they made that choice... That however strengthens my motivation to stay away from it and use ArrowKt anyway. It's a bit like some of the old anti-patterns (Exceptions for domain errors, nulls etc.) just won't go away even though there are better designs - made from better to far superior in Kotlin (because Kotlin makes it so much easier to do things the good way)
correction: "a good way" not "the", after all there are almost certainly multiple good ways 🙂
d
When you want performance and you have lots of data to wrap... value classes help a lot -- but as long as Kotlin doesn't support union types, you can't represent both error and values in Result and still have a value class, so they probably chose that the success path which is the most common shouldn't be wrapped, and the error type would be wrapped.
At least the value class encapsulates the Any well and doesn't leek it out...
So it gets pretty similar to Either on the user's side
r
there are always edge cases, I wouldn't base general design on edge cases 🙂 especially when the argument is performance because a lot (not all! of course) of cases where people design for performance it's premature optimization and often it turns out that if there are performance problems they can be hidden in completely different places anyway, so much better to measure and take the bottlenecks there and only optimize what's needed regarding union types I must admit I find them a bit of a mixed blessing. For combining errors in various application layers where there might not be a meaningful intermediate interpretation, I can kind of see it. For the successful return type they make my hairs stand, because it means that the designer of the function/api/whatever either wasn't clear on what to return or simple didn't bother to design what's returned
d
Yeah, everything can be abused... but it's good to have these things in one's toolbox for when it IS good to use them.