scalaz-effectのIOモナドを使ってみる

Scalaの標準ライブラリはコレクションに関してはは充実しているものの、IOに関しては貧弱そのものです。scala-ioが標準になるのではという時期もありましたが、いつの間にか動きが止まってしまったようです。もちろんJava標準のAPIやCommons IOなどを使ってもよいのですが、もっとScalaのメリットを活かした使いやすいライブラリが欲しいところです。

そこでScalazのモジュールの1つであるscalaz-effectに含まれているIOを試してみることにしました。ScalazでIO、嫌な予感がしますね。そう、IOモナドです。

scalaz-effectを使うにはbuild.sbtに以下の依存関係を追加します。

libraryDependencies += "org.scalaz" % "scalaz-effect_2.11" % "7.2.0"

まずはコンソールにメッセージを表示してみます。

import scalaz._, Scalaz._, effect._, IO._

val action: IO[Unit] = for {
  _ <- putStrLn("Hello")
  _ <- putStrLn("World")
} yield ()

action.unsafePerformIO()

完全にHaskellですね。putStrLnIO[Unit]を返し、それをfor式で合成しています。IOunsafePerformIO()で実行することができます。

ローンパターンを使ってファイルの読み込みを行ってみます。

import scalaz._, Scalaz._, effect._, IO._
import std.effect.closeable._
import java.io._

val action: IO[Unit] = for {
  bytes <- IO(new FileInputStream("test.txt")).using { in =>
    IO {
      new Array[Byte](in.available) <| { in.read }
    }
  }
  _ <- putStrLn(new String(bytes, "UTF-8"))
} yield ()

action.unsafePerformIO()

また、入出力処理で例外が発生した場合、なにもしなければそのままスローされますが、catchLeftIO[A]の戻り値を\/[Throwable, A]に変換することができます。

val action: IO[\/[Throwable, Unit]] = (for {
  ...
} yield ()).catchLeft

action.unsafePerformIO() match {
  case \/-(_) => // 成功
  case -\/(e) => // 失敗
}

scalaz-effectを使うことでローンパターンが使えたり例外処理が便利になってはいるものの、入出力処理そのものが簡単に書けるようになるというわけではありません。

また、実際使ってみると、unsafePerformIO()を呼ぶのをわすれてしまったり、複雑な合成をした際にうっかりIOがネストしてしまいネストした部分の処理が実行されなかったりなど、ハマりポイントも結構多いですし、Scalaは普通に手続き的な記述や副作用を扱うことができるので、そういう部分と混在するとコードもわかりにくくなってしまいます。*1

個人的にはScalaでIOモナドは使わないほうがいいんじゃないかなぁと感じました。

*1:これらの問題点はSlick3のDBIOを使う場合も同じですね…。