Overloadable question `?` and elvis `?:` operators...
# language-proposals
h
Overloadable question
?
and elvis
?:
operators? This is already kinda part of the rich error proposal, but it only works for error unions there, but could also just be useful on its own. It's essentially just syntax sugar for a monad
map
. Basic example (not sold on this syntax):
Copy code
sealed interface ParseResult<T> {
   data class Success(val value: T) : ParseResult<T>
   data class FormatError(val expected: String, val got: String) : ParseResult<Nothing>
   data class Failure(val cause: Exception) : ParseResult<Nothing>

   operator fun <R> question(operation: T.() -> R): ParseResult<R> =
      if (this is Success) this.value.operation() else this
   
   operator fun <R : T> elvis(operation: () -> R): T = 
      when (this) {
         Success -> this.value
         else -> operation()
      }  
}

fun Foo.Companion.parse(str: String): ParseResult<Foo> = //...

fun example() {
    val foo: Foo = Foo.parse("hello world")?.doSomething() ?: Foo.None
}
I don't really like how the elvis overload works here, but I think it gets the idea across.
y
This looks better in Rich Errors syntax though:
Copy code
error data class FormatError(val expected: String, val got: String)
error data class Failure(val cause: Exception)
typealias ParseResult<T> = T | FormatError | Failure
fun Foo.Companion.parse(str: String): ParseResult<Foo>
fun example() {
  val foo: Foo = Foo.parse("hello world")?.doSomething() ?: Foo.None
}
h
You may be right, it does look better there. But if rich errors don't end up being added as a feature I think this would be a good alternative. It feels less invasive to add another operator overload than to create a new
error
classifier.
y
(categorised) Unions are a more "limited" feature in some sense though, or at least less breaking of Kotlin's principles.
h
Yes, they are more limited, but in some ways that's not desirable. I tend to be a fan of features that feel like they don't add any special rules or handling, and instead are just an extension of existing rules. Making safe calls specifically handle unions feels like a new feature, and not an extension of existing features. If
?
overloading was added, then all null-safe calls could be made into a simple extension function included in the prelude like so:
Copy code
operator fun <T : Any, R> T?.question(operation: T.() -> R): R? = 
   if (this == null) null else operation(this)
Compiler optimizations could be added for it, of course, but by making this an overload suddenly safe calls aren't a special case for unions, but instead just a normal operation.
y
Similary, defining
null
as an
error object
does the same in the case of Rich Errors. The idea is extending
null
to be able to have info inside.
h
I guess that's also true, but if it truly were being treated like an error object then nullable types should be written as
T | null
. But then I guess you could just say
typealias T? = T | null
. Regardless, yes I think rich errors lead to a nicer syntax, but like I was saying if they don't get added this is a nice alternative imo.
1
y
We're fully agreed on that! I think Rich Errors also have a strong theoretical foundation (like in terms of how they work on the type system) and so they feel like a better feature to include vs just an operator overload. It also escapes the issue of Monad ordering (e.g. if you have
Result<ParseError<Foo>>
vs
ParseError<Result<Foo>>
handling it will be different, even though they're really the same thing, but this doesn't happen with unions since they're commutative)
👍 1