Работа с нечистыми побочными эффектами в FP, IO Monad - PullRequest
4 голосов
/ 16 мая 2019

Пытаясь понять, как лучше всего справляться с побочными эффектами в FP.

Я реализовал эту элементарную реализацию ввода-вывода:

  trait IO[A] {
    def run: A
  }
  object IO {
    def unit[A](a: => A): IO[A] = new IO[A] { def run = a }
    def loadFile(fileResourcePath: String) = IO.unit[List[String]]{ 
        Source.fromResource(fileResourcePath).getLines.toList }
    def printMessage(message: String) = IO.unit[Unit]{ println(message) }
    def readLine(message:String) = IO.unit[String]{ StdIn.readLine() }
  }

У меня есть следующий вариант использования:

- load lines from log file
- parse each line to BusinessType object
- process each BusinessType object
- print process result

Случай 1: Таким образом, код Scala может выглядеть следующим образом

val load: String => List[String]
val parse: List[String] => List[BusinessType]
val process: List[BusinessType] => String
val output: String => Unit

Случай 2: Я решил использовать IO выше:

val load: String => IO[List[String]]
val parse: IO[List[String]] => List[BusinessType]
val process: List[BusinessType] => IO[Unit]
val output: IO[Unit] => Unit

В случае 1 нагрузка является нечистой, потому чтоон читает из файла, поэтому вывод также нечистый, потому что он записывает результат в консоль.

Для большей функциональности я использую случай 2.

Вопросы:

- Aren't case 1 and 2 really the same thing?
- In case 2 aren't we just delaying the inevitable? 
  as the parse function will need to call the io.run 
  method and cause a side-effect?
- when they say "leave side-effects until the end of the world" 
  how does this apply to the example above? where is the 
  end of the world here?

1 Ответ

6 голосов
/ 16 мая 2019

Кажется, что вашей монаде ввода / вывода не хватает всего монадного материала, а именно той части, где вы можете flatMap использовать ее для создания большего ввода / вывода из меньшего ввода / вывода.Таким образом, все остается «чистым» до самого звонка run в самом конце.

В случае 2 мы не просто откладываем неизбежное?так как функция разбора должна будет вызвать метод io.run и вызвать побочный эффект?

Нет.Функция parse не должна вызывать io.run.Он должен вернуть еще один IO, который вы можете затем объединить с его входным IO.

когда говорят «оставить побочные эффекты до конца света», как это относится к приведенному выше примеру?где конец света здесь?

Конец света будет последним, что сделает ваша программа.Вы только run один раз.Остальная часть вашей программы «чисто» строит для этого один гигантский ввод-вывод.

Что-то вроде

def load(): IO[Seq[String]]    
def parse(data: Seq[String]): IO[Parsed]  // returns IO, because has side-effects
def pureComputation(data: Parsed): Result  // no side-effects, no need to use I/O
def output(data: Result): IO[Unit]

// combining effects is "pure", so the whole thing
// can be a `val` (or a `def` if it takes some input params)
val program: IO[Unit] = for {
      data <- load()    // use <- to "map" over IO
      parsed <- parse()
      result = pureComputation(parsed)  // = instead of <-, no I/O here 
      _ <- output(result)
   } yield ()

// only `run` at the end produces any effects
def main() {
   program.run()
}
...