Scala

ScalaでRustっぽく書きたい 〜try!編〜

More than 1 year has passed since last update.

途中で失敗したら強制的に例外を吐きたい

use std::io;
use std::fs::File;
use std::io::Read;

fn main() {
    println!("Hello, world!");
    println!("{:?}", foo());
}


fn foo() -> Result<String, io::Error> {
    println!("-- 1");
    let mut f = File::open("username.txt")?;
    println!("-- 2");
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)

これはRustのコードで、ファイルが存在しなければ -- 2 は出力されずに、そこで作業が終了し強制的に Err(Error { repr: Os { code: 2, message: "No such file or directory" } })
が返るコードです。

今回はこれの早期リターンの部分をScalaで書いてみたいと思います。

scalamacros によってマクロを作る

sbt-examplesbt-example-newstyle を参考にして、一つマクロを組んでみます。

package examples

import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context


object Macros {
  def guardimpl[T](c: Context)(defn: c.Expr[Option[T]]) = {
    import c.universe._
    c.Expr[T](
      q""" if ($defn.isEmpty) { return None } else { $defn.get } """)
  }
  def guard[T](defn: Option[T]): T = macro guardimpl[T]
}

Rustでは元々?try! マクロでした。しかし、Scalaでは tryが予約されていて使えなかったので、guardにしました。

使い方はこんな感じです。

package foo

import examples.Macros._


object Hello extends App {
  val user = User("tarou")
  println(user.yourName("tarou"))
  println(user.yourName("jirou"))
}


case class User(name: String) {


  def yourName(name: String): Option[String] = {

    val ret: String = guard[String] {
      if (this.name == name) Some(this.name) else None
    }

    println(s"your name is ${ret}")
    Some(ret)

  }

}

結果は

your name is tarou
Some(tarou)
None

となります。jirou の場合は名前が間違っているため名前を呼ばれることはありません。
guardは、成功するとOption型をアンラップして中身を返すため、値をそのまま使う事ができます。

macro内でreturnを使っているため、defn が None だった場合は、早期リターンでNoneが返ります。macroを改造してEitherや単純にExceptionを返してあげても面白いかもしれません。