This small library introduces a Rust-like ?
-Operator for Scala 3.
// Add to build.sbt
libraryDependencies += "net.reactivecore" %% "quest" % <Version>
import quest.*
def getUser(id: Int): Option[String] = { ... }
def getPermissions(id: Int): Option[List[String]] = { ... }
def getUserAndPermissions(id: Int): Option[(String, List[String])] = quest {
val user = getUser(id).?
val permissions = getPermissions(id).?
Some((user, permissions))
}
quest(f: => T): T
initiates a block within which the question operator can be utilized.?
extracts the value from some type (e.g.Either[L,R]
) if it represents a success, or exits the quest block if it represents a failure.bail(value: T)
immediately exits the quest block with the specified value. It's an alias forscala.util.boundary.break
.
Option[T]
Either[L,R]
Try[T]
To use other types with the question operator, implement the QuestionOperatorSupport
type class.
The quest block leverages scala.util.boundary
which uses exceptions for early exit. In cases of failure, an scala.util.boundary.Break
exception is thrown and caught by the scala.boundary.apply
function. (Note that this is optimised to a labelled jump, instead of exception, when no intermediate closures are in between ?
and quest
)
Two helper classes simplify its use:
scala.boundary.Label[T]
captures the return type of thequest
/break
method, enabling the question operator and leveraging Scala's type system to determine the correct return typeQuestionOperatorSupport[T]
decodes each supported type into it's Failure and Success type. Failure and Success type can be gathered using the Aux-Pattern:QuestionOperatorSupport.Aux[T,F,S]
- Short notation
- Minimal codebase (50 LOC)
- Compatible with IntelliJ IDEA (unlike some macros)
- Supports Loom-based virtual threads
scala.util.boundary
uses exceptions for control flow, deviating from purely functional Scala practices. This approach may cause issues in certain contexts:- It is incompatible with delayed execution contexts (e.g.,
Future
, Effect Systems or collection views), potentially throwingBreak
exceptions unexpectedly.
- It is incompatible with delayed execution contexts (e.g.,
- A small performance test measured an overhead of ~5ns per Failure return per Call in comparison to flatMap and return.
- Martin Ordersky: Direct Style Scala (Scalar 2023)
- Built upon
scala.util.boundary
after Hint on Reddit