Help us understand the problem. What is going on with this article?

Loan パターンのネストは継続モナドでシュッと解決できるよという話

More than 1 year has passed since last update.

先日見かけた Closeモナド についての言及になります

この記事の概要

長くなりそうなので最初にまとめです

  • Close モナドにはいくつかの問題があります
  • それらの問題は既存の継続モナドによって解決できます
  • リソース管理のために新たなモナドを考案する必要はありません

Close モナドが抱える問題

動作の問題

  • run を二回以上呼べません
    • リソースがインスタンス変数として保持されているためです
    • 言い換えると mapflatMap 自体に副作用があります

コードの問題

  • Close[R, A]A にアクセスするための手段が map しかありません
    • 副作用が目的の処理であっても map が必要になってしまいます
  • クラス内にリソースを閉じるための try-finally が二回書かれています
    • DRY に書きたいですね

継続モナドの導入

まずは最も簡潔な(と思われる)実装を先に載せておきます

package continuation

class Continue0[+A] private(callback: (A => Unit) => Unit) {

  def map[B](f: A => B): Continue0[B] = Continue0 {
    g => apply(f andThen g)
  }
  def flatMap[B](f: A => Continue0[B]): Continue0[B] = Continue0 {
    g => apply(a => f(a) apply g)
  }
  def apply(f: A => Unit): Unit = callback(f)

  def run(): Unit = callback(_ => ())
}
object Continue0 {
  def apply[A](f: (A => Unit) => Unit): Continue0[A] = {
    new Continue0(f)
  }
  def from[A](a: A): Continue0[A] = {
    new Continue0[A](f => f(a))
  }
}

呼び出し側のコードはこんな感じです

  def sample0() = {
    val continue = for {
      x1 <- Continue0 from 1
      x2 <- Continue0 from 2
      x3 <- Continue0 from 3
    } yield {
      x1 - x2 - x3
    }
    continue { n =>
      println(n)// -4
    }
  }

for-yield を展開してみましょう。綺麗なネストの出現を確認できます

  def sample1() = {
    val continue =
      Continue0 from 1 flatMap { x1 =>
        Continue0 from 2 flatMap { x2 =>
          Continue0 from 3 map { x3 =>
            x1 - x2 - x3
          }
        }
      }
    continue { n =>
      println(n)// -4
    }
  }

なんとなく形が見えてきましたね

継続モナドを利用して Close を実装する

といっても、コールバック関数を実行してリソースを閉じるだけです

package continuation

// Closer は元記事と同じ
import close.Closer

object Close {
  def apply[A: Closer](res: => A): Continue0[A] =
    Continue0 { callback =>
      val target = res
      try callback(target)
      finally implicitly[Closer[A]] close target
    }
}

念のため使用例も見てみましょう

  def main(args: Array[String]): Unit = {
    implicit def closer[R <: Closeable]: Closer[R] = Closer { x =>
      println(s"close: ${x.toString}")
      x.close()
    }
    val a = for {
      in     <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
      reader <- Close(new InputStreamReader(in, "UTF-8"))
      buff   <- Close(new BufferedReader(reader))
      out    <- Close(new FileOutputStream("dest.txt"))
      writer <- Close(new OutputStreamWriter(out, "UTF-8"))
    } yield {
      println("[begin]")

      var line = buff.readLine()
      while (line != null) {
        println(line)

        writer.write(line + "\n")
        line = buff.readLine()
      }
      println("[end]")
    }
    a.run()
    a.run() // 二回目
  }

問題なさそうです。run も無事に二回呼べています。

[begin]
This
is
a
pen
[end]
close: java.io.OutputStreamWriter@400f2b73
close: java.io.FileOutputStream@13150587
close: java.io.BufferedReader@33ddbb85
close: java.io.InputStreamReader@122c984a
close: java.io.FileInputStream@2bc16dcf
[begin]
This
is
a
pen
[end]
close: java.io.OutputStreamWriter@10436c0d
close: java.io.FileOutputStream@4e1cec13
close: java.io.BufferedReader@3804d352
close: java.io.InputStreamReader@188b6e88
close: java.io.FileInputStream@13234a90

もう for-yield の最後の値を得るために map を呼ぶ必要もありません

    val foo = for {
      in     <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
      reader <- Close(new InputStreamReader(in, "UTF-8"))
      buff   <- Close(new BufferedReader(reader))
      out    <- Close(new FileOutputStream("dest.txt"))
      writer <- Close(new OutputStreamWriter(out, "UTF-8"))
    } yield {
      "hello"
    }
    foo {
      message => println(message) // hello
    }

これで Close 自体のリファクタは終わりです。おつかれさまでした。

継続モナドを改善してコールバック関数を不要にする

簡単のため省きましたが、前述の継続モナドにはまだ不自然な i/f が残っています。

class Continue0[+A] private(callback: (A => Unit) => Unit) {

(A => Unit) => (Unit)Unit って何だよ!そこの制限を外せば値を直接返せるじゃん!って思いますよね。思わなかったら今から思ってください。

しかしながら、ここで何も考えずに愚直に型パラメータを導入しても :

package continuation

class Continue1[R, +A] private(callback: (A => R) => R) {

  def map[B](f: A => B): Continue1[R, B] = Continue1 {
    g => apply(f andThen g)
  }
  def flatMap[B](f: A => Continue1[R, B]): Continue1[R, B] = Continue1 {
    g => apply(a => f(a) apply g)
  }
  def apply(f: A => R): R = callback(f)

  /*
  def run(): Unit = callback(_ => ())

  [error]  found   : Unit
  [error]  required: R
  [error]   def run(): Unit = callback(_ => ())
  [error]                                   ^
   */
}

run で上記のように叱られます。
callback の実行でしか必要のないはずの型 R をクラスに保持させているのが原因ですね。

ということで、多相の apply を得るために callback に専用の型を与えてしまいましょう。

package continuation

trait Callback[A] {
  def apply[R](f: A => R): R
}
class Continue[+A] private(callback: Callback[A]) {

  def map[B](f: A => B): Continue[B] =
    Continue apply new Callback[B] {
      override def apply[R](g: B => R): R = {
        callback(f andThen g)
      }
    }
  def flatMap[B](f: A => Continue[B]): Continue[B] =
    Continue apply new Callback[B] {
      override def apply[R](g: B => R): R = {
        callback(a => f(a) apply g)
      }
    }
  def apply[R](f: A => R): R = callback(f)
  def run(): A = apply(identity)
}
object Continue {
  def apply[A](f: Callback[A]): Continue[A] = {
    new Continue(f)
  }
  def from[A](a: A): Continue[A] = {
    Continue apply new Callback[A] {
      override def apply[R](f: A => R): R = f(a)
    }
  }
}

CallbackContinue で表現できることが分かったのでまとめます :

trait Continue[A] {
  self =>
  def map[B](f: A => B): Continue[B] = new Continue[B] {
    override def apply[R](g: B => R): R = self(f andThen g)
  }
  def flatMap[B](f: A => Continue[B]): Continue[B] = new Continue[B] {
    override def apply[R](g: B => R): R = self(a => f(a) apply g)
  }
  def apply[R](f: A => R): R
  def run(): A = apply(identity)
}

object Continue {
  def from[A](a: A): Continue[A] = {
    new Continue[A] {
      override def apply[R](f: A => R): R = f(a)
    }
  }
}

Close も少しゴツい実装に変わりますが… :

object Close {
  def apply[A: Closer](res: => A): Continue[A] =
    new Continue[A] {
      override def apply[R](f: A => R): R = {
        val target = res
        try f(target)
        finally implicitly[Closer[A]] close target
      }
    }
}

この変更によって直接 runA の値を返せるようになります。
もうコールバック関数の存在すら意識する必要はありません。やった!

    val foo = for {
      in     <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
      reader <- Close(new InputStreamReader(in, "UTF-8"))
      buff   <- Close(new BufferedReader(reader))
      out    <- Close(new FileOutputStream("dest.txt"))
      writer <- Close(new OutputStreamWriter(out, "UTF-8"))
    } yield {
      ...
      "hello"
    }
    println(foo.run()) // "hello"

これで一通りの i/f 改善も終わりです。今度こそおつかれさまでした。

参考リンク

  • Continuation monad in Scala
    • Tonny Morris さんによる記事です
      • 投稿は 2008 年ですが概念は今でも変わっていません
      • 処理を分岐させる callcc 実装がとてもおもしろいです
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした