#scala #functional-programming #immutability
Вопрос:
Я работаю с нижеприведенными материалами, чтобы изучить функциональное программирование и scala, я вырос на python.
case class Point(x: Int, y:Int)
object Operation extends Enumeration {
type Operation = Value
val TurnOn, TurnOff, Toggle = Value
}
object Status extends Enumeration {
type Status = Value
val On, Off = Value
}
val inputs: List[String]
def parseInputs(s: String): (Point, Point, Operation)
Идея состоит в том, что у нас есть матрица света ( Point
), каждая Point
из которых может быть либо On
такой, либо Off
такой, как описано в Status
.
Мои входные данные представляют собой серию команд, в которых запрашивается либо TurnOn
один , TurnOff
либо Toggle
все огни от одного Point
к другому Point
(прямоугольная область, определенная с помощью двух точек, находится в левом нижнем углу и правом верхнем углу).
Мое первоначальное решение выглядит так:
type LightStatus = mutable.Map[Point, Status]
val lightStatus = mutable.Map[Point, Status]()
def updateStatus(p1: Point, p2: Point, op: Operation): Unit = {
(p1, p2) match {
case (Point(x1, y1), Point(x2, y2)) =>
for (x <- x1 to x2)
for (y <- y1 to y2) {
val p = Point(x, y)
val currentStatus = lightStatus.getOrElse(p, Off)
(op, currentStatus) match {
case (TurnOn, _) => lightStatus.update(p, On)
case (TurnOff, _) => lightStatus.update(p, Off)
case (Toggle, On) => lightStatus.update(p, Off)
case (Toggle, Off) => lightStatus.update(p, On)
}
}
}
}
for ((p1, p2, op) <- inputs.map(parseInputs)) {
updateStatus(p1, p2, op)
}
Теперь у меня есть lightStatus
карта для описания конечного состояния всей матрицы. Это работает, но кажется мне менее функциональным, так как я использовал изменяемую карту вместо неизменяемого объекта, поэтому я попытался переформулировать это в более функциональный способ, в итоге я получил следующее:
inputs.flatMap(s => parseInputs(s) match {
case (Point(x1, y1), Point(x2, y2), op) =>
for (x <- x1 to x2;
y <- y1 to y2)
yield (Point(x, y), op)
}).foldLeft(Map[Point, Status]())((m, item) => {
item match {
case (p, op) =>
val currentStatus = m.getOrElse(p, Off)
(op, currentStatus) match {
case (TurnOn, _) => m.updated(p, On)
case (TurnOff, _) => m.updated(p, Off)
case (Toggle, On) => m.updated(p, Off)
case (Toggle, Off) => m.updated(p, On)
}
}
})
У меня есть пара вопросов, касающихся этого процесса:
- Моя вторая версия не кажется мне такой чистой и простой, как первая версия, я не уверен, связано ли это с тем, что я не так хорошо знаком с функциональным программированием, или я просто написал плохой функциональный код.
- Есть ли какой-то способ упростить синтаксис на втором фрагменте? Особенно
(m, item) => ???
функция в этойfoldLeft
части? Что-то вроде(m, (point, operation)) => ???
дает мне синтаксическую ошибку - Выполнение второго фрагмента кода занимает значительно больше времени, что меня немного удивляет, поскольку эти два кода по сути делают одно и то же, поскольку у меня не слишком много фона Java, есть идеи, что может вызвать проблему с производительностью?
Большое спасибо!
Ответ №1:
С точки зрения функционального программирования, ваш код страдает от того, что…
lightStatus
Карта «поддерживает состояние» и, следовательно, требует мутации.- Большая «область» изменений статуса == большое количество обновлений данных.
Если вы можете принять каждый статус освещения в качестве Boolean
значения, вот дизайн, который не требует мутаций и имеет быстрое обновление статуса даже на очень больших площадях.
case class Point(x: Int, y:Int)
class LightGrid private (status: Point => Boolean) {
def apply(p: Point): Boolean = status(p)
private def isWithin(p:Point, ll:Point, ur:Point) =
ll.x <= p.x amp;amp; ll.y <= p.y amp;amp; p.x <= ur.x amp;amp; p.y <= ur.y
//each light op returns a new LightGrid
def turnOn(lowerLeft: Point, upperRight: Point): LightGrid =
new LightGrid(point =>
isWithin(point, lowerLeft, upperRight) || status(point))
def turnOff(lowerLeft: Point, upperRight: Point): LightGrid =
new LightGrid(point =>
!isWithin(point, lowerLeft, upperRight) amp;amp; status(point))
def toggle(lowerLeft: Point, upperRight: Point): LightGrid =
new LightGrid(point =>
isWithin(point, lowerLeft, upperRight) ^ status(point))
}
object LightGrid { //the public constructor
def apply(): LightGrid = new LightGrid(_ => false)
}
использование:
val ON = true
val OFF = false
val lg = LightGrid().turnOn(Point(2,2), Point(11,11)) //easy numbers
.turnOff(Point(8,8), Point(10,10))
.toggle(Point(1,1), Point(9,9))
lg(Point(1,1)) //ON
lg(Point(7,7)) //OFF
lg(Point(8,8)) //ON
lg(Point(9,9)) //ON
lg(Point(10,10)) //OFF
lg(Point(11,11)) //ON
lg(Point(12,12)) //OFF
Комментарии:
1. Итак, вы хотите сказать, что… в этом случае, возможно, лучше использовать изменяемую карту, а не строго следовать функциональным принципам? А что касается ваших вопросов, к сожалению, в дальнейшем мне потребуется поддерживать яркость каждого источника света, поэтому технически я не могу выполнить простое
true
/false
2. Не все способы использования отлично подходят для функционального программирования. Один из них-это когда вы должны поддерживать значительно большие и динамические данные о состоянии. Если вы будете следовать FP, вам всегда придется воссоздавать новое состояние, что может привести к значительно большему сбору мусора (особенно в случае моделей вложенных состояний) по сравнению с изменяемым решением.
3. @sarveshseri-Даже если государство большое и сложное, если вы сохраняете большую его часть, новое государство будет делиться большей частью своей структуры со старым государством. Я не думаю, что GC станет шоуменом. При подходе, отличном от FP, если вы обращаетесь к большим структурам данных, чтобы настроить их, вам будет трудно доказать себе, что все и только те фрагменты кода, которые могут достичь измененной части, должны видеть измененную версию.
4. влияние @airfoyle GC зависит от частоты изменений и структуры ваших данных.