Любая полезная программа использует побочные эффекты. В FP мы пытаемся подтолкнуть эти побочные действия к краям программы, пытаясь сохранить как можно больше кода.
В конце концов будет место, где будут возникать побочные эффекты, если вы используете государственную монаду или какую-либо другую технику.
Обычно у вас есть чистая функция, которая принимает некоторое состояние и преобразует ее в новое состояние, которое возвращается обратно вызывающему и вместе с результатом. Это можно сделать либо с обычной передачей аргумента, либо с состоянием, состоящим из государственной монады с преобразованиями, выполняющимися в ее контексте.
простой аргумент прохождения пример:
object StateExample extends App {
//--- start of pure part of your program ---//
type PhoneNumber = String
type CallState = Map[PhoneNumber, User]
case class User(username: String)
def startCall(calls: CallState, caller: User, phone: PhoneNumber): CallState =
calls + (phone -> caller)
def finishCall(calls: CallState, phone: PhoneNumber): CallState =
calls - phone
def startCallingPeople(calls: CallState) = {
val intermediateState1 = startCall(calls, User("one"), "123")
val intermediateState2 = startCall(intermediateState1, User("two"), "456")
intermediateState2
}
def hangupCalls(calls: CallState) = {
val intermediateState1 = finishCall(calls, "123")
val intermediateState2 = finishCall(intermediateState1, "456")
intermediateState2
}
// --- end of pure part of the program ---//
// --- start of impure part of your program ---//
var callState: CallState = Map()
def runSimulation(): Unit = {
println(s"BEFORE ANY CALLS: $callState")
callState = startCallingPeople(callState)
println(s"AFTER CALLING 2 PEOPLE: $callState")
callState = hangupCalls(callState)
println(s"AFTER HANGING UP: $callState")
}
runSimulation()
// --- end of impure part of your program ---//
}
печатает:
BEFORE ANY CALLS: Map()
AFTER CALLING 2 PEOPLE: Map(123 -> User(one), 456 -> User(two))
AFTER HANGING UP: Map()
Посмотрите, как состояние передается от функции к функции и трансформировали состояние который генерируется и передается дальше. В этой «чистой» части кода нет побочных эффектов, он только описывает, как вещи должны быть преобразованы. «Нечистая» часть кода выполняет грязную работу по использованию остальной части программы и выполняет побочные эффекты.
В качестве альтернативы вы можете использовать государственную монаду в «чистой» части программы и запустить ее в «нечистой» части и сохранить созданное состояние так же, как состояние сохранено в var
выше. Ниже приведен пример:
import scalaz.State
object StateMonadExample extends App {
def startCall(caller: User, phone: PhoneNumber): State[CallState, Unit] =
State { s => s + (phone -> caller) ->() }
def finishCall(phone: PhoneNumber): State[CallState, Unit] =
State { s => (s - phone) ->() }
def startCallingPeople: State[CallState, Unit] =
startCall(User("one"), "123").flatMap(_ => startCall(User("two"), "456"))
def hangupCalls: State[CallState, Unit] =
finishCall("123").flatMap(_ => finishCall("456"))
// mutable part of your program
var callState: CallState = Map()
// side effecting part of your program
def runSimulation(): Unit = {
test1() // intermediate state being saved
test2() // intermediate state being passed through
test3() // same as test 2 but also outputs intermediate state without updating it as test 1 does
}
def test1(): Unit = {
// reset initial state just in case
callState = Map()
println("TEST 1:")
println(s"BEFORE ANY CALLS: $callState")
callState = startCallingPeople.run(callState)._1
println(s"AFTER CALLING 2 PEOPLE: $callState")
callState = hangupCalls.run(callState)._1
println(s"AFTER HANGING UP: $callState")
println("END OF TEST 1.\n")
}
def test2(): Unit = {
// reset initial state just in case
callState = Map()
println("TEST 2:")
println(s"BEFORE ANY CALLS: $callState")
val computation = for {
_ <- startCallingPeople
_ <- hangupCalls
} yield()
callState = computation.run(callState)._1
println(s"AFTER CALL AND HANGUP: $callState")
println("END OF TEST 2.\n")
}
def test3(): Unit = {
// reset initial state just in case
callState = Map()
println("TEST 3:")
println(s"BEFORE ANY CALLS: $callState")
val computation = for {
s1 <- startCallingPeople.flatMap(_ => State { s: CallState => println(s"AFTER CALLING 2 PEOPLE: $s"); s ->() })
_ <- hangupCalls
} yield()
callState = computation.run(callState)._1
println(s"AFTER HANGING UP: $callState")
println("END OF TEST 3.\n")
}
runSimulation()
}
печатает:
TEST 1:
BEFORE ANY CALLS: Map()
AFTER CALLING 2 PEOPLE: Map(123 -> User(one), 456 -> User(two))
AFTER HANGING UP: Map()
END OF TEST 1.
TEST 2:
BEFORE ANY CALLS: Map()
AFTER CALL AND HANGUP: Map()
END OF TEST 2.
TEST 3:
BEFORE ANY CALLS: Map()
AFTER CALLING 2 PEOPLE: Map(123 -> User(one), 456 -> User(two))
AFTER HANGING UP: Map()
END OF TEST 3.
Обратите внимание, что State
монады заботится о прохождении через состояние. По существу, мы просто собираем кучу вычислений, а затем выполняем их, вызывая run
и передавая начальное состояние.
Когда учитывается параллелизм, вы можете применять те же принципы в меньшем масштабе, но некоторые вещи начинают разрушаться. Например, если вы позволите двум потокам обновить одно и то же состояние, вам нужно будет синхронизировать его и убедиться, что ни один из этих потоков не читает устаревшую версию состояния. Синхронизация приводит к блокировке и замедлению работы программы.
Практический подход состоит в том, чтобы либо сохранить состояние извне в некоторой базе данных (управляет синхронизацией для вас), либо как-то избежать синхронизации. Если бы мне пришлось сохранить состояние в памяти, я бы, вероятно, использовал Akka и представлял каждый активный звонок в качестве актера. Актеры могут безопасно инкапсулировать изменяемое состояние, потому что они обрабатывают каждое сообщение последовательно. Когда звонок закончен, я убью актера, чтобы освободить ресурсы. Вы можете разделить ваше приложение по-другому - возможно, вместо того, чтобы иметь одного актера за звонок, у вас может быть один актер на один коммутатор. Это действительно зависит от требований. Обратите внимание, что актеры принимают изменчивость, поэтому это не чистое решение FP.
Вывод состоит в том, что в конечном итоге у вас появятся побочные эффекты, но вы должны знать, как минимизировать и изолировать их от остальной части программы.
Посмотреть полный проект здесь: https://github.com/izmailoff/scala-state-example
Я начал писать ответ, но это поворот в BlogPost :). Я постараюсь закончить, как только смогу ... –
Большое спасибо. С нетерпением жду вашего сообщения. –
Добавлен пример государственной монады. Не уверен, найду ли я время, чтобы написать пример Акки - может быть. Надеюсь, это ясно. –