🥰

Scala 3 boundary/break で optional chaining と error short circuit

2023/12/07に公開

この記事は Scala アドベントカレンダー 2023 7 日めの記事です.

この記事では Scala 3 で追加された boundary/break 構文を使って nullable な値のチェーンやエラーの early-return を実現する方法について書く.

よく知られているように(?) TypeScript や Kotlin では nullable な値を ? でチェーンすることができる(optional chaining).

const mail = book?.author?.email;

また、Rust ではResult で失敗する関数や OptionNone になる処理に ? をつけることで処理を short-circuit して抜け出せる. これは TypeScript や Kotlin の optional chaining を Result 型にも拡張したものとしてみることができる.

Rust: Result の short-circuit

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!()
}

Rust: Option の short-circuit


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) を用いて実装できる.

Scala 2: Either の short-circuit

def f(): Either[String,Int] = {
  for {
    str <- mayFailWithString()
    i <- str.toIntOption.toRight("error message")
  } yield i
}

private def mayFailWithString(): Either[String,String] = ???

Scala 2: Option の short-circuit

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 な値のチェーンができる.

Scala 3: Either の short-circuit

エラーの場合は以下のような 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)

Scala 3: Option の short-circuit

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)

参考

https://www.reddit.com/r/scala/comments/15acc0q/comment/jtnlree/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

Discussion