Помогите мне понять этот код Scala: скалаз IO Monad и последствия - PullRequest
5 голосов
/ 14 сентября 2011

Это продолжение этого вопроса.

Вот код, который я пытаюсь понять (это от 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. Вот как я понимаю. Это скальп .Monad , поэтому ему необходимо определить pure и bind абстрактные значения черты scalaz.Monad.

pure принимает значение и превращает его в значение, содержащееся в контейнере"type. Например, это может занять Int и вернуть List[Int]. Это кажется довольно простым.

bind принимает тип" container "и функцию, которая отображает тип, который содержит контейнерк другому типу. Возвращаемое значение - это тот же тип контейнера, но теперь он содержит новый тип. Например, можно взять List[Int] и сопоставить его с List[String], используя функцию, которая отображает Int s вString s. bind в значительной степени совпадает с map?

Реализация bind iгде я застрял.Вот код:

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 разрешает тип контейнера?

1 Ответ

14 голосов
/ 14 сентября 2011

IO[A] предназначен для представления Action, возвращающего A, где результат действия может зависеть от среды (что угодно, значения переменных, файловая система, системное время ...) ивыполнение действия также может изменить среду.На самом деле тип scala для Action будет Function0.Function0[A] возвращает A при вызове, и ему, безусловно, разрешено зависеть и изменять среду.IO - это Function0 под другим именем, но он предназначен для того, чтобы отличить (пометить?) Те функции0, которые зависят от окружения, от других, которые на самом деле являются чистым значением (если вы говорите, что f - это функция [A], котораявсегда возвращает одно и то же значение, без каких-либо побочных эффектов, нет большой разницы между f и его результатом).Если быть точным, функция помечается не так сильно, как IO должен иметь побочный эффект.Это то, что те, кто не помечен, не должны иметь ни одного.Обратите внимание, однако, что упаковка нечистых функций в IO является полностью добровольной, у вас не будет никакой гарантии, когда вы получите Function0, что она чистая. Использование IO определенно не является доминирующим стилем в scala .

pure принимает значение и превращает его в значение, содержащееся в типе «контейнер».

Совершенно верно, но «контейнер» может означать довольно много вещей,И тот, кто возвращается чистым, должен быть как можно более легким, он должен быть тем, который не имеет значения.Суть списка в том, что они могут иметь любое количество значений.Тот, кто вернулся чистым, должен иметь его.Дело в том, что ИО зависит от окружающей среды и влияет на нее.Тот, кто вернулся чистым, не должен делать ничего подобного.Так что это на самом деле чистый Function0 () => a, завернутый в IO.

bind почти так же, как map

Не так, bind - это то же самое, что flatMap.Когда вы пишете, map получит функцию от Int до String, но здесь у вас есть функция от Int до List[String]

Теперь, забудьте на мгновение IO и подумайте, что связывает /flatMap будет означать для действия, то есть для Function0.Давайте получим

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 - это скрытая функция 0.Вместо того, чтобы просто делать a(),, мы должны сделать a.unsafePerformIO.И чтобы определить один вместо () => 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 до IOIO { action() }).Если у вас есть f: A => IO [B], вы также можете изменить это значение на f: A => Function0[B], просто составив преобразование от IO до Function0, поэтому (x: A) => f(x).unsafePerformIO.

То, что происходит здесь в привязке к IO:

  1. () => a.unsafePerformIO: превращение a в Function0
  2. (x:A) => () => f(x).unsafePerformIO): поворот f в A => Function0[B]
  3. неявно [Monad [Function0]]: получить монаду по умолчанию для Function0, то же самое, что ActionMonad выше
  4. bind(...): применить bind монады Function0 к аргументам a и f, которые только что были преобразованы в Function0
  5. Включая IO{...}: преобразовать результат обратно в IO,

(Не уверен, что мне это очень нравится)

...