🥰
Scala 3 boundary/break で optional chaining と error short circuit
この記事は Scala アドベントカレンダー 2023 7 日めの記事です.
この記事では Scala 3 で追加された boundary/break 構文を使って nullable な値のチェーンやエラーの early-return を実現する方法について書く.
よく知られているように(?) TypeScript や Kotlin では nullable な値を ?
でチェーンすることができる(optional chaining).
const mail = book?.author?.email;
また、Rust ではResult
で失敗する関数や Option
で None
になる処理に ?
をつけることで処理を short-circuit して抜け出せる. これは TypeScript や Kotlin の optional chaining を Result
型にも拡張したものとしてみることができる.
Result
の short-circuit
Rust: pub fn f() -> Result<i32,String> {
let str = may_fail_with_string()?;
let i = str.parse::<i32>().map_err(|_| "error message".to_string())?;
Ok(i)
}
fn may_fail_with_string() -> Result<String,String> {
todo!()
}
Option
の short-circuit
Rust:
struct A{b: Option<B>}
struct B{c: Option<C>}
struct C{d: Option<D>}
struct D{i:i32}
pub fn g() -> Option<i32> {
let a: A = todo!();
let i = a.b?.c?.d?.i;
Some(i)
}
Scala 2 ではこのような短絡処理は for
式(flatMap
) を用いて実装できる.
Either
の short-circuit
Scala 2: def f(): Either[String,Int] = {
for {
str <- mayFailWithString()
i <- str.toIntOption.toRight("error message")
} yield i
}
private def mayFailWithString(): Either[String,String] = ???
Option
の short-circuit
Scala 2: case class A(b: Option[B])
case class B(c: Option[C])
case class C(d: Option[D])
case class D(i:Int)
def g(): Option[Int] = {
val a : A = ???
for {
b <- a.b
c <- b.c
d <- c.d
} yield d.i
}
// あるいは
// a.b.flatMap(_.c.flatMap(_.d.map(_.i)))
nullable な値や失敗しうる値を統一されたインターフェースで操作できるがやや煩雑である. また、Option
は(Rust と違い)オブジェクトのアロケーションが発生するため非効率である.
Scala 3 の bounary/break を short-circuit に使う
Scala 3 では boundary
を使うことでより簡潔かつ効率よくこのようなshort-circuit や Nullable な値のチェーンができる.
Either
の short-circuit
Scala 3: エラーの場合は以下のような failable
boundary を定義する.
//> using scala "3.3.1"
import scala.util.boundary, boundary.{Label, break}
object failable:
inline def apply[E,T](inline body: Label[Either[E,Nothing]] ?=> Either[E,T]): Either[E,T] =
boundary(body)
extension [E,T](r: Either[E,T])
/** Exits with `E` to next enclosing `failable` boundary */
transparent inline def ! (using Label[Either[E,Nothing]]): T =
r match
case r:Right[?,T] => r.value
case e:Left[E,?] => break(e.asInstanceOf)
import failable.*
def f(): Either[String, Int] = failable:
val str = mayFailWithString().!
val i = str.toIntOption.toRight("error message").!
Right(i)
Option
の short-circuit
Scala 3: Option の場合は以下のような nullable
boundary を定義する.
//> using scala "3.3.1"
import scala.util.boundary, boundary.{Label, break}
object nullable:
inline def apply[T](inline body: Label[Null] ?=> T): T | Null =
boundary(body)
extension [T](r: T | Null)
/** Exits with null to next enclosing `nullable` boundary */
transparent inline def ? (using Label[Null]): T =
if r == null then break(null) else r.nn
case class A(b: B| Null)
case class B(c: C| Null)
case class C(d: D| Null)
case class D(i:Int)
val a: A = ???
import nullable.*
def g() = nullable:
val i = a.b.?.c.?.d.?.i
Some(i)
参考
Discussion