@Ichoran First, let me thank you for taking this seriously and replying with a constructive criticism. It is clearly not trivial to do so given how controversial this topics is – as seen by other replies here – and I very much appreciate your way of conduct.
Although I disagree on some of the problems you listed, I do agree with the general notion that the proposal will introduce far too many new constructs and rules, serving only a particular use case.
I believe this use case is rather common, and that existing tools do not satisfy the need for a clearer code. I think I have another idea, though – less intrusive – which I will explain in a separate comment.
I did not, as I’m not familiar with Rust all that much. I did however examine it a bit now that you’ve mentioned it, and I agree that is quite a neat feature (so thanks for the reference!).
I still believe it is not sufficient – for two reasons – but then again I’m not entirely familiar with this feature, so correct me if I’m wrong.
The first reason being is that this feature is not entirely compatible with higher-order functions. For the sake of demonstration, let’s imagine that this operator exists in Scala:
def foo(): Result[Int, Error] = {
unsafe()? + 2
// is equivalent to
val i = unsafe() match {
case Ok(i) => i
case Err(e) => return Err(e)
}
Ok(i + 2)
}
def unsafe(): Result[Int, Error] = ???
This surely reduces a lot of boilerplate, and it might b e worth having this feature – perhaps via macros? – regardless of error handling.
When we consider code with higher order functions, though, this feature cannot help reducing the boilerplate the same way exceptions do:
def foo(): Result[Int, Error] = {
Seq(1,2,3).map(i => unsafe(i)?).sum
}
def unsafe(i: Int): Result[Int, Error] = ???
This example will not compile, as the anonymous function (unsafe?
) returns Result
of Int
s, and sum
is not defined on non-numeric items. It’s not necessarily a problem with the sum
method in particular, but a general problem of seamlessly “reducing” a container of Result
s to a singular one.
It might be possible to overcome this problem by making the the operator behave differently in anonymous functions. Instead of making the function return type a Result
, its return type should be the valid result, but it should still use return
for the invalid result (err), and would still require the outer function to return a Result
.
Say, if the compiler was smart enough to translate the above example to this:
def foo(): Result[Int, Error] = {
val is: Seq[Int] = Seq(1,2,3).map { i =>
unsafe(i) match {
case Ok(i2) => i2
case Err(e) => return Err(e)
}
}
Ok(is.sum)
}
def unsafe(i: Int): Result[Int, Error] = ???
In which case the return
actually throws a NonLocalReturnControl
exception (if I’m not mistaken), and this functionality is said to be dropped in Dotty, so I’m not sure whether this is feasable. Also, I’m not entirely sure whether the relaxation of the “must return Result
” restriction for anonymous functions would not incur other problems.
The second problem with this mechanism is that it doesn’t solve the boilerplate of multiple levels of nested exceptions:
def compile(rawScript: String): Result[Script, Err] = {
val script: Result[Script, Err] = {
if (rawScript.isEmpty) return Err("script is empty")
val header = compilerHeader(rawScript)?
...
compose(header, body, ...)
}
script.expect(s"Failed to compile script: '$rawScript'")
}
def compilerHeader(rawScript: String): Result[Header, Err] = {
val rawHeader = rawScript.substring(0, 15)
val header: Result[Header, Err] = {
findBadWord(rawHeader).map { badWord =>
return Err(s"bad word found: '$badWord'")
}
... // other unsafe operations
}
header.expect(s"Failed to compile header: '$rawHeader'")
}
def findBadWord(s: String): Option[String] = ???
This code is a bit more cumbersome than the exception-based one; but more importantly, the explicit return
s will ignore some of the expect
messages so they won’t be included in the final result.
To clarify, I would expect an error message like so (could be reversed order):
bad word found: 'voldemort'
-- caused: Failed to compile header: 'voldemort rules'
-- caused: Failed to compile script: 'voldemort rules, not dumbledore'
Edit: To correct what I said earlier, nested return
s are not dropped in Dotty, but merely replaced with a non-keyword mechanism that is slightly more verbose (the underlying implementation still uses exceptions).
Also, it’s probably worth noting that exceptions are only intended for error handling in sequential code. Concurrent code has other solutions (coroutines, messages, etc), albeit construction of multiple levels of nested errors is problematic (this is due the nature of concurrent code).