CLOVER🍀

That was when it all began.

Scalaz Effects IOで遊ぶ

ScalazでIOモナドおよびSTモナドを扱うパッケージとして、effectsというのものがあります。Haskellライクに入出力とか可変状態が扱えるということで、特にIOは触ってみたかったんですよね。

以下のスライドを参考にさせていただきました。
http://www.slideshare.net/SanshiroYoshida/scalaz-effects

というわけで、まずは標準入出力から。

下準備として、これだけimportします。

import scalaz._
import scalaz.Scalaz._
import scalaz.effects._

続いて、標準入力から読み込んで、ちょっと変更して出力するメソッド。ほとんどスライドから借りてきたコードです。

  def greeting(): Unit = {
    putStrLn("Greeting Example").unsafePerformIO

    val io: IO[Unit] =
      for {
        _ <- putStrLn("Please, Enter Your Name!!")
        inputStr <- readLn
        outputStr = "Welcome to [%s], Enjoy Programming!".format(inputStr)
        _ <- putStrLn(outputStr)
      } yield ()

    io.unsafePerformIO
  }

おおー、面白い面白い。

意味的には、以下で1行標準出力に出力。

        _ <- putStrLn("Please, Your Name!!")

以下で、標準入力から1行読み込みとなります。

        inputStr <- readLn

最後に、以下の1行で実行します。これがないと何も起こらないので、あしからず。

    io.unsafePerformIO

続いて、ファイルから読んでみましょう。scalaz.effects.getFileLinesメソッドあたりを使えばよさそうなことはなんとなくわかるのですが、例が全然ありません…。

ちなみに、getFileLinesメソッドはEnumeratorMを返します。

Stack OverflowでこんなQAが挙がっていたので、そこのコードをマネしてちょっと書いてみました。
http://stackoverflow.com/questions/7633609/scalazs-traverse-with-io-monad

  def readLinesFromFileAsIterV(fileName: String): Unit = {
    import java.io.File
    import IterV._
    
    val io: IO[Unit] =
      for {
        lines <- getFileLines(new File(fileName))(collect[String, List]).map(_.run)
        _ <- lines.traverse_(putStrLn)
      } yield ()

    io.unsafePerformIO
  }

一応、これで動きます。ちなみに、以下の箇所の戻り値はこうなっています。

        lines <- getFileLines(new File(fileName))(collect[String, List]).map(_.run)
        // 戻り値はIO[List[String]]


ただ、元のQAの内容を見ると、「でかいファイルを読ませるとStackOverflowErrorになっちゃうんだけど」と書いてあります。

scalaz.effects.getReaderLinesが、末尾再帰になっていないのがいかんと書いてあったのでちょっと見てみました。
scalaz.effect.package.scala

  /** Enumerate the lines from a BufferedReader */
  def getReaderLines(r: => BufferedReader): EnumeratorM[IO, String] = new EnumeratorM[IO, String] {
    def apply[A](it: IterV[String, A]) = {
      def loop: IterV[String, A] => IO[IterV[String, A]] = {
        case i@Done(_, _) => io { i }
        case i@Cont(k) => for {
          s <- rReadLn(r)
          a <- s.map(l => loop(k(El(l)))).getOrElse(io(i))
        } yield a
      }
      loop(it)
    }
  }

ココですね。

          a <- s.map(l => loop(k(El(l)))).getOrElse(io(i))

確かに…。微妙だー。

※ちょっと追記
引用元のslideshareにもうちょっとプリミティブな例があったので、こちらも試してみました。

  def readLinesFromFileSimple(fileName: String): Unit = {
    import java.io.{BufferedReader, File}

    def recurFile(r: BufferedReader, i: IO[Option[String]]): IO[Unit] =
      i >>= {
        case Some(s) => putStrLn(s) >|> recurFile(r, rReadLn(r))
        case None => ().pure[IO]
      }

    val io: IO[Unit] =
      for {
        r <- bufferFile(new File(fileName))
        _ <- recurFile(r, rReadLn(r))
        _ <- closeReader(r)
      } yield ()

    io.unsafePerformIO
  }

ファイルのオープン/クローズを、自前でする例です。残念ながら、こちらも末尾再起ではありませんが。ただ、こちらの方がちょっと書いていて面白いです。関数recurFile内で登場している妙な記号のようなメソッドは、暗黙の型変換が入り、scalaz.MAとなることにより使用できるメソッドです。

とりあえず、今回書いたコードを貼っておきます。
EffectsIOExample.scala

import scalaz._
import scalaz.Scalaz._
import scalaz.effects._

object EffectsIOExample extends App {
  greeting()
  readLinesFromFileAsIterV("src/main/scala/EffectsIOExample.scala")
  readLinesFromFileSimple("src/main/scala/EffectsIOExample.scala")

  def greeting(): Unit = {
    putStrLn("Greeting Example").unsafePerformIO

    val io: IO[Unit] =
      for {
        _ <- putStrLn("Please, Enter Your Name!!")
        inputStr <- readLn
        outputStr = "Welcome to [%s], Enjoy Programming!".format(inputStr)
        _ <- putStrLn(outputStr)
      } yield ()

    io.unsafePerformIO
  }

  def readLinesFromFileAsIterV(fileName: String): Unit = {
    import java.io.File
    import IterV._
    
    val io: IO[Unit] =
      for {
        lines <- getFileLines(new File(fileName))(collect[String, List]).map(_.run)
        _ <- lines.traverse_(putStrLn)
      } yield ()

    io.unsafePerformIO
  }

  def readLinesFromFileSimple(fileName: String): Unit = {
    import java.io.{BufferedReader, File}

    def recurFile(r: BufferedReader, i: IO[Option[String]]): IO[Unit] =
      i >>= {
        case Some(s) => putStrLn(s) >|> recurFile(r, rReadLn(r))
        case None => ().pure[IO]
      }

    val io: IO[Unit] =
      for {
        r <- bufferFile(new File(fileName))
        _ <- recurFile(r, rReadLn(r))
        _ <- closeReader(r)
      } yield ()

    io.unsafePerformIO
  }
}