Когато създаваме неизменна структура от данни, често трябва да имаме програма, която съдържа някакво състояние, което да се променя по време на изпълнението.
Един пример е създаването на неизменна опашка. В библиотеката на scala сте инициализирали неизменна опашка по следния начин:
val empty = Queue[Int]()
След това можете да поставите опашка, която връща нова опашка с актуализирания елемент. Можете също така да премахнете опашка от опашка, която връща кортеж от елемента, който премахвате, и новата опашка.
val one = empty.enqueue(1)
val (one, emptyQ) = one.dequeue()
Ако обаче искате да извършите поредица от операции с неизменната опашка, трябва да предадете новия елемент на следващата операция. Като този:
val one = empty.enqueue(1)
val two = one.enqueue(2) // enqueue from one
val three = two.enqueue(3) // enqueue from two
Може да е податливо на грешки, ако трябва да извършите много операции чрез изрично прехвърляне на едно състояние към друго.
В тази статия искам да споделя как можете да използвате монадата cats State за конструиране на опашка. Чрез използването на State monad, конструирането на неизменна структура от данни не е необходимо да предава едно състояние на друго изрично. Следователно, намалява количеството податливи на грешки шаблони.
Ред за изпълнение
Започваме с прилагането на обикновената неизменна опашка, която показва същата операция на обикновена неизменна опашка на scala. След това прилагаме същата неизменна опашка, но с Cats State Monad.
Отказ от отговорност: изпълнението на опашката е високо
Създайте обикновена неизменна опашка
Нека създадем конструктора на опашката:
class FunctionalQueue[+A](vector:Vector[A])
Основният клас на FunctionalQueue съдържа вектор, който съдържа всички елементи, които са поставени в опашка или от опашка.
Нека внедрим функцията за поставяне в опашка и изваждане от опашка:
def enqueue[B >: A](elmt:B): FunctionalQueue[B] = new FunctionalQueue(vector :+ elmt)
def dequeue: (A, FunctionalQueue[A]) = (vector.head, new FunctionalQueue[A](vector.tail))
Функцията за поставяне и изваждане от опашката просто добавя стойността към вектора и извлича стойността от вектора.
Сега добавете фабричен метод за конструктора FunctionalQueue, като дефинирате придружаващия обект.
object FunctionalQueue {
def apply[A]():FunctionalQueue[A] = new FunctionalQueue[A](Vector.empty[A])
}
Можете да извикате функцията в main, като това:
println(s"creating immutable queue without State monad")
val functionalQueue = FunctionalQueue[Int]
println(s"enqueue 1 immutable queue")
val enqueue1 = functionalQueue.enqueue(1)
println(s"enqueue 2 immutable queue")
val enqueue2 = enqueue1.enqueue(2)
println(s"front ${enqueue2.front}")
val (head, rest) = enqueue2.dequeue
println(s"dequeue head: ${head} rest : ${rest}")
Какво е държавна монада
Според Scala с котки, монадата State ни позволява да предаваме допълнително състояние като част от изчисление.
Представянето на екземпляра State е State[S,A]
, където представлява функцията S => (S,A)
.
Това означава, че приема някакво състояние и връща резултат заедно с новоизчисленото състояние.
Нека се опитаме да създадем просто състояние:
import cats.data.State
val a = State[Int,String] {integerState =>
(integerState, s"The state is ${state}")
}
Състоянието свързва всички изчисления, преди първата входна променлива да е готова за предаване. След като цялата програма е свързана, можете да преминете в първоначалното състояние и да изпълните run
, за да получите очакваното крайно състояние и неговия резултат.
val (endState, result) = a.run(2).value // 2 is the initial input that is passed in
// endState: 2 result : The state is 2
Силата на държавата се крие в картата и функционалността на flatMap. Той може да пренася състоянието от един екземпляр в друг.
Всяко състояние представлява индивидуална трансформация и можете да ги комбинирате, като използвате flatMap, за да трансформирате пълната последователност от промени:
В примера по-долу plus1
и plus2
връща стойност на изчисленото ново състояние и историята на описанието на това изчисление.
import cats.data.State
val plus1 = State[Int, String]{state =>
(state+1, s"The result of this state is ${state+1}")
}
val plus2 = State[Int,String] {state =>
(state +2, s"The result of this state is ${state+2}")
}
val program = for {
historyOne <- plus1 // historyOne is the String
historyTwo <- plus2
} yield List(historyOne, historyTwo)
val (result, history) = program.run(0).value
// result = 3
// history = List("The result of this state is 1","The result of this state is 3" )
Както можете да видите, plus1
и plus2
са свързани, дори ако не взаимодействаме с тях за разбиране.
Рефакторинг на функционална опашка със състояние монада
След като вече знаете как работи State monad, нека преработим Functional Queue с помощта на State monad.
Приложете enqueue
и dequeue
с монада State.
type QueueFunc[A] = State[Vector[A], Option[A]]
def enqueue[A](elmt:A): QueueFunc[A] = State[Vector[A], Option[A]]{ oldVector =>
(oldVector :+ elmt, oldVector.headOption)
}
def dequeue[A]: QueueFunc[A] = State[Vector[A],Option[A]] { oldVector =>
(oldVector.tail, oldVector.headOption)
}
Създадох QueueFunc
като псевдоним на тип, който представлява State[Vector[A], Option[A]]
. Състоянието съдържа вектор, който съдържа тип A
и незадължителната глава на опашката. Функциите, поставяне в опашка и изваждане от опашка, приемат стар вектор и добавят или премахват главата на вектора.
Ето го, направихме цялата си реализация на неизменната опашка със състояние!
Как изпълнявате функцията?
Нека изпълним същата операция като FunctionalQueue
с нашата нова реализация.
Не забравяйте, че използваме flatMap, за да комбинираме всяка операция, без наистина да взаимодействаме с нейното актуализирано състояние.
Предоставяме всички очаквани стъпки на изпълнение. След това го свързваме с програма, която предоставя първоначалното му състояние и изпълнява функцията run
. В този случай първоначалната стойност е празен вектор.
// supply our operation
val program = for {
_ <- enqueue[Int](1)
_ <- enqueue[Int](2)
end <- dequeue[Int]
} yield end
val (newState, head) = program.run(Vector.empty[Int]).value
// newState = Vector(2)
// head = 1
За вкъщи
- Монадата State ви помага да елиминирате целия предразположен към грешки шаблонен код, който предава актуализираното състояние към следващата операция.
- Екземплярът на монада State преминава в състояние и връща резултата заедно с актуализираното си състояние.
- Силата на State monad разчита на операциите
map
иflatMap
, които свързват един екземпляр към друг. Всеки екземпляр на състояние представлява атомна трансформация. Тяхната комбинация представлява последователност от промени. Не е необходимо да взаимодействате с междинното състояние в за разбиране.
Цялата информация и пример са в Github.
Информацията на GitHub има 3 различни подхода за внедряване на неизменна опашка. Първият е обикновената неизменна опашка без монада състояние (първи пример). Вторият имитира обикновения неизменен интерфейс на опашката с монада State - последното стабилно състояние на изпълнение с монада State (2-ри пример от тази статия).
Благодаря, че прочетохте! Ако харесвате тази публикация, не се колебайте да се абонирате за моя бюлетин, за да получавате седмични есета за кариера в технологиите, интересни връзки и съдържание!
Можете да ме следвате и в Medium за повече публикации като тази.