Pre-SIP: Checked Exceptions

This is a pre-SIP for checked exceptions into Scala; i.e, having the ability to make the compiler emit errors when checked exceptions are not handled.

Motivation

A lot has already been said about checked exceptions over the years. There are many reasons for and against checked exceptions.

In Scala we have Try, Either and Option to deal with exceptional situations, but sometimes exceptions could produce clearer code. Some may refute that such code is clearer, or argue that it incurs too much penalty or violates some theoretical principle, but I believe this is a matter of opinion – where there is no clear verdict – and a matter of proper and educated usage.

What I love about Scala is its combination of features and ideas from (seemingly) opposite paradigms, often providing more than one way to model the code. Scala is less opinionated, providing less idiomatic solutions, and lets the developers use the tools of their choice. I view checked exceptions as one such tool, and so I would like to have them in the language.

There has been an attempt to introduce checked exceptions via a compiler plugin, but it is (a) outdated and (b) doesn’t support the full capabilities that exceptions require, which will be described in this proposal.

Goals

The goals of this discussion are:

  1. Identifying technical problems.
  2. Suggesting improved designs.
  3. Expressing opinion regarding specific features.

The following are not the goals of this discussion:

  1. Arguing over the motivation behind checked exceptions; the motivation should be discussed only when it’s relevant to a certain technical decision.

  2. Planning the introduction of the features in this proposal in the language. It may be worth mentioning technical migration concerns, but I believe that overall we should consider this proposal as a major feature that will require some experimentation, and will likely be introduced with a compiler-flag.

Syntax & Features

The first new addition is the throws keyword, which replaces the @throws annotation and behaves just as it does in Java:

def foo(bar: Int): Int throws Exception = ???

def bar(): Unit = {
  foo(1) // compiler error (need to handle exception)
}

^ - the exception character

In order to reduce verboseness and keep a method’s signature mostly keyword-free, a caret (^) may be used instead of throws:

def foo(bar: Int): Int ^ Exception = ???

The caret is a good candidate for an “exception character”, and for these reasons:

  1. It’s simple, appearing in the ASCII table and easily available on QWERTY keyboards.
  2. It’s not already used in Scala as a special character.
  3. It symbolizes “up”, corresponding to the general notion of exceptions being “thrown up” or “raised”.

Function types

Types of functions that throw exceptions can be expressed with a caret:

type ExceptionalF = Int => Int ^ Exception

def foo(bar: Int): Int ^ Exception = ???
val foo2: ExceptionalF = foo

def bar(exceptional: ExceptionalF) = ???
bar(foo)

Similarly to the return type, a function’s exception type is covariant.

Functions that do not throw any exception can still be assigned to an exceptional function type, as they can be thought of as throwing Nothing:

val f: ExceptionalF = (i) => i + 1

def baz(i: Int): Int = ???
// is equivalent to
def baz(i: Int): Int ^ Nothing = ???
// and can be assigned to
val f: ExceptionalF = baz

Multiple exceptions

A function that may throw multiple exception types can express this using dotty’s union types:

def foo(bar: Int): Int throws ExceptionA | ExceptionB = ???
def foo(bar: Int): Int ^ ExceptionA | ExceptionB = ???
type ExceptionalF = Int => Int ^ ExceptionA | ExceptionB

Generic exceptions

Since functions are first-class citizens, there is a need to be able to bind exception types to those of other functions. We could use generic types encoding for this purpose:

trait Container[A] {
  def map[B, E](f: A => B ^ E): Container[B] ^ E = ???
}

In this example, the exception type of the map function is determined by the exception type of f, similarly to how the return type does. This means that map doesn’t need to handle exceptions thrown by f.

Anchored exceptions

Checked exceptions that are propagated through a large stack only to be handled at the beginning of it may produce code which is harder to maintain, since every function in that stack is required to declare the exception. This becomes a real burden to refactor such stack when the original exception type changes – all of the functions’ signatures on the stack need to change as well.

One solution to this problem can be attempted via a new mechanism called “anchored exceptions”. The term is borrowed from this article (by Marko van Dooren and Eric Steegmans), which describes the problem at length and suggests a solution with a less-than-optimal syntax, as it is extremely verbose.

I would like to propose a more concise syntax which is also tailored to Scala. This syntax is far from finalized, and I haven’t experimented with all the potential cases, but I believe this is the right direction in coming up with a new and useful syntax.

Here is a simple demonstration of this capability:

def read(path: String): String ^ IOException = ???

def print(path: String): Unit ^ AE = {
  val content = read(path) ^ AE
  Console.print(content)
}

In this example, the exception type of print is anchored to that of read via the AE identifier, which means two things:

  1. The exception type of print will change along with the exception type of read.
  2. The compiler will not emit an error in print, as the exception of read is declared for print as well.

Anchored exception types may also be combined with non-anchored ones:

def print(path: String): Unit ^ AE | ExceptionB = {
  if (path.size == 0) throw new ExceptionB
  val content = read(path) ^AE
  Console.print(content)
}

Or with generic exception types:

def mapAndValidate[E](f: A => B ^ E): B ^ E | ValidationE = {
  val b = f(a)
  validate(b) ^ ValidationE
  b
}

In fact, it may be possible to encode a generic exception propagation using anchored exceptions and the ^=> operator:

trait Container[A] {
  def map[B](f: A ^=> B): Container[B] ^ f = ???
  // equivalent to:
  def map[B, E](f: A => B ^ E): Container[B] ^ E = ???
}

Currying

Multi parameter lists (currying) support an exception type only after the last list:

// valid:
def foo(bar: Int)(baz: String) ^ Exception = ???

// invalid:
def foo(bar: Int) ^ Exception (baz: String) = ???

The main reasoning for disallowing exception types in the middle of a curried signature is due to the confusing and somewhat irregular syntax it produces.

Custom exceptions

Creating custom exceptions is considered a good practice, but it is rather cumbersome to implement in Scala. It could be useful to introduce a new way of defining custom exceptions that reduces the boilerplate while maintaining these abilities:

  1. Un-applying the custom exception (extractor object).
  2. Properly handling construction with null message / cause.
  3. Construction with or without a stacktrace.
  4. Enabling suppression.

Here is one potential way of encoding custom exceptions:

// definition
exception ParseException(line: Int, reason: String) {
  def message: String = s"$line: $reason"
  override def checked = false
}

// construction (throw is not required)
throw ParseException(22, "invalid character")
throw ParseException(22, "invalid character", cause = e)
throw ParseException(22, "invalid character", trace = false)

// pattern matching
e match {
  case ParseException(line, reason, message, cause) => ???
}

There are obviously other potential ways of expressing this and many small variations that could be made to this one; this is just the general idea.

Exception wrapping

Custom exceptions are often used in order to wrap another exception and add more context to the stack trace:

try {
  unsafeOperation
} catch {
  case NonFatal(e) => throw ParseException(22, "invalid character", cause = e)
}

However, this syntax is quite verbose; perhaps it’s worth introducing a more concise syntax for this use-case:

unsafeOperation ^ e => ParseException(22, "invalid character", cause = e)

I’m not sure whether combining this syntax with the syntax of anchored exceptions is the right choice or not.

Pretty stack traces

One last thing that might be worth adding to the standard SDK is a “prettier” representation of stacktraces – or rather, a more readable one – like in this Scastie example (but obviously more efficient).

Problematic cases

There are a few cases in which it may be problematic to have a checked exceptions mechanism:

  1. Lazy evaluations (lazy val).
  2. Implicit definitions (implicit def).
  3. Constructors (body of class or object).
  4. Top-level code (instead of package objects).

It may be possible to overcome the difficulties in some of these scenarios – I didn’t put too much thought to it – though I suspect that there are some which are unavoidable. In such cases, perhaps it’s best to have the compiler warn on throwing a (checked) exception / invoking a method with a checked exception type.

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

10 Likes

In my personal experience checked exceptions introduce more problems rather than solve. I believe that exceptions should be used for exceptional cases only. Otherwise regular return values are more preferable.

3 Likes

Could you elaborate on these problems? Perhaps they could be solved with a better design of exceptions.

This proposal focuses on Exceptions. However, the general problem is how to properly handle effects in the language. If I remember correctly, this was researched a little by the dotty team, but is currently on the backseat in favor of completing Scala 3.x.

2 Likes

Interesting. Is there anything you can refer to, or was it mostly undocumented conversations?

I obviously don’t expect this to be implemented for the release of Scala 3; this is more of a “nice to have” feature.

I think java checked exceptions is rather inconvenient tool which has significant disadvantages.
There are many articles about it for example: http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/

The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it.

Actually checked exceptions do not require call stack they are just alternative values.

Scala has either type for that purpose. It is rather good. Of course I would prefer to have something more better.

  • It can be helpful to have compile time check which guarantee that all error cases are handled.
  • It would be better if error handling do not increase embedded code blocks

May be union types will be able to help.

def doSomeThing():  String | Error1| Error2| Error3 = ...

doSomething() match {
    case s: String  => ...
    case e: Error1 => ...
    case e: Error2 => ...
}

The open question is how to force a programmer to write “match”.

But I really do not want checked exceptions in scala :slight_smile:

2 Likes

I guess I do have to explain why exceptions can be very useful and are not so bad.

This article points a few drawbacks with exceptions. Some of those are indeed inherently problematic, but some are a result of bad and uneducated usage, and others are plain wrong.

Not problems

Argument: 1. Checked and runtime (unchecked) exceptions are functionally equivalent.

Counter: They are not. They do share some functionalities (call stack propagation and catch-finally-handling), but differ on another crucial functionality – whether they need to be declared or not (when unhandled).

This difference also signify their intended use; checked exceptions are for possible outcomes of a function that need to be considered (much like Try is), while runtime exceptions are for irregular outcomes that shouldn’t be considered.

Argument: 2. Checked exceptions cannot be “fixed”.

Counter: They are not expected to be fixed; they are expected to be acknowledged, just like any Either or Try result type.

Argument: 3. Incompatible with functional programming.

Counter: This may be true for Java, but Java is not FP. This proposal is for Scala, and it demonstrates how exceptions can be compatible with FP.

Argument: 4. They incur boilerplate on the signature of all the methods’ in the stack.

Counter: That is true for composition types (Either / Try) as well. Replace : RValue ^ E with : Either[E, RValue].

Inherent problems

Argument: 5. When the exception type of a method on the stack changes, all the other methods need to be changed manually.

With composition types, there’s the ability to use a generic exception type (in the left of the Either), so no change is required for the other methods.

This proposal addresses this problem with two suggestions: (a) generic exception declaration, and (b) anchored exception declaration.

Argument: 6. Wrapping lower-level exception with custom exceptions incur a lot of boilerplate (many catch clauses).

Again, this proposal addresses this problem and suggest a more concise syntax for this use case.

How exceptions are better than composition types?

They can, in some scenarios, produce clearer and more concise code. This is examined in this SO question which is also linked at the opening post, but I’ll provide a demonstration in here as well.

Imagine you have a method that needs to invoke several other methods and compose their results together to come up with its return value. One of the many examples for such a scenario is writing code for an (external) DSL compiler – the compiler needs to compile different parts of a script using different methods, while each method may fail, as the script may be invalid (bad user input).

Let’s examine this contrived example that uses Either to express the potential failures of every function, assuming we care about the “error value” and wish to use it later (perhaps to display information to the user). I’m going to assume that Either is right-biased.

trait Script { def execute(): Any }

def compile(rawScript: String): Either[CompilerException, Script] = {
  val emptyValidation = if (rawScript.isEmpty) Left(CompilerException("script can't be empty")) else Right(())
  
  for {
    _ <- emptyValidation
    header <- compileHeader(rawScript)
    footnotes <- compilerFootnotes(rawScript)
    body <- compileBody(rawScript, footnotes)
    script <- compose(header, body, footnotes)
  } yield script
}

def compileHeader(script: String): Either[CompilerException, Header] = ???

def compileBody(script: String, footnotes: Seq[Footnote]): Either[CompilerException, Body] = ???

def compileFootnotes(script: String): Either[CompilerException, Seq[Footnote]] = {
  val rawFootnotes: Seq[String] = ???
  val tryFootnotes: Seq[Either[CompilerException, Footnote]] = rawFootnotes.map(compileFootnote)
  reduce(tryFootnotes)
}

def compileFootnote(raw: String): Either[CompilerException, Footnote] = ???

def reduce[L, R](eithers: Seq[Either[L, R]]): Either[L, Iterable[R]] = ??? // utility code

def compose(header: Header, body: Body, footnotes: Seq[Footnote]): Either[CompilerException, Script] = ???

And now let’s imagine it with the proposed exceptions syntax:

trait Script { def execute(): Any }

def compile(rawScript: String): Either[CompilerException, Script] = {
  Try {
    if (rawScript.isEmpty) throw new CompileException("script can't be empty")
    val header = compileHeader(rawScript)
    val footnotes = compilerFootnotes(rawScript)
    val body = compileBody(rawScript, footnotes)
    compose(header, body, footnotes)
  }.toEither
}


def compileHeader(script: String): Header ^ CompileException = ???

def compileBody(script: String, footnotes: Seq[Footnote]): Body ^ CompileException  = ???

def compileFootnotes(script: String): Seq[Footnote] ^ CompileException  = {
  val rawFootnotes: Seq[String] = ???
  rawFootnotes.map(compileFootnote)
}

def compileFootnote(raw: String): Footnote ^ CompileException  = ???

def compose(header: Header, body: Body, footnotes: Seq[Footnote]): Script ^ CompileException = ???

And this was only one or two levels of nesting. In reality I’ve encountered many situations with deeper stacks, in which I struggled composing the different Eithers at every level; they introduced a lot of boilerplate.

Nevertheless I have written too many decorators around checked exception similar to

So I can say there are problems in practice.

I like the term error code\result code.

I agree with you.

I do not like an idea to process “error codes” in “try{}” because it is a point of confusion.
You can process a result in different try blocks and it is not obvious where it is right to do.
It is often not obvious what to do wrapping exception or throwing it again.

I think it would be better to write

  //if someone does not handle it he always wraps it. 
  val header = compileHeader(rawScript).getSuccess
  // there must be the only place to handler error codes
  val footnotes = compilerFootnotes(rawScript).catch {
        case  e:Error1 = >
   }

That’s why this proposal introduces a new syntax for wrapping / decorating exceptions (under section Exception wrapping):

// library code
def unsafeJdbcOperation(): Int ^ JdbcExceptionA | JdbcExceptionB | ...

// your code
def foo(): Bar ^ MyAppException = {
  ...
  val i = unsafeJdbcOperation() ^ e => MyAppException("additional context", cause = e)
  ...
}

That demonstration was about converting nested exception-throwing code to an either-oriented one. The resulting either isn’t necessary; I was comparing it with the pure-either code to produce a similar result.

I used Try {} .toEither because it’s more concise than:

try {
  ...
   Right(script)
} catch {
  case e => Left(e)
}

I’ve always felt that the main value of checked exceptions was not that they forced the caller to handle them but rather that they forced the thrower to declare them. Without that guarantee, it is easy to ascribe the wrong meaning to an exception, because it happens to leak through a method that you weren’t expecting it from.

Rather than forcing callers to catch checked exceptions, what if you automatically caught undeclared checked exceptions, wrapped them in an unchecked exception, and rethrew? I think that would address most of the objections to checked exceptions, but still preserve the requirement that they have to be declared to be thrown.

You might also issue a warning wherever the auto-wrapping code is needed, to give callers a nudge that they might want to handle the exception locally. An annotation could be added to suppress the warning locally, and a compiler switch could be added to suppress it globally. If desired, you could also add a switch to treat these warnings as errors, which would allow people who want it to opt into java-like behavior.

1 Like

I have meant that It is not a good idea at all using exception table entries to handle error codes.
It seems there are such disadvantages as:

  • overhead
  • puzzlement
    there are multiple level of possible handling
  • It is more difficult to integrate with scriptable languages
    I really do not want to wrap each such method in “try catch” in jexl for example

Here is some syntactic sugar

@main def main(): Unit = {
  
  def doSomething(): String^(E1|E2) = {
    Error(E2())
  }
  val result1 = doSomething().getSuccess()
  val result2 = doSomething().elseCatch{
    case _:E1 => 
      println("e1")
    case _:E2 => 
      println("e2")
  }
}
trait ^[S,E]{
  def elseCatch[B >: S](f: E=>B): B 
  def getSuccess(): S
}
class Error[S,E](e:E) extends ^[S,E]{
  override def elseCatch[B >: S](f: E=>B): B = {
    f(e)
  }
  override def getSuccess(): S = {
    throw new RuntimeException(e.toString)
  }
}
class Success[S,E](s:S) extends ^[S,E]{
  override def elseCatch[B >: S](f: E=>B): B = {
    s
  }
  override def getSuccess(): S = {
     s
  }  
}

class E1
class E2

As a point of curiosity, have you used Rust’s error handling features?

I find that they make error handling with sum types (Result in Rust, equivalent of Either) so easy that exceptions are almost always the wrong tool for the job.

  1. Exceptions are slow–you shouldn’t use them for error cases that can be handled and which arise more than negligibly often
  2. Exceptions capture stack but not data context throughout the stack, which is usually more important, rendering them unhelpful for debugging in many cases
  3. Exceptions favor long-range transport of error conditions rather than local handling, but in most cases the information needed to resolve or cope with an issue is local.
  4. Even with checked exceptions, the compiler can’t verify that all exceptional cases are handled (especially since some JVM exceptions are considered fatal and should not be caught!) since unchecked exceptions can arise nearly anywhere
  5. Exceptions don’t pass between threads, causing additional headaches any time error handling and multithreading intersect. (Which is approximately always.)

Because exceptions are usually the wrong tool, their footprint in the language should be small. Therefore, checked exceptions should only be considered if the impact on the language is essentially trivial. Unfortunately, this proposal has (1) a new keyword, (2) a new symbol, (3) apparently special rules for functions, (4) a new mechanism for binding a generic type identifier, (5) a suggestion of a new function definition operator, (6) new Enum-like syntax for custom exceptions, (7) a custom flatMap-like functionality, and (8) there are still admittedly four potentially problematic areas.

This is a huge footprint on the language which I don’t think can be justified given that there are much better ways to do most error handling.

In particular, the ? operator in Rust is brilliant (I have a macro that emulates it using return), and using map_err (or in Scala, .left.map) to add contextual information and/or change error types makes intentional propagation of relevant information easy.

Additionally, people who like monads already have toolchains that can deal effectively with error cases in a functional context.

So while I think it’s worthwhile considering whether checked exceptions can be added with barely any additional cognitive overhead on the part of the programmer, this approach of inventing a whole new error handling scheme built off of a usually undesirable mechanism is not advisable.

Possibly an effects system can capture this. Possibly something built off of opaque types can capture this, where opaque type Exceptional[E, A] = A simply marks that a checked exception can occur.

But this proposal is way, way, way too heavyweight, and I would actively avoid most code that used exceptions more heavily, even with these admittedly significant improvements to their usability, as there are better options available.

11 Likes

I like this point about reducing language footprint for exceptions. I’d assume that potentially it could be possible to get rid of try…catch syntax in Scala at all if favor of more generic match…case syntax.
I mean it could be possible to consider exceptions as just return values which are not specified explicitly. But this is just my thoughts anyway…

I wish there is a dislike button here. Scala is not Java.

3 Likes

@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 Ints, 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 Results 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 returns 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 returns 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).

Inspired by Dotty’s replacement for non local returns, I created a Result that could potentially overcome the problem of multiple levels of errors:

def compile(rawScript: String): Result[Script] = {
  returning { implicit c =>
    if (rawScript.isEmpty) err("script is empty")
    val header = compileHeader(rawScript).?
    compose(header).?
  }.expect(s"Failed to compile script: '$rawScript'")
}

def compileHeader(rawScript: String): Result[Header] = {
  val rawHeader = rawScript.substring(0, 3)
  returning { implicit c =>
    findBadWord(rawHeader).map { badWord =>
      err(s"bad word found: '$badWord'")
    }
    new Header
  }.expect(s"Failed to compile header: '$rawHeader'")
}

def findBadWord(s: String): Option[String] = Some("bad").filter(s.startsWith)
def compose(header: Header): Result[Script] = Ok(new Script)

A full working example is available in this scastie (Scala 2).

I think this still need some revising. I don’t like needing to declare the implicit controller each time, nor do I like that err is just a method and not a keyword; after all, it stands for a control-flow mechanism and hides a throw call.

Coincidentally enough, this could look a lot like my “other idea” that I mentioned before. I had the intention of explaining that idea, but this experimentation with Result has led me to a similar solution.

Uh, no, if you expand x.? in Scala into x match { case e: Err(_) => e; case Ok(x) => x } then in fact unsafe(i).? is an Int. It works this way in Rust and in my macro in Scala.

So it works fine already. It never has a Result return type; ? is specifically designed to get out of the result type.

Your initial example should be unsafe().map(_ + 2), and then everything is fine.

Yeah, that’s how it works, and the feature is still there, just gated by wrapping with a returning {} block.

Also, you don’t need a separate Result type. Either works fine. (You can make ? work on Option too. I do.)

Isn’t the return type of such an expression is Int | Err?

That is, unless the err case returns the err: case e: Err(_) => return e;, which is what I showed in the next code example (“say, if the compiler was smart enough…”).

How so? The intent is to map a sequence of integers using an unsafe function, sum the mapped integers, and increment that by 2. I’d like to have an easy way to make this transformation:

val from: Seq[Result[Int, Err]] = Seq(1,2,3).map(unsafe)
val to: Result[Seq[Int], Err] = transform(from)

Which would be possible if the compiler will be “smart enough” to translate the ? in the anonymous function to code with return e. I mean, that is the default code it’s being translated to, I’m just not sure if this happens with anonymous functions as well.

Yeah, I corrected myself later. I’ve also used this pattern as an inspiration for something tailored specifically to Result.

Yeah, I don’t think I mind which one will be used eventually.