hi! I was wondering if there are any patterns for ...
# arrow
j
hi! I was wondering if there are any patterns for composing functions that return errors from separate type hierarchies? i.e.
Copy code
fun one(): Either<MyErrorType, Something>
then
fun two() : Either<MyOtherErrorType, Something>
I'd like to avoid
MyErrorType
and
MyOtherErrorType
being in the same error type hierarchy, so I probably need to map/wrap them to something common. Are there any known patterns to do that?
this is where i miss the union type
💯 4
same 4
c
I tend to create a sealed class:
Copy code
sealed class FooError {
    data class MyErrorType(val error: MyErrorType) : FooError()
    data class MyOtherErrorType(val error: MyOtherErrorType) : FooError()
}

context(Raise<FooError>)
fun foo() {
    withError(FooError::MyErrorType) { one() }
    withError(FooError::MyOtherErrorType) { two() }
}
s
That indeed works best, it's some boilerplate until we have union types. Potentially with K2 it's possible to build a compiler plugin for union types until they're released 🤪 Only annoying this is that it would probably also require a IntelliJ plugin to work. Still I am tempted to work on that to give nicer IDEA support for Arrow. Even if it's just hints, and refactoring tips, etc.
c
I really wish we could just…
Copy code
union class FooError {
    MyErrorType,
    MyOtherErrorType,
}
…but well. Maybe after K2 they'll have more time to bring back the union types ticket to the table?
j
Yeah, I asked the question as that is precisely what I want to avoid. MyErrorType and MyOtherErrorType are both sealed interfaces but they have no common parent. I wouldn't like to make them related as I don't want to couple the two packages that these types live in.
c
My answer doesn't make them related, it's a new sealed class that has both as regular fields. There is no inheritance here. Ignoring the boilerplate, my example is a tagged union, whereas the ideal would be an untagged union, but it's an union nonetheless.
j
Oh, sorry! I misread. Let me look again 🙂
s
That's why I typically use
sealed interface
instead of
sealed class
. It easily allows for "composing hierarchies" without all this additional overhead. I just do it to avoid the boilerplate...
Maybe after K2 they'll have more time to bring back the union types ticket to the table?
Their current focus is K2, they've mentioned that they expect an increase in velocity in other areas such as union/intersection types but their is also still some discussion about the syntax.
👍 4
We used to have
Union2
,
Union3
,
Union4
, etc which you could use as an generic solution for this as well.
Copy code
sealed interfae Union2<out A, out B> {
    data class First<A>(val value: A) : Union2<A, Nothing>()
    data class Second<B>(val value: B) : Union2<Nothing, B>()
}

context(Raise<Union2<MyErrorType, MyOtherErrorType>)
fun foo() {
    withError(Union2::First) { one() }
    withError(Union2::Second) { two() }
}
If you need this often it could be useful to introduce as a small utility
j
@CLOVIS that's very helpful. I had something similar in mind, but I wasn't aware of
withError
. I'll play with that!
c
withError
is something I requested to make exactly this pattern possible 😅 it's new in 1.2.0 I think
🙌 2
s
withError
was introduced in between 1.2.0-RC and 1.2.0 thanks to feedback from @CLOVIS 🥳
j
@simon.vergauwen Thanks! I saw some issues on arrow's github related to unions and was wondering what happened to that.
c
What I usually do: • for each domain entity, I have a sealed interface
Failure
• for each domain operation, I have a sealed interface that inherits from that entity's
Failure
• for each possible error case, I have a data class or object that implements all interfaces corresponding to operations that can throw it. Example: https://gitlab.com/opensavvy/formulaide/-/blob/main/core/core-users/src/commonMain/kotlin/User.kt?ref_type=heads#L183 I'm not saying it's the best way to do it (it's very verbose…), but with
withError
it's a lot simpler. I'm going to continue iterating over it until I find something I'm happy with
1
In my case, it's absolutely important to have separate DTOs for all failure types, but all the mapping of successful results/failed errors is very painful.
j
@CLOVIS looks like I'm following you on the journey as I started coming up with something similar 😅
c
Please do send a message in this channel once in a while if you find new ways to encode it. I really want to be able to exhaustively encode all possible errors, but it's a lot of work.
👍 1
Mapping code is far from fun.
💯 2
If you want to learn more about how I specifically structure my applications, I wrote about it here → https://opensavvy.gitlab.io/pedestal/documentation/backbone/index.html I'm definitely the odd one out doing this though, it's not endorsed by Arrow etc
1
I think there's a world in which Arrow Exact simplifies mapping for all types (not just single-field classes), but I don't see it yet.
s
It also really depends what kind of application, and architecture you use. In microservices when using DDD and ordering code by feature/domain I really have to create different errors that need to be combined. I also choose to duplicate errors in different "feature modules", which I think is a smaller price to pay than having to create wrapping hierarchies
👍 1
j
thanks! useful stuff
arrow intensifies 3