Scala IO monad: какой смысл?
Недавно я просмотрел видео о том, как вы могли прийти к монаде IO, разговор был в scala. Мне действительно интересно, что из-за того, что функции возвращают из них IO [A]. Лямбда-выражения, обернутые в объект IO, являются мутациями и в какой-то момент выше изменения, которое они должны соблюдать, я имею в виду выполненный, так что что-то происходит. Вы не просто подталкиваете проблему выше дерева где-то еще?
Единственное, что я вижу, это то, что он позволяет ленивую оценку в том смысле, что если вы не вызываете операцию unsafePerformIO, побочных эффектов не возникает. Кроме того, я предполагаю, что другие части программы могут использовать/совместно использовать код и децитировать, когда он хочет, чтобы возникали побочные эффекты.
Мне было интересно, все ли это? Есть ли преимущества в тестируемости? Я предполагаю, что не так, как вам нужно было бы наблюдать эффекты, которые отрицают это. Если вы использовали черты/интерфейсы, вы можете управлять зависимостями, но не тогда, когда эффекты происходят в этих зависимостях.
Я собрал следующий код в коде.
case class IO[+A](val ra: () => A){
def unsafePerformIO() : A = ra();
def map[B](f: A => B) : IO[B] = IO[B]( () => f(unsafePerformIO()))
def flatMap[B](f: A => IO[B]) : IO[B] = {
IO( () => f(ra()).unsafePerformIO())
}
}
case class Person(age: Int, name: String)
object Runner {
def getOlderPerson(p1: Person,p2:Person) : Person =
if(p1.age > p2.age)
p1
else
p2
def printOlder(p1: Person, p2: Person): IO[Unit] = {
IO( () => println(getOlderPerson(p1,p2)) ).map( x => println("Next") )
}
def printPerson(p:Person) = IO(() => {
println(p)
p
})
def main(args: Array[String]): Unit = {
val result = printPerson(Person(31,"Blair")).flatMap(a => printPerson(Person(23,"Tom"))
.flatMap(b => printOlder(a,b)))
result.unsafePerformIO()
}
}
Вы можете видеть, как эффекты откладываются до тех пор, пока не станет основной. Я придумал это после того, как почувствовал это из видео.
Является ли моя реализация правильной и правильно ли я понимаю.
Мне также интересно, следует ли использовать milage, он должен быть объединен с ValidationMonad, как в ValidationMonad [IO [Person]], чтобы мы могли коротко замыкаться, когда происходят исключения? Мысли, пожалуйста.
Blair
Ответы
Ответ 1
Для сигнатуры типа функции важно записать, имеет ли он побочные эффекты. У вашей реализации IO есть ценность, потому что это действительно так. Это делает ваш код лучше документированным; и если вы реорганизуете свой код, чтобы отделить, насколько это возможно, логику, которая включает IO из логики, которая этого не делает, вы сделали функции, не связанные с IO, более сложными и более проверяемыми. Вы можете сделать тот же рефакторинг без явного типа ввода-вывода; но использование явного типа означает, что компилятор может помочь вам выполнить разделение.
Но это только начало. В коде в вашем вопросе действия IO кодируются как лямбда, и поэтому непрозрачны; вы ничего не можете сделать с действием ввода-вывода, кроме запуска его, и его эффект при запуске жестко запрограммирован.
Это не единственный возможный способ реализации монады IO.
Например, я могу использовать классы case IO, которые расширяют общую черту. Затем я могу, например, написать тест, который запускает функцию, и видит, возвращает ли он правильное действие IO.
В тех классах, которые представляют разные виды операций ввода-вывода, я не могу включать жестко закодированные реализации действий, которые выполняются при запуске. Вместо этого я мог бы отделить это от использования шаблона typeclass. Это позволит менять в разных реализациях действия IO. Например, у меня может быть один набор реализаций, которые общаются с производственной базой данных, и другой набор, который говорит о том, что в целях тестирования используется макет базы данных в памяти.
В главе 13 ( "Внешние эффекты и ввод-вывод" ) функциональное программирование Бьярнасона и Чиусано хорошо описано в Scala. См. Особенно 13.2.2, "Преимущества и недостатки простого типа ввода-вывода".
UPDATE (декабрь 2015 г.): re "свопинг в разных реализациях действий IO", в наши дни все больше людей используют "свободную монаду" для такого рода вещей; см., например, John De Goes: "" Современная архитектура для FP".
Ответ 2
Преимущество использования IO-монады состоит в чистых программах. Вы не выдвигаете побочные эффекты выше цепи, но устраняете их. Если у вас есть нечистая функция, например:
def greet {
println("What is your name?")
val name = readLine
println(s"Hello, $name!")
}
Вы можете удалить побочный эффект, переписав его на:
def greet: IO[Unit] = for {
_ <- putStrLn("What is your name?")
name <- readLn
_ <- putStrLn(s"Hello, $name!")
} yield ()
Вторая функция является ссылочно прозрачной.
Очень хорошее объяснение, почему использование модов IO приводит к чистым программам, можно найти в Rúnar Bjarnason слайды от scala.io( видео можно найти здесь).