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

person injoy    schedule 06.01.2019    source источник


Ответы (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 удаляет часть операций ввода-вывода, а в том, что он откладывает ее на более поздний момент времени. Обычно мы говорим, что ввод-вывод выполняется на «краях» приложения в методе Main. Эти взаимодействия с внешним миром по-прежнему будут происходить, но поскольку мы инкапсулируем их внутри IO, мы можем рассуждать о них как о значениях в нашей программе, что приносит много пользы. Например, теперь мы можем компоновать побочные эффекты и зависеть от успеха/неудачи их выполнения. Мы можем смоделировать эти эффекты ввода-вывода (используя другие типы данных, такие как Const), и многие другие удивительно приятные свойства.

person Yuval Itzchakov    schedule 06.01.2019
comment
Привет @Yuval Большое спасибо! Кстати, могу я узнать, какая функция будет назначена a в IO? И как мы будем вызывать IO.run? - person injoy; 06.01.2019
comment
@injoy a будет любой функцией, которую вы предоставите. Например, одно из ваших выражений — IO { readLine }, где мы назначаем функцию чтения строк как a. - person Yuval Itzchakov; 07.01.2019
comment
Извините, я имею в виду три строки внутри для понимания всех возвращаемых типов ввода-вывода. На самом деле они не выводят строки на стандартный вывод, верно? Так как же мы можем напечатать эти строки? Благодарю. - person injoy; 07.01.2019
comment
@injoy После того, как вы вернете значение IO[Unit] из метода converter, вам нужно вызвать run: converter.run. Обратите внимание еще раз, что запуск ввода-вывода должен происходить на краях вашей программы. - person Yuval Itzchakov; 07.01.2019
comment
Привет @Yuval, как для понимания вернуть значение IO [Unit]? Я думал, что _ <- PrintLine(fahrenheitToCelsius(d).toString) отбросит значение ввода-вывода, верно? Кроме того, что делает yield ()? Спасибо. - person injoy; 07.01.2019
comment
@injoy Он не отбрасывает IO, он отбрасывает значение Unit, возвращаемое внутри IO. yield () даст значение единицы. - person Yuval Itzchakov; 09.01.2019

Самый простой способ взглянуть на монаду 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.
person Ivan Stanislavciuc    schedule 06.01.2019