https://kotlinlang.org logo
Channels
100daysofcode
100daysofkotlin
100daysofkotlin-2021
advent-of-code
aem
ai
alexa
algeria
algolialibraries
amsterdam
android
android-architecture
android-databinding
android-studio
androidgithubprojects
androidthings
androidx
androidx-xprocessing
anime
anko
announcements
apollo-kotlin
appintro
arabic
argentina
arkenv
arksemdevteam
armenia
arrow
arrow-contributors
arrow-meta
ass
atlanta
atm17
atrium
austin
australia
austria
awesome-kotlin
ballast
bangladesh
barcelona
bayarea
bazel
beepiz-libraries
belgium
benchmarks
berlin
big-data
books
boston
brazil
brikk
budapest
build
build-tools
bulgaria
bydgoszcz
cambodia
canada
carrat
carrat-dev
carrat-feed
chicago
chile
china
chucker
cincinnati-user-group
cli
clikt
cloudfoundry
cn
cobalt
code-coverage
codeforces
codemash-precompiler
codereview
codingame
codingconventions
coimbatore
collaborations
colombia
colorado
communities
competitive-programming
competitivecoding
compiler
compose
compose-android
compose-desktop
compose-hiring
compose-ios
compose-mp
compose-ui-showcase
compose-wear
compose-web
confetti
connect-audit-events
corda
cork
coroutines
couchbase
coursera
croatia
cryptography
cscenter-course-2016
cucumber-bdd
cyprus
czech
dagger
data2viz
databinding
datascience
dckotlin
debugging
decompose
decouple
denmark
deprecated
detekt
detekt-hint
dev-core
dfw
docs-revamped
dokka
domain-driven-design
doodle
dsl
dublin
dutch
eap
eclipse
ecuador
edinburgh
education
effective-kotlin
effectivekotlin
emacs
embedded-kotlin
estatik
event21-community-content
events
exposed
failgood
fb-internal-demo
feed
firebase
flow
fluid-libraries
forkhandles
forum
fosdem
fp-in-kotlin
framework-elide
freenode
french
fritz2
fuchsia
functional
funktionale
gamedev
ge-kotlin
general-advice
georgia
geospatial
german-lang
getting-started
github-workflows-kt
glance
godot-kotlin
google-io
gradle
graphic
graphkool
graphql
graphql-kotlin
graviton-browser
greece
grpc
gsoc
gui
hackathons
hacktoberfest
hamburg
hamkrest
helios
helsinki
hexagon
hibernate
hikari-cp
hire-me
hiring
hongkong
hoplite
http4k
hungary
hyderabad
image-processing
india
indonesia
inkremental
intellij
intellij-plugins
intellij-tricks
internships
introduce-yourself
io
ios
iran
israel
istanbulcoders
italian
jackson-kotlin
jadx
japanese
jasync-sql
java-to-kotlin-refactoring
javadevelopers
javafx
javalin
javascript
jdbi
jhipster-kotlin
jobsworldwide
jpa
jshdq
juul-libraries
jvm-ir-backend-feedback
jxadapter
k2-early-adopters
kaal
kafka
kakao
kalasim
kapt
karachi
karg
karlsruhe
kash_shell
kaskade
kbuild
kdbc
kgen-doc-tools
kgraphql
kinta
klaxon
klock
kloudformation
kmdc
kmm-español
kmongo
knbt
knote
koalaql
koans
kobalt
kobweb
kodein
kodex
kohesive
koin
koin-dev
komapper
kondor-json
kong
kontent
kontributors
korau
korean
korge
korim
korio
korlibs
korte
kotest
kotest-contributors
kotless
kotlick
kotlin-asia
kotlin-beam
kotlin-by-example
kotlin-csv
kotlin-data-storage
kotlin-foundation
kotlin-fuel
kotlin-in-action
kotlin-inject
kotlin-latam
kotlin-logging
kotlin-multiplatform-contest
kotlin-mumbai
kotlin-native
kotlin-pakistan
kotlin-plugin
kotlin-pune
kotlin-roadmap
kotlin-samples
kotlin-sap
kotlin-serbia
kotlin-spark
kotlin-szeged
kotlin-website
kotlinacademy
kotlinbot
kotlinconf
kotlindl
kotlinforbeginners
kotlingforbeginners
kotlinlondon
kotlinmad
kotlinprogrammers
kotlinsu
kotlintest
kotlintest-devs
kotlintlv
kotlinultimatechallenge
kotlinx-datetime
kotlinx-files
kotlinx-html
kotrix
kotson
kovenant
kprompt
kraph
krawler
kroto-plus
ksp
ktcc
ktfmt
ktlint
ktor
ktp
kubed
kug-leads
kug-torino
kvision
kweb
lambdaworld_cadiz
lanark
language-evolution
language-proposals
latvia
leakcanary
leedskotlinusergroup
lets-have-fun
libgdx
libkgd
library-development
lincheck
linkeddata
lithuania
london
losangeles
lottie
love
lychee
macedonia
machinelearningbawas
madrid
malaysia
mathematics
meetkotlin
memes
meta
metro-detroit
mexico
miami
micronaut
minnesota
minutest
mirror
mockk
moko
moldova
monsterpuzzle
montreal
moonbean
morocco
motionlayout
mpapt
mu
multiplatform
mumbai
munich
mvikotlin
mvrx
myndocs-oauth2-server
naming
navigation-architecture-component
nepal
new-mexico
new-zealand
newname
nigeria
nodejs
norway
npm-publish
nyc
oceania
ohio-kotlin-users
oldenburg
oolong
opensource
orbit-mvi
osgi
otpisani
package-search
pakistan
panamá
pattern-matching
pbandk
pdx
peru
philippines
phoenix
pinoy
pocketgitclient
polish
popkorn
portugal
practical-functional-programming
proguard
prozis-android-backup
pyhsikal
python
python-contributors
quasar
random
re
react
reaktive
realm
realworldkotlin
reductor
reduks
redux
redux-kotlin
refactoring-to-kotlin
reflect
refreshversions
reports
result
rethink
revolver
rhein-main
rocksdb
romania
room
rpi-pico
rsocket
russian
russian_feed
russian-kotlinasfirst
rx
rxjava
san-diego
science
scotland
scrcast
scrimage
script
scripting
seattle
serialization
server
sg-user-group
singapore
skia-wasm-interop-temp
skrape-it
slovak
snake
sofl-user-group
southafrica
spacemacs
spain
spanish
speaking
spek
spin
splitties
spotify-mobius
spring
spring-security
squarelibraries
stackoverflow
stacks
stayhungrystayfoolish
stdlib
stlouis
strife-discord-lib
strikt
students
stuttgart
sudan
swagger-gradle-codegen
swarm
sweden
swing
swiss-user-group
switzerland
talking-kotlin
tallinn
tampa
teamcity
tegal
tempe
tensorflow
terminal
test
testing
testtestest
texas
tgbotapi
thailand
tornadofx
touchlab-tools
training
tricity-kotlin-user-group
trójmiasto
truth
tunisia
turkey
turkiye
twitter-feed
uae
udacityindia
uk
ukrainian
uniflow
unkonf
uruguay
utah
uuid
vancouver
vankotlin
vertx
videos
vienna
vietnam
vim
vkug
vuejs
web-mpp
webassembly
webrtc
wimix_sentry
wwdc
zircon
Powered by
Title
g

George

04/02/2023, 3:29 PM
I guess it’s kinda hard to give a general direction on how error handling is supposed to work, since there are many cases where it requires "special" handing. That being said, i follow Roman's blog for advice on how error handling should be, and it has worked great for me. For some reason I took for granted that this way (blog’s) is the most idiomatic, since it depends only on language features without any additional helpers (like Either). I’m not sure what do you mean by "exceptions are too risky to use", but imho: many applications like yours cannot terminate due to an exception (bug, etc), that's where advice controllers (spring terminology, not sure how other frameworks name the pattern) come in play. At least that’s how I handled exceptions (that’s also how the blog advices to use them, it refers to them as top-level framework code). Note: imo, if arrow works for you, then that’s fine too.
s

simon.vergauwen

04/02/2023, 3:40 PM
I'm obviously biased, so take this with a grain of salt. First of all, Arrow would be happy if anything it covers lands in Kotlin Std, or KotlinX. It has always been made it clear that it favor idioms in the language, or official support through KotlinX. Arrow want to be a unified front on FP / safer code, rather than causing fragmentation like we see in some other languages. If any decisions in this direction is ever made, Arrow will be super excited to drop / migrate whatever it has into the same direction. Friction with the language / eco-system results in poor UX. The more Arrow has embraced the language, trying to get rid of wrapper types like
IO
in favor of
suspend
, embracing DSLs, etc the happier we've become writing FP code in Kotlin (and I think the same goes for Arrow users). Personally, I don't think the language has to make an official recommendation. on Arrow, FP, or any OSS library / framework. is not a one-fits-all solution. I.e. Spring vs Ktor vs Quarkus vs Vert.X vs ... all are great candidates in Kotlin. In some cases I also use
Result
, and in others
Either
, nullable types or whatever fits my use-case best. So an official recommendation might even be counterproductive, "pushing" people to a single solution whilst it might not fit for their use-case. Arrow is also not exclusive around
Either
, and offers many other patterns well known in FP. I am personally not even a fan of the term FP anymore, same for OOP. Since with modern languages these things have faded a bit, and often a mix of put styles is used / found with great success.
m

mitch

04/02/2023, 11:39 PM
Thank you @simon.vergauwen those are all very valid point. I’ve seen arrow team has made tremendous effort for making the API consistent with the direction of Kotlin stdlib. In my opinion as a user of the library, especially around error handling best practices, it provides an elegant solution to that gap in the current Kotlin ecosystem. In my personal view I think this is an opportunity for the language to benefit from absorbing some of the learnings in arrow and adopt it as native linguistic constructs. I agree @George about error handling, global / local exception handlers are useful in that regards. We use spring in production so we had controller advices. However, as the code grows, contributors start to lose context as for origins of the exceptions. Some has been silently caught / rethrown as a different exception and forgot to be handled. We also mistakenly caught wrong exceptions or mistakenly uncaught the right ones… We have probably caught cancellation exceptions and OOME as well, it’s very possible.. We had gone through that exercise in the past… With those learnings, the need of a type with a generic failure is very real. We can’t constantly be paged in 2am in the morning for something that the compiler should be able to help… That’s where we’ve decided to leverage Arrow’s
Either<L, R>
type instead of hand-writing our own. So.. with that said, I’d like to go back to the proposition so to make sure the intention is not lost. Why doesn’t Kotlin have this (imo) highly relevant type in the language? I believe one thing that we all would likely agree is for domain failure states to be modelled as an explicit return type. Exceptions are exceptional, and should only be used for exceptional circumstances.
catch { }
is possibly dangerous as it might catch other unwanted exceptions such as
CancellationException
or
OutOfMemoryError
. Let’s introduce something like
Result<E, T>
- instead of having throwable as its failure type, users will have the liberty to type the error channel. Something like below hypothetical type:
// Sorry, the name clashes with Result<T>, 
// decision of using value class, and identifier orders are hypothetical implementation detail 
// the language implementer can choose to implement this as sealed type or native union type accordingly
value class Result<E, A> private constructor(val value: E | A) { 
  fun valueOrNull(): A? = ...
  fun isSuccess(): Boolean = ... // kotlin contract, implies value is A
  fun isFailure(): Boolean = ... // kotlin contract, implies value is E
  // etc...
}
Afterwards perhaps we can then leverage the kotlin type-safe builder to then create a DSL, similarly to how kotlinx
flow { }
and Arrow’s
either { }
can be constructed and composed. Here I’m drawing inspirations from Arrow:
interface ResultContext<E> {
  fun fail(failure: E): Nothing
  fun <B> Result<E, B>.valueOrFail(): B // this extension name is hypothetical, in Arrow it's called bind()
}

inline fun <E, A> result(@BuilderInference fn: ResultContext<E>.() -> A): Result<E, A> = ...

// in the caller's code
suspend fun getManagedUser(id: UserId): Result<Fail, ManagedUser> = result {
  val user = getUser(id).toResult { Fail.UserNotFound }.valueOrFail()
  val managedStatus = getManagedStatus(user)
  
  if (!managedStatus.isManaged) {
    fail(Fail.UserIsNotManaged)
  }
  
  ManagedUser(user, managedStatus)
}
Having this type will solve a lot of problems and possibly alleviate some confusion on the topic of error handling. I want to understand more on where is the language heading towards? On this regards, I 100% agree to this.
Personally, I don’t think the language has to make an official recommendation. on Arrow, FP, or any OSS library / framework. is not a one-fits-all solution. I.e. Spring vs Ktor vs Quarkus vs Vert.X vs ... all are great candidates in Kotlin.
The reason why I mentioned recommendation is perhaps because in my view error handling is a common principle of software development lifecycle. One can choose how to handle errors which is agnostic to frameworks, but adhere to best practices and principles. In my opinion Arrow provides good example for that and some patterns (e.g. the
either { }
builder) may need to be brought into attention of a broader audience. i.e. it’s not recommending a library per se but recommending the approach taken to be aligned with idiomatic kotlin. If I were to draw some parallels between languages which employs a similar principle around exceptions. Rust has this principle, and also a solution. It provides a language level solution with
Result<T, E>
which is either a
Ok<T>
or
Err<E>
- a typed error channel. This allows segregation of domain-level errors
E
and encourage safe programming with values, translating and handling errors between boundaries - i.e. each boundaries would have different
E
types. Kotlin in other hand has the right principles but lack clear guidance or language solution. Would be great to get some alignment on the solution might look like. I won’t be surprised if we find many users may have tried reinventing or have reinvented this wheel multiple times… This is where I believe we can benefit from a slightly clearer guidance.
e

ephemient

04/03/2023, 4:14 AM
Error-handling is not a solved problem in any programming language. Even Rust, which has a standard
Result<T, E>
type used widely throughout its ecosystem, has issues. There is a wide divergence between library authors who want to provide actionable and recoverable errors (leading to solutions like thiserror to help reduce the boilerplate required), and application authors who want manage many different errors without having to wrap all of their dependencies' error types into a common hierarchy (leading to other solutions like anyhow - from the same author!).
In my work, we have a whole collection of different result types for different parts of our application, along the lines of
NetworkResult<out T>
 |- Success<out T>(value: T) : NetworkResult<T>
 \- Error : NetworkResult<Nothing>
     |- AuthenticationError : Error
     |- ClientError : Error
     |- TransportError : Error
     \- ServerError : Error
CacheResult<out T>
 |- Success<out T>
 |   |- FromNetwork<out T>(value: T) : Success<T>
 |   |- FromDisk<out T>(value: T) : Success<T>
 |   \- FromMemory<out T>(value: T) : Success<T>
 \- Error : CacheResult<Nothing>
     \- ...
DialogResult<out T>
 |- Positive<out T>(value: T) : DialogResult<T>
 |- Neutral : DialogResult<Nothing>
 \- Negative : DialogResult<Nothing>
etc., plus a boatload of
FooParseResult
for different types of
Foo
. It just made sense, coming from our pre-Kotlin experiences. Don't use exceptions for anything recoverable, and the modeling is dependent on context. Which I think ends up being pretty similar to elizarov's article? (It hadn't been published yet at the time we were laying out this design.)
That doesn't lead itself to anything that really fits in the standard library itself, though. So I think that's why we don't have that.
m

mitch

04/03/2023, 6:13 AM
@ephemient Thank you for giving a very concrete example with the Error tree in the application! Much appreciated. Yeah I agree that each application will inevitably define a particular way which fits the most to its needs. I’d like to point to an interesting observation though. Isn’t it then possible for the
XyzResult
type in that case be modelled as either a failure or success with generic type? i.e. • wouldn’t
NetworkResult<out T>
would then be
Result<NetworkError, NetworkValue<T>>
? • wouldn’t
CacheResult<out T>
be
Result<CacheFailure, CacheSuccess<T>>
? • and
DialogResult<T>
is
Result<DialogFailure, Dialog<T>>
The reason why I point this out is because all of a sudden because result is a provided type, we can leverage kotlin’s very own type-safe builder to compose this ergonomically and handle the error types safely between domain boundaries. consider
interface ResultContext<E> {
  fun fail(failure: E): Nothing
  fun <B> Result<E, B>.valueOrFail(): B // this extension name is hypothetical, in Arrow it's called bind()
}

inline fun <E, A> result(@BuilderInference fn: ResultContext<E>.() -> A): Result<E, A> = ...
then we can write something like below where translation between domain boundaries can happen organically
suspend fun getPositiveDialog(id: DialogId): Result<GetPositiveDialogFailure, CustomDialog> = result {
  val dialogData: DialogData? = getDialogDataFromCacheById(id).map { it.value }
    .recover { getDialogDataFromNetworkById(id).map { it.value } } 
    .mapFailure { failure -> /* map the getDialog failure to GetPositiveDialogFailure */ }
    .valueOrFail()
  
  ensureNotNull(dialogData) {
    GetPositiveDialogFailure.DialogNotFound
  }
  
  // kotlin contract, dialogData is no longer null, update cache async
  cacheUpdaterCtx.launch { updateCache(dialogData) }
  
  val dialog: Dialog<CustomDialog> = extractDialog(dialogData)
    .let { convertDialog<CustomDialog>(it) }
    ?: fail(GetPositiveDialogFailure.DialogConversionFailure)
  
  when (dialog) {
    is Positive -> dialog.value // CustomDialog
    is Neutral -> GetPositiveDialogFailure.NeutralDialog
    is Negative -> GetPositiveDialogFailure.NegativeDialog
  }
}
e

ephemient

04/03/2023, 6:17 AM
yes, and would be pretty similar to Rust: you have a standard
Result
type, but every module has its own
Error
hierarchy. it does mean you get standard
map
functions etc. for the outer
Result
(ditto Arrow's
Either
) but it doesn't help at all with converting the errors between domains. our codebase has custom mappers going up the chain, and those would still remain even with a typed
Result
or
Either
.
m

mitch

04/03/2023, 6:22 AM
Ah yes. the conversions between domain types is going to be inevitable and is definitely a drawback / benefit depending on how you look at it when one has to deal with types that crosses multiple boundaries. I guess an analog in the previous java life would be catching an exception and rethrowing a different one
e

ephemient

04/03/2023, 6:24 AM
if we had union types,
Result<T, E1 | E2 | E3 ...>
then it would be like Java's checked exception propagation (and actually Rust kind of allows you to do that, where
?
will automatically
.into()
if possible) but there's a lot of other impacts to the language with that
among other things, I have no idea what interop with Java would look like for that…
m

mitch

04/03/2023, 6:30 AM
If I were to hypothesize the interop to java will be exactly like how other generic types would be treated. Considering if this type is the one in question:
value class Result<E, A> private constructor(val value: E | A) { 
  fun valueOrNull(): A? = ...
  fun isSuccess(): Boolean = ... // kotlin contract, implies value is A
  fun isFailure(): Boolean = ... // kotlin contract, implies value is E
  // etc...
}
then yeah java code can just call
result.valueOrNull()
as pretty much how it does it with java very own
Optional<T>
Taking a step back. It’s actually this conversation that I’m after. Notice how we finally are both talking about domain boundaries and how errors should be translated, by whom, where, and when. Not only that - we can compose complex control flows of programs by leveraging kotlin language with
result { }
builder. This is what I’m after of this conversation. Having some type that has a typed error channel gives this.
having said that - the fact that domain boundaries exist means it’s only natural that failures in one domain to be handled / translated between them. So although yes it might feel like it’s a drawback, it is actually not when you look at it from a system perspective. Some failures in one domain may not have the same meaning in another domain and therefore the translation will be different. This is where I believe Kotlin is best positioned to answer this, because it has all the right constructs. It has suspend function & continuation, it has sealed types, it has type-safe builder, all that’s left of that is the type that enables that. That is:
Result<E, A>
I guess I need to give proper kudos to @simon.vergauwen and arrow maintainers for introducing the concept of builders for options/eithers/result. As a user I can’t emphasize on how inspiring and mindblowing this is. I have to be honest, this syntax is amazing, it allows writing flat program with very minimal nesting. This program looks like it’s using guard clauses through early returns, but actually under the hood it can use coroutines cancellations. A “short-circuit” event then not does it return early, it also cancels all the coroutines in the scope. I really think that something like this needs to be considered by the Kotlin language.
suspend fun getManagedUser(id: UserId): Result<Fail, ManagedUser> = result {
  val user = getUser(id).toResult { Fail.UserNotFound }.valueOrFail()
  val managedStatus = getManagedStatus(user)
  
  if (!managedStatus.isManaged) {
    fail(Fail.UserIsNotManaged)
  }

  // ... insert more rules here
  
  ManagedUser(user, managedStatus)
}
Thanks @simon.vergauwen! I really like how the pragmatic approach that Arrow take.. I guess perhaps a silly question from me 😓 I just want to know what is considered idiomatic in kotlin error handling. Where is Arrow in this rather ambiguous error handling landscape, would you consider it idiomatic or would it not? At this point I don’t see Arrow as an FP library anymore, with everything around heavyweight FP concepts such as Semigroup and traverse / sequence being deprecated & removed in favour of a friendlier DSL concepts. I think it’s fair to say that it’s converging towards… just plain Kotlin and helpers to do the right things.
s

simon.vergauwen

04/03/2023, 7:49 AM
I'm very glad to hear, since that was exactly our goal! I don't consider it very FP-ish either, as I mentioned in my first comment. It just provides some abstractions, and DSLs for dealing with typed errors. Similarly it offers DSLs for resource safety, saga pattern, etc rather than using heavy FP concepts with transformers, typeclasess, etc. Like I said, I am very biased but I consider the APIs and DSLs we're not proposing to be idiomatic. Everything has taken inspiration from either Kotlin Std & KotlinX in addition to community feedback.
m

mitch

04/03/2023, 8:04 AM
That’s good to hear. I do see parallels in kotlinx and stdlib especially with
mono { }
or
flow { }
builders. So I do believe the way errors are handled with types and values instead of exception to be idiomatic. In which case.. there is a definite gap in the language about a missing
Result<E, A>
type. Yes Arrow did fill that gap, but I think this might be because it’s a systemic problem.. I’d like to understand better the reason why isn’t this type available in the kotlin library. Surely there is a reason behind it and it has probably being considered as I do find articles and comments online and even in the blog itself that asks for this. If this type is introduced to the standard library or as part of opt-in package in kotlinx.typesafe. That would solidify this and alleviate all the ambiguity on the best practice on error handling and how that should be done in Kotlin.
j

Joffrey

04/03/2023, 12:44 PM
I believe there are other production applications like ours for which exceptions are too risky to use
Exceptions are present on the JVM, and even if you never throw any, you always have to remember that anything could throw an exception anywhere. So there is no (sane) way to completely get rid of exceptions. In general people setup exception handling at a high level (usually in the main framework they're using) to deal with unhandled exceptions, and avoid app death where necessary. Of course this doesn't mean we should use exceptions everywhere for error handling. As @ephemient pointed out, you may represent business-level errors with sealed classes (or nullability for the simplest cases). From what I have gathered, Roman's blog post is describing the idiomatic way of handling errors in Kotlin at most levels. I recommend also reading the initial KEEP for the design of
kotlin.Result
, which describes the intended style of Kotlin as well.
Note that this ☝️ doesn't invalidate the use of railway programming in general. I am just pointing out that language-wise, Kotlin will probably not add facilities to help with railway-oriented programming because it favors a more direct style by default. So I'm assuming the other style(s) will rather be supported by external libraries like Arrow
m

mitch

04/03/2023, 9:40 PM
I see, noted about that I wasn’t aware that style has a term! Thanks for giving that enlightenment. Out of curiosity. Hypothetically if say we pivot to the direct approach, how would you recommend me to approach error handling in direct-style programming? Say I have this toy function to bake a cake, how would we rewrite this into a direct style?
suspend fun bakeCake(...): Outcome<Fail, Cake> =
  outcome { 
    val butter: Butter = buyButter(...).valueOrFail()
    val carrot: Carrot? = buyCarrot(...).valueOrFail()
    val sugar: Sugar = buySugar(...).valueOrFail()
    val flour: Flour = buyFlour(...).valueOrFail()

    carrot?.let { carrot -> 
      ensure (carrot.isFresh()) {
        Fail.StaleCarrot
      }
    }

    bakeCarrotCake(recipe, carrot, sugar, butter, flour).valueOrFail()
  }
s

simon.vergauwen

04/04/2023, 9:33 AM
https://youtrack.jetbrains.com/issue/KT-186/Support-pattern-matching-with-complex-patterns#focus=Comments-27-7071428.0-0 Maybe something interesting to share here. Roman mentions here that Kotlin favors undeep ADTs. So if you prefer working with railway-oriented programming as @Joffrey mentioned I think the new style Arrow is already promoting that and context receivers will help exactly with that. Having to nest "success" into a layer of introduces more nesting. Including, or combining multiple of these types becomes increasingly problematic since it either requires nesting them in each-other, or redefining and duplicating entire hierarchies to combine/flatten them. This can be completely avoided with the style Arrow offers. Since you can horizontally compose errors, and doesn't require wrapping "success". As I mentioned above:
fun Raise<String>.one(): Int = 1
vs
fun one(): Either<String, Int> = 1.right()
. There is no need for
bind
or
valueOrFail
as shown in the example from @mitch. This sadly currently occupies the receiver, but with context receivers that is resolved.
context(Raise<UserAlreadyExists>)
fun User.save(): Unit = ...

context(Raise<UpdateRejected>)
fun Remote.createProfile(
  user: User,
  update: Update
): Unit = ...

context(
  Raise<UserAlreadyExists>,
  Raise<UpdateRejected>
)
fun serviceMethod(...): Unit {
  ...
  user.save()
  ...
  remote.createProfile(user, ...)
  ...
}
This supports all patterns of railway programming, still supports the style of coding that Mitchell shared above, and is more in line with more modern functional languages are evolving towards. It completely eliminates the need for
flatMap
or more complex patterns such as monad transformers, traverse, etc. It's also future proof with other potential upcoming features, such as union types:
context(
  Raise<UserAlreadyExists | UpdateRejected>
)
fun serviceMethod(...): Unit
As I mentioned, IMO nothing is a one-fits-all solution, that is something I definitely learned as having worked half my career as a fanatic OOP Java developer. I guess that also seems to be the conclusion of this discussion. I think both styles fit well in Kotlin, and I am proud of the improvements we made in Arrow.
m

mitch

04/05/2023, 11:34 AM
I need my mindblown emoji 🤯 :mind-blown:
s

simon.vergauwen

04/05/2023, 11:35 AM
My pleasure @mitch ☺️ You can find more details, and examples on the new website as well. And of course as always, more than happy to discuss more in detail in #arrow as well 😉
e

elizarov

04/05/2023, 12:06 PM
Let me add that we are explicitly keeping this use-case in mind when we design context receivers. Long story short,
context(Raise<UserAlreadyExists>)
is a kind of coeffect — a “capability to raise an error” that the function requires from its context. Coeffects are totally equivalent to effects, that are traditionally used in functional languages to model exceptions and things like this. However, coeffects (contexts) are more ergonomic and fit way better into Kotlin programming style.