В настоящее время я пытаюсь применить более функциональный стиль программирования к проекту с использованием низкоуровневого (основанного на LWJGL) графического интерфейса. Очевидно, что в таком случае необходимо переносить много состояний, которые изменяются в текущей версии. Моя цель состоит в том, чтобы в конечном итоге иметь совершенно неизменное состояние, чтобы избежать изменений состояния в качестве побочного эффекта. Я некоторое время изучал линзы сказаза и монады монахов, но моя главная проблема остается: все эти методы основаны на копировании на запись. Поскольку мое состояние имеет как большое количество полей, так и некоторые поля значительного размера, я беспокоюсь о производительности.
Насколько я знаю, наиболее распространенным подходом к модификации неизменяемых объектов является использование сгенерированного метода copy
для case class
(это также то, что делают линзы под капотом). Мой первый вопрос: как этот метод copy
действительно реализован? Я провел несколько экспериментов с таким классом, как:
case class State(
innocentField: Int,
largeMap: Map[Int, Int],
largeArray: Array[Int]
)
Сравнивая результаты, а также просматривая вывод -Xprof
, похоже, что обновление someState.copy(innocentField = 42)
фактически выполняет глубокую копию, и я наблюдаю значительное снижение производительности при увеличении размера largeMap
и largeArray
. Я как-то ожидал, что вновь построенный экземпляр разделяет ссылки на объекты исходного состояния, так как внутренне ссылка должна просто передаваться конструктору. Могу ли я каким-то образом заставить или отключить это глубокое поведение по умолчанию по умолчанию copy
?
Раздумывая над проблемой копирования на запись, мне было интересно, есть ли более общие решения этой проблемы в FP, которые хранят изменения неизменяемых данных в виде поэтапного пути (в смысле "сбора обновлений", или "сбор изменений" ). К моему удивлению, я ничего не мог найти, поэтому я попробовал следующее:
// example state with just two fields
trait State {
def getName: String
def getX: Int
def setName(updated: String): State = new CachedState(this) {
override def getName: String = updated
}
def setX(updated: Int): State = new CachedState(this) {
override def getX: Int = updated
}
// convenient modifiers
def modName(f: String => String) = setName(f(getName))
def modX(f: Int => Int) = setX(f(getX))
def build(): State = new BasicState(getName, getX)
}
// actual (full) implementation of State
class BasicState(
val getName: String,
val getX: Int
) extends State
// CachedState delegates all getters to another state
class CachedState(oldState: State) extends State {
def getName = oldState.getName
def getX = oldState.getX
}
Теперь это позволяет сделать что-то вроде этого:
var s: State = new BasicState("hello", 42)
// updating single fields does not copy
s = s.setName("world")
s = s.setX(0)
// after a certain number of "wrappings"
// we can extract (i.e. copy) a normal instance
val ns = s.setName("ok").setX(40).modX(_ + 2).build()
Теперь мой вопрос: что вы думаете об этом проекте? Является ли это своего рода шаблоном проектирования FP, о котором я не знаю (помимо сходства с шаблоном Builder)? Поскольку я не нашел ничего подобного, мне интересно, есть ли какая-то серьезная проблема с этим подходом? Или существуют ли более стандартные способы решения узкого места копирования-на-записи, не отказываясь от неизменности?
Есть ли даже возможность унифицировать функции get/set/mod каким-то образом?
Edit:
Мое предположение о том, что copy
выполняет глубокую копию, действительно было неправильным.