帮我理解这段 Scala 代码:scalaz IO Monad 和 implicits。

5
这是this问题的后续。
以下是我试图理解的代码(来自http://apocalisp.wordpress.com/2010/10/17/scalaz-tutorial-enumeration-based-io-with-iteratees/):
object io {
  sealed trait IO[A] {
    def unsafePerformIO: A
  }

  object IO {
    def apply[A](a: => A): IO[A] = new IO[A] {
      def unsafePerformIO = a
    }
  }

  implicit val IOMonad = new Monad[IO] {
    def pure[A](a: => A): IO[A] = IO(a)
    def bind[A,B](a: IO[A], f: A => IO[B]): IO[B] = IO {
      implicitly[Monad[Function0]].bind(() => a.unsafePerformIO,
                                        (x:A) => () => f(x).unsafePerformIO)()
    }
  }
}

这段代码的使用方法如下(我假设已经暗含了import io._):
def bufferFile(f: File) = IO {   new BufferedReader(new FileReader(f)) }

def closeReader(r: Reader) = IO {   r.close }

def bracket[A,B,C](init: IO[A], fin: A => IO[B], body: A => IO[C]): IO[C] = for { a <- init
      c <- body(a)
      _ <- fin(a) }   yield c

def enumFile[A](f: File, i: IterV[String, A]): IO[IterV[String, A]] =  bracket(bufferFile(f),
          closeReader(_:BufferedReader),
          enumReader(_:BufferedReader, i))

我现在正在尝试理解 implicit val IOMonad 的定义。这是一个 scalaz.Monad,因此需要定义 scalaz.Monad 特质的 purebind 抽象值。 pure 接受一个值并将其转换为包含在 "容器" 类型中的值。例如,它可以将一个 Int 转换为一个 List[Int]。这似乎非常简单。 bind 接受一个 "容器" 类型和一个将容器所持有的类型映射到另一种类型的函数。返回的值是相同的容器类型,但现在它持有一个新类型。例如,使用将 Int 映射到 String 的函数,可以将 List[Int] 映射到 List[String]。那么,bind 是否与 map 大致相同呢?

bind 的实现是我卡住的地方。这是代码:

def bind[A,B](a: IO[A], f: A => IO[B]): IO[B] = IO {
  implicitly[Monad[Function0]].bind(() => a.unsafePerformIO,
      (x:A) => () => f(x).unsafePerformIO)()
}

这个定义接受一个类型为IO[A]的参数,并使用一个函数将其映射到类型为IO[B]的结果。该函数接受一个类型为A的参数并返回一个类型为IO[B]的结果。我猜想,为了实现这一点,它必须使用flatMap来“展平”结果(对吗?)。 = IO { ... }与以下表达式相同:
 = new IO[A] {
  def unsafePerformIO = implicitly[Monad[Function0]].bind(() => a.unsafePerformIO,
      (x:A) => () => f(x).unsafePerformIO)()
  }
}

我认为?

implicitly 方法查找实现了 Monad[Function0] 的隐式值(值,对吗?)。这个隐式定义是从哪里来的?我猜这是从 implicit val IOMonad = new Monad[IO] {...} 定义中来的,但我们现在正在该定义内部,事情有点循环,我的大脑开始陷入无限循环 :)

另外,bind 的第一个参数 (() => a.unsafePerformIO) 似乎是一个不带参数并返回 a.unsafePerformIO 的函数。我该如何阅读这个?bind 将容器类型作为其第一个参数,因此也许 () => a.unsafePerformIO 解析为容器类型?


Scalaz现在实际上已经提供了一个IO单子。导入scalaz.effects._即可使用。 - Apocalisp
1个回答

14

IO[A]旨在表示返回AAction,其结果可能取决于环境(即任何东西,变量值、文件系统、系统时间等),同时执行该操作也可能修改环境。实际上,Scala中表示Action的类型应为Function0Function0[A]在调用时返回A,它肯定允许依赖和修改环境。 IO是以另一个名称表示的Function0,但旨在区分(标记?)那些依赖于环境的Function0和那些实际上是纯值的函数(如果说f是一个总是返回相同值而没有任何副作用的函数[A],那么f和其结果之间没有什么区别)。要精确地说,标记为IO的函数不一定必须具有副作用。而是那些没有被标记的函数必须没有副作用。但是请注意,将不纯的函数封装在IO中完全是自愿的,当您获取Function0时,没有保证它是纯的。在Scala中,“使用IO肯定不是主流风格”。

pure接受一个值并将其转换为包含在“容器”类型中的值。

很正确,但“容器”可能意味着很多事情。而由pure返回的那个值必须尽可能轻,它必须是不会有任何区别的值。列表的要点是它们可以具有任意数量的值。由pure返回的那个值必须只有一个。IO的要点是它依赖于和影响环境。由pure返回的那个值必须不这样做。因此,实际上是将纯的Function0() => a包装在IO中。

bind与map基本相同

不完全如此,bind与flatMap相同。正如您所写的,map将接收从IntString的函数,但这里您有从IntList[String]的函数。

现在,暂时忘记IO,并考虑一下对于Action,即对于Function0来说,bind/flatMap意味着什么。 让我们假设有

val askUserForLineNumber: () => Int = {...}
val readingLineAt: Int => Function0[String] = {i: Int  => () => ...}

现在,如果我们必须像bind/flatMap一样组合那些项目以获得返回字符串的动作,那么它必须是相当清晰的:向读取器请求行号,读取该行并返回它。这将是

val askForLineNumberAndReadIt= () => {
  val lineNumber : Int = askUserForLineNumber()
  val readingRequiredLine: Function0[String] = readingLineAt(line)
  val lineContent= readingRequiredLine()
  lineContent
}

更普遍地说

def bind[A,B](a: Function0[A], f: A => Function0[B]) = () => {
  val value = a()
  val nextAction = f(value)
  val result = nextAction()
  result
}

更短:

def bind[A,B](a: Function0[A], f: A => Function0[B]) 
  = () => {f(a())()}

所以我们知道bind对于Function0必须是什么,pure也很清楚。我们可以这样做

object ActionMonad extends Monad[Function0] {
  def pure[A](a: => A) = () => a
  def bind[A,B](a: () => A, f: A => Function0[B]) = () => f(a())()
}

现在,IO假扮成Function0。我们不能仅仅执行 a(),而是要执行 a.unsafePerformIO。如果要定义一个IO,不再使用 () => body,而是使用 IO {body}

object IOMonad extends Monad[IO] {
  def pure[A](a: => A) = IO {a}
  def bind[A,B](a: IO[A], f: A => IO[B]) = IO {f(a.unsafePerformIO).unsafePerformIO}
}
在我看来,这已经足够好了。但实际上它重复了ActionMonad。你提到代码中的重点是避免这种情况,并重用对于Function0所做的工作。人们可以很容易地从IO转换为Function0(使用()=>io.unsafePerformIo),也可以从Function0转换为IO(使用IO { action() })。如果您有f:A => IO [B],则还可以通过与IO到Function0变换组合来将其更改为f:A => Function0 [B],因此(x:A)=> f(x).unsafePerformIO。
在IO绑定中发生的事情是:
1.()=> a.unsafePerformIO:将a转换为Function0
2.(x:A)=>()=> f(x).unsafePerformIO:将f转换为A => Function0 [B]
3.implicitly [Monad [Function0]]:获取Function0的默认monad,与上面的ActionMonad完全相同
4.bind(...):将刚刚转换为Function0的参数a和f应用于Function0 monad的bind
5.封闭的IO {...}:将结果转换回IO。
(不确定我是否非常喜欢它)

我该如何阅读这个代码 val readingLineAt: Int => Function0[String] = Int => String {i: Int => () => ...}?例如,“readingLineAt”是一个返回一个方法的值,该方法接受一个Int并返回一个Function0 [String]。我可以理解到这一步,但我不确定实现方式。Int => String 是某种匿名函数吗? - three-cups
抱歉,打错字/复制错误/粘贴错误。已修正。 - Didier Dupont
感谢您的所有帮助。我仍在研究这篇文章(最近没有太多空闲时间)。我认为您的意思是 val nextAction = f(value) 而不是 val nextAction = f(a) - three-cups

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接