I know there's zero chance of it happening, becaus...
# language-evolution
r
I know there's zero chance of it happening, because Kotlin has deliberately decided to favour exceptions over Either for error handling, but I wish we could have a
bar*.foo()
dereference operator to mean
bar.map { it.foo() }
and possibly a
bar**.foo()
to mean
bar.flatMap { it.foo() }
, in the same way that we have
bar?.foo()
to mean
if (bar == null) null else bar.foo()
.
j
Why obfuscating code which is readable for any person with english knowledge with custom operators?
r
For the same reason we do it with
?.
- because it allows chaining & reduced nesting
j
Question mark is more intuitive in my opinion, and it allows chaining, but the other is just a shortcut, it is not replacing any if or when expression.
Copy code
val baz: Baz? = foo?.bar?.baz
Copy code
bar
   .map { it.foo }
   .filter { ... }

// or

bar
   .map(Bar::foo)
   .filter { ... }

// vs

bar*
   .foo()
   .filter { ... }
r
Question mark is more intuitive in my opinion
I suspect that's a case of the "syntax I am familiar with is more intuitive that syntax I have not personally encountered before" cognitive bias
j
The
*
operator already exists in Kotlin
r
It doesn't need to be
*
, though Groovy uses
*
in essentially this way
j
The question mark is solving a problem which leads to a lot of boilerplate (and not sure if even mutability). The other just remove a few chars
How would you write this without question marks
Copy code
val baz: Baz? = foo?.bar?.baz
I suspect that's a case of the "syntax I am familiar with is more intuitive that syntax I have not personally encountered before" cognitive bias
The nullability system in Kotlin is based on
?
, to indicate types, etc. It has a massive usage in a Kotlin basic feature. So it is intuitive to think
?
is related to nullability when it is seen as an operator. I don't similarly see the other.
r
In an exception throwing world where
foo
,
bar
and
baz
all throw exceptions I can write
foo().bar().baz()
. In an Either world I have to write
foo().flatMap { it.bar() }.flatMap { it.baz() }
, which I think introduces substantial cognitive overhead dealing with the nesting. I think
foo()**.bar()**.baz()
. leaves it clearer to the intent, to chain the happy path route. Anyway, not likely to happen, so not worth getting worked up about. Just a pain point to be accepted.
j
To be honest I don't think Kotlin lives in an exception-throwing world. It talks and has APIs for more use cases. https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07
w
In the
bar*.foo() == bar.map { it.foo() }
, what type is
bar
? Is it a collection or
Either
?
r
I'd be happy for it to be any functor, so a collection or an Either.
removed Apologies, completely misread your message
w
Context receivers actually can make the issue more bearable already 🙂. And even without context receivers, Arrow already has other convenience methods to get a
Raise
context, such as the
either
function.
2
j
Arrow
Raise
API was removing the need to
bind
indeed. I haven't more detail about this specific use case, cc @simon.vergauwen
c
Raise
is essentially to
.bind()
what
suspend
is to
.await()
. Just like coroutines remove all need to
.await()
after calling each function, Arrow's
Raise
removes the need to
.bind()
after calling each function, so you can just chain them normally.
The idea is that there is no need for a special new syntax, if there is nothing to write at all 🙂
j
So this part could be written directly with
foo.bar.baz
?
c
If
foo
,
bar
and
baz
are getters with a
Raise
context receiver, yes.
👀 1
Also, you don't need to take
Either<Ohno, Bar>
as argument. This function requires
Raise<Ohno>
. So it can only be called when the caller knows how to handle
Ohno
. For example:
Copy code
either<Ohno, Baz> {
    val foo: Either<Ohno, Bar> = …
    a(foo)
}
well, the user could just write
Copy code
either<Ohno, Baz> {
    val foo: Either<Ohno, Bar> = …
    a(foo.bind())
}
and now
a
only receives valid values
👌 1
In general, you want to think of it as: •
Raise
is the "world of computation": it's the function's type •
Either
is the "world of values": it's a representation of the result of a function that can fail Traditionally, we would write many functions that work with
Either
:
Copy code
suspend fun getUser(id: String): Either<NotFound, User> = …
suspend fun getName(user: User): Either<NotFound, String> = …
which can be combined with
flatMap
:
Copy code
getUser("123").flatMap { getName(it) }
Raise
works directly at the function level. There are no more special return types anymore:
Copy code
context(Raise<NotFound>) suspend fun getUser(id: String): User = …
context(Raise<NotFound>) suspend fun getName(user: User): String = …
because you cannot call a contextual Raise function when you don't have the error management in place, no explicit syntax is necessary:
Copy code
getName(getUser("123"))
.bind
is just the conversion function from
Either
to
Raise
. If you use
Raise
everywhere, you never need to convert, and thus you never need any special syntax.
1
s
Yes,
Raise
and context parameters might be what you're looking for. At least most maintainers have always seen them as the "redemption" of wrappers like
Either
, and operators like `map`/`flatMap`. If you get rid of those, there is actually very little functionality that you actually need.
raise
, and
recover
is all you need. All the rest is just regular function application. This is going to be one of the topics of my talk during KotlinConf ☺️
❤️ 3
Raise
is essentially to
.bind()
what
suspend
is to
.await()
.
❤️