Когато създаваме неизменна структура от данни, често трябва да имаме програма, която съдържа някакво състояние, което да се променя по време на изпълнението.

Един пример е създаването на неизменна опашка. В библиотеката на 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 за повече публикации като тази.