Scalaの新しいI/Oライブラリbetter-filesを使ってみる

ScalaのI/Oライブラリシリーズ第三弾です。今回試してみたのはbetter-filesという、2015年に開発が開始された比較的新しいものです。名前からもわかる通り、I/Oライブラリといってもファイル操作を重視したライブラリになっています。

github.com

特徴はjava.ioに対するユーティリティやimplicit conversionを提供するものではなく、新たにScala用のインターフェースが実装されており、Javaとの相互運用のためのメソッドが提供されているということです。Scala標準のコレクションライブラリと同じような立ち位置と考えるとわかりやすいかもしれません。

better-filesを使用するにはbuild.sbtに以下の依存関係を追加します。

libraryDependencies += "com.github.pathikrit" %% "better-files" % "2.14.0"

まずはFileを生成します。様々な生成方法があり、java.io.FileからtoScalaで変換することもできます。

val f1 = File("build.sbt")
val f2 = file"src/scala"
val f3 = new java.io.File("build.sbt").toScala
val f4 = f2/"test/Sample.scala"
val f5 = "src"/"scala"/"main"/"test"/"Sample.scala"

読み込み、書き込みを簡単に行うことができます。

// 文字列で読み込み
val string = f.contentAsString
// バイト配列で読み込み
val bytes = f.byteArray

// 一行ごとに処理(裏では全体を一気に読み込んでるので巨大ファイルは不可)
f.lines.foreach { line =>
  ...
}

// 文字列を書き込み、追記
f.write("こんにちは").append("世界")
// バイト配列を書き込み
f.write("こんにちは".getBytes("UTF-8"))(File.OpenOptions.default)

// DSLっぽいやつ
f < "Hello" << "World"
// 反対方向にも書ける(IntelliJだと赤くなる)
"World" `>>:` "Hello" `>:` f

文字コードはデフォルトではUTF-8ですが、明示的に指定することもできます。書き込み系のメソッドFile.OpenOptionsというimplicitパラメータを取り、デフォルトではFile.OpenOptions.defaultなのですが、なぜかバイト配列を書き込むメソッドだけはデフォルト値が定義されておらず、明示的に渡す必要があります。

ファイル操作はチェーンさせることができます。また、ワイルドカードを使用した検索も可能です。

// ファイル操作をチェーン
f.createIfNotExists()
  .appendNewLine()
  .append("Hello World!")
  .copyTo(File("test.bak"))

// ワイルドカードで検索
val files = File("src").glob("**/*.{java,scala}")
files.foreach { file =>
  println(file.pathAsString)
}

// オプションを指定すると正規表現も使える
val files = File("src").glob(".*\\.scala$", syntax = "regex")

Javaのクラスにも簡単に変換できるので既存のJavaライブラリとの相互運用も容易です。

val file: java.io.File             = f.toJava
val in: java.io.InputStream        = f.newInputStream
val out: java.io.OutputStream      = f.newOutputStream
val reader: java.io.BufferedReader = f.newBufferedReader
val writer: java.io.BufferedWriter = f.newBufferedWriter

ローンパターン的なやつはこんな感じ。JavaのストリームはautoClosedでbetter-filesのManagedResourceに変換することで自動的にクローズされるようになります。なのでファイル以外の場合でも利用可能です。

// 自動でクローズされる
for {
  in  <- f1.inputStream
  out <- f2.outputStream
} in.pipeTo(out)

// Javaのストリームの場合
for {
  in  <- new FileInputStream("test.txt").autoClosed
  out <- new FileOutputStream("test.out").autoClosed
} in.pipeTo(out)

もちろんyieldで値を戻すこともできますが、戻り値がTraversableになってしまうのがちょっと微妙かもしれません。この方法だと仕方ないですが…。

この他にもLinuxコマンド風のDSL、Zipファイルの作成と展開、定型的なファイルの読み込むためのスキャナ、ファイルのモニタリングなどの機能があり、かなり高機能なライブラリです。モナモナしてないので使い勝手はよさそうですが、エラーに関しては全体的に例外方式のようで、この点については賛否両論あるかもしれません。