Hi there. What are the current (and for 2.0) best ...
# arrow
j
Hi there. What are the current (and for 2.0) best practices for optionals? I know that documentation prefers the Kotlin’s nullables,
?.
and
?:
but still, I personally find Arrow’s
.tap {}
,
.map {}
etc more consistent with the rest of the code (eithers, validateds etc)
c
If you need nesting, use optionals. If you don't need nesting, prefer nullables in most cases.
s
We favor DSLs because it results in much more consistent and Kotlin idiomatic code. We support nullable types in all DSLs. The documentation explains the difference between two approaches, and the use-cases for them.
Quiver also exposes some more utilities that we’re deprecating in Arrow for those that prefer the fluent API style
m
I think there’s no really right or wrong answer or recommendation so to speak - it all depends on the problem space, preference, and tooling options that the team chose. For instance, in our company, some JVM devs that were using spring boot reactive and kotlin coroutines opted to use Option. This avoid a whole class of problems including accidental
Mono.empty
which happens when a null value mistakenly propagated through a reactive channel. Those are hard to debug. However, other teams have also shown that nullables can also be used with extreme caution. Some of the team had experienced unexpected behaviour due to mistakenly propagated / interpreted null when dealing with dynamodb aws sdk 2.0.. That caused some unexplained latency spike. My team personally chose to using Option, rather consistently. The reason is because it allows us to not think of any possibility of mistakenly propagated / interpreted null. It makes it easier for us to write confidently, without having to worry about integrations between java libs (think opensaml, aws sdk, spring) or nested nullability.. However, it needs to be noted that we also have different teams that went full throttle nulls and they were ok with it. Another convenience that I found good about option is because I can create various extension functions on them because they are not synthetic types.
the good thing about arrow is that regardless of our choice, the
option { }
and
nullable { }
dsl allows us to write virtually identical program.
s
Yes, there is no right or wrong as Mitchell says. I agree that team preference; and consistency is most important.
f
late to the party but there's some discussions on the subject here too https://github.com/arrow-kt/arrow-core/issues/114
j
Thank you! Honestly, there's a lot of valid and convincing arguments both for nullables and for Option, and they are great to know to make better informed decisions when coding.
f
my 2 cents: kotlin should never have added nullable types, they should have just enforced non-null, period, and provided a plain old regular Maybe type in the library out of the box 😛 likewise they should have just provided a plain old regular Either type instead of their confusing advocacy of exceptions for some things, return null for others, return sealed error types for yet other things, and sometimes Result buuuuut here we are 🙃
c
@Fred Friis I disagree quite strongly 😅
null
is very important for users migrating from Java codebases, and an
Option
type is quite expensive on the JVM when used on a large scale (that's why
Optional
is not recommended to use anywhere other than function return types). Kotlin's
null
handling has been, and remains, the gateway drug for most of the Java community, and I doubt Kotlin would be so popular today if it didn't start like this.
For exception handling, Arrow is great but it's still quite opinionated and a bit of work to learn (though it's getting much better than it once was). I assume that's the main reason why Arrow Core is the part of the standard library. The language designers know that there is no official Kotlin construct for error handling, you can see them studying the subject on some YouTrack issues (e.g. the one about a language feature to replace function duplicates with
orNull
variants).
f
I used to think that too, but with time I've changed my mind a bit, it may change again haha Kotlin already out of the box did lots of things quite different to Java, after all, that's the whole point, right? If a language that adheres as much as possible to Java was desired, we can just code in Java (or groovy, or some other failed JVM language that isn't meaningfully different to Java) I just don't think not allowing null out of the box period is such a radical idea as many people portray it an alternative would be to enforce non-null period as the default but have an escape hatch compiler argument or something I also don't buy the performance argument, if the choice had been to have Maybe/Either out of the box, they would have made sure to make it performant (possibly by using nulls internally in the compiler, but not exposing it to users, just like some functional libraries do with their immutable collections - mutate them behind the scenes but present them as immutable) and the fact that Kotlin devs themselves don't seem to be able to decide how their language should do error handling in general is... well. IMO error handling is a solved problem, functional error handling is winning in every language you look, so I don't understand why languages like Kotlin, python3 etc etc insist on continuing the failed experiment of exception based error handling (which is MORE complex and requires MORE syntax built into the language than just returning an Either) it could be that when we learn to program, we learn that methods/functions return what they say they do... and that's all. no nulls, no ?, no exception rug pulls, no homemade non-monadic maybe/either lookalike sealed classes, and all the things that can be mapped over inherit from functor, flatmapped from monad etc etc rather than accidental ?.let{} syntax here, a lone method map (that doesn't come from an interface) there just
Copy code
fun getUser(userId: UserId) : Maybe<User>
or
Copy code
fun getUser(userId: UserId) : Either<Error, User>
c
but have an escape hatch compiler argument or something
That's a very bad idea. If you do this, you get a fractured language where the rules are different for each codebase. Or you get massive codebases where one day in the past someone relaxed the null safety checks and now we can never enable them again because the entire codebase is incorrect—but hey, it works, right? (that's the state of the TS codebase at my day job)
f
agreed, so my preference is to just enforce non-null, period. i don't agree that that's a radical or difficult idea. it's the only sane way to actually fix the billion dollar mistake imo (like Haskell and other language that don't have nulls, which admittedly are more esoteric, but not necessarily because of that one quality)
c
possibly by using nulls internally in the compiler, but not exposing it to users
But that doesn't work if you want to nest them. So now, you either have to always box, or forbid nesting nullable values, which is just
null
with extra syntax. If you choose the first option, changing the nullability information of a field is a binary-incompatible change for all containers of that type, and a source-incompatible change for Java interop. If Java didn't autobox/autounbox
Integer
automatically, I don't think the Kotlin team would have fused them into a single type in Kotlin, because it would have made the language to complicated to use for interop.
Kotlin has grown a lot since its inception, especially with coroutines, but we must remember: when it first started, it was an unwavering requirement to have perfect interoperability with Java. That's one of the reasons Kotlin gained popularity on other JVM languages, like Scala: it wasn't scary for Java devs.
f
🤷 tbh, I don't care how it's done internally, the point is I don't buy that languages that enforce non-null period and instead provides Maybe and Either out of the box must inherently have slow compilers or whatever, I'm sure that if that's the choice from the get go, people will figure out solutions to make the language just as performant as languages that have null
c
and the fact that Kotlin devs themselves don't seem to be able to decide how their language should do error handling in general is... well.
Yeah, I agree. So far, the idiomatic way is: • exceptions should only be used for things that definitely should not happen, and are not recoverable automatically. • except I/O stuff, which is allowed to use exceptions because explicitly handling all cases is too verbose • domain errors should use sealed class hierarchies to communicate failures. Officially, the recommended solution is to create them yourself. I prefer using Arrow. • absolutely NEVER use Result for error management, and burn all code that does.
f
Scala did the worst of both worlds by having incredibly dense syntax, lots of footguns everywhere, having these types out of the box, but also not having null safety lol
c
must inherently have slow compilers or whatever
Actually, that's not the only solution. Languages like Ocaml or Rust handle this very performantly. But they can not interop with a language that doesn't do it exactly the same way as they do. That's not an option for Kotlin, which had to interop with Java.
Rust is helped a lot here by their decision not to have any binary compatibility, ever. If they decide to change how it works, they just… change it. But again, that's only because they have absolutely no interop rules with anything else. On the JVM, you can't do that.
f
I don't buy that either Kotlins "interop" with java in the case with null is that they're String!, ie neither String nor String? might as well have gone with Maybe<String> for such calls, I don't see the problem but we're not going to agree lol though I enjoy talking about this everyone has their own ideas and I might just have to go and make my own language to get what I want (which I won't lol)
c
and requires MORE syntax built into the language than just returning an Either
Yet, even Arrow is moving away from returning Either, using context receivers instead. At large, devs really hate wrapper types.
Interop is both ways: you should be able to write a Kotlin function that is used by a Java dev that doesn't know Kotlin.
!
is a workaround the Kotlin team came up with to avoid forcing developers to use additional syntax to use the value when it's unknown whether it's safe or not. If they went with
Maybe
, you would be forced to handle the errors, which would make interop more complicated. It's also single-way; you can't write a Kotlin function that creates such a value. But again, what is
Maybe
? Is it a class? Then, it can't work, because you can't expose nullables to Java anymore. Is it syntax sugar for a nullable reference? Sure, but then you have all the downsides of the nullable values we have, with an added wrapper type.
f
java callers could just use maybe.getOrNull or whatever and i don't think this has turned out to be a big thing in real life - how many java shops call kotlin libraries? it's overwhelmingly the other way around
c
There are a lot of mixed codebases. Though I agree it's falling out of fashion nowadays that people are less worried of Kotlin
s
I wrote a section for this on the website as well, https://arrow-kt.io/learn/typed-errors/nullable-and-option/
m
@Fred Friis I recently asked in #language-proposals on error handling and Kotlin direction. Perhaps this discussion can be relevant: • Arrow 2.x + context receivers: https://kotlinlang.slack.com/archives/C0B9K7EP2/p1680694416774339?thread_ts=1680388003.721149&amp;cid=C0B9K7EP2 • Roman Elizarov’s comment on that https://kotlinlang.slack.com/archives/C0B9K7EP2/p1680696378754839?thread_ts=1680388003.721149&amp;cid=C0B9K7EP2 I also wrote a related medium article, analyzing the cognitive complexity of various error handling approaches. https://betterprogramming.pub/typed-error-handling-in-kotlin-11ff25882880.
It looks extremely promising but context receivers is still far away, maybe a year or two if not more..?
s
It looks extremely promising but context receivers is still far away, maybe a year or two if not more..?
That's really hard to say. Kotlin 1.9.0 is rolling out, so 2.0 is expected at the end-of-this year. So I am hoping for context receivers next year, but I mean it depends what you want out of them. Completely stable, or are you fine with experimentally multiplatform, or perhaps
@OptIn
🤷 Completely stable is probably some time away, but I doubt most people will wait for that if it's relatively safe to
OptIn
. I mean everyone was using "experimental" Kotlin Duration as well.
c
Personally, it's whenever they're binary-stable. I don't care if the syntax changes as long as it doesn't break already-compiled code (like contracts, which will be source-stable… one day 😅). I assume most library authors are the same
(w.r.t contracts: my understanding is their final syntax will be very similar to context receivers, so they're probably next in line)
s
Well... writing a library is very different from an application 😅 I was speaking in the context of an application which is 95% (ore more) of the developers out there
c
I feel like applications care more about source compatibility 🤔 More effort to rewrite it later if it turns out different
s
mRight, but I don't expect context receivers to drastically change in source compatibility. Especially after they'd land in let's say 2.1 as multiplatform, unless there is a real industry need or big problem after them becoming more widespread used.
f
@CLOVIS @mitch thanks both of you for interesting and friendly discussion and material 🙂