Как в образце Simple IO Type избавиться от побочных эффектов в "FP in Scala"?

Я читаю главу 13.2.1 и наткнулся на пример, который может обрабатывать ввод ввода-вывода и тем временем избавляться от побочных эффектов:

object IO extends Monad[IO] {
  def unit[A](a: => A): IO[A] = new IO[A] { def run = a }
  def flatMap[A,B](fa: IO[A])(f: A => IO[B]) = fa flatMap f
  def apply[A](a: => A): IO[A] = unit(a)    
}

def ReadLine: IO[String] = IO { readLine }
def PrintLine(msg: String): IO[Unit] = IO { println(msg) }

def converter: IO[Unit] = for {
  _ <- PrintLine("Enter a temperature in degrees Fahrenheit: ")
  d <- ReadLine.map(_.toDouble)
  _ <- PrintLine(fahrenheitToCelsius(d).toString)
} yield ()

У меня есть пара вопросов относительно этого куска кода:

  1. в unit функция, что делает def run = a действительно?
  2. в ReadLine функция, что делает IO { readLine } действительно? Будет ли это действительно выполнить println функция или просто вернуть тип ввода-вывода?
  3. Что значит _ в смысле понимания (_ <- PrintLine("Enter a temperature in degrees Fahrenheit: "))
  4. Почему это устраняет побочные эффекты IO? Я видел, что эти функции все еще взаимодействуют с входами и выходами.

2 ответа

Решение
  1. Определение вашего IO как следует:

    trait IO { def run: Unit }
    

    Следуя этому определению, вы можете понять, что написание new IO[A] { def run = a } означает инициализацию анонимного класса из вашей черты и назначение a быть методом, который запускается при вызове IO.run, Так как a является параметром по имени, на самом деле ничего не запускается во время создания.

  2. Любой объект или класс в Scala, следующий за контрактом apply метод, может быть вызван как: ClassName(args)где компилятор будет искать apply метод объекта / класса и преобразовать его в ClassName.apply(args) вызов. Более подробный ответ можно найти здесь. Как таковой, потому что IO сопутствующий объект обладает таким методом:

    def apply[A](a: => A): IO[A] = unit(a)    
    

    Расширение допускается. Таким образом, мы на самом деле называем IO.apply(readLine) вместо.

  3. _ имеет много перегруженных применений в Scala. Это означает, что "меня не волнует значение, возвращаемое из PrintLine"Откажитесь от него". Это так, потому что возвращаемое значение имеет тип Unit, к которому мы не имеем никакого отношения.

  4. Это не то, что IO Тип данных удаляет часть выполнения ввода-вывода, это то, что он откладывает его на более поздний момент времени. Обычно мы говорим, что IO работает на "краях" приложения, в Main метод. Эти взаимодействия с внешним миром все еще будут происходить, но, поскольку мы инкапсулируем их в IO, мы можем рассуждать о них как о ценностях в нашей программе, что приносит большую пользу. Например, теперь мы можем составлять побочные эффекты и зависеть от успеха / неудачи их выполнения. Мы можем смоделировать эти эффекты ввода-вывода ( используя другие типы данных, такие какConst) и многие другие удивительно приятные свойства.

Самый простой способ посмотреть на IO монада как маленький кусочек определения программы.

Таким образом:

  1. Это IO определение, run метод определяет, что IO Монада делает. new IO[A] { def run = a } это Scala способ создания экземпляра класса и определения метода run,
  2. Там немного синтаксического сахара происходит. IO { readLine } такой же как IO.apply { readLine } или же IO.apply(readLine) где readLine является функцией вызова по имени типа => String, Это вызывает unit метод из object IO и, таким образом, это просто создание экземпляра IO класс, который еще не работает.
  3. поскольку IO это монада, для понимания можно использовать. Требуется сохранение результата каждой монады в синтаксисе, подобном result <- someMonad, Чтобы игнорировать результат, _ можно использовать, таким образом, _ <- someMonad читает как выполнить монаду, но игнорирует результат.
  4. Это все методы IO определения, они ничего не запускают и, следовательно, нет побочного эффекта. Побочные эффекты появляются только тогда, когда IO.run называется.
Другие вопросы по тегам