Производительность в области функционального программирования в scala

#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)
      }
  }
})
 

У меня есть пара вопросов, касающихся этого процесса:

  1. Моя вторая версия не кажется мне такой чистой и простой, как первая версия, я не уверен, связано ли это с тем, что я не так хорошо знаком с функциональным программированием, или я просто написал плохой функциональный код.
  2. Есть ли какой-то способ упростить синтаксис на втором фрагменте? Особенно (m, item) => ??? функция в этой foldLeft части? Что-то вроде (m, (point, operation)) => ??? дает мне синтаксическую ошибку
  3. Выполнение второго фрагмента кода занимает значительно больше времени, что меня немного удивляет, поскольку эти два кода по сути делают одно и то же, поскольку у меня не слишком много фона Java, есть идеи, что может вызвать проблему с производительностью?

Большое спасибо!

Ответ №1:

С точки зрения функционального программирования, ваш код страдает от того, что…

  1. lightStatus Карта «поддерживает состояние» и, следовательно, требует мутации.
  2. Большая «область» изменений статуса == большое количество обновлений данных.

Если вы можете принять каждый статус освещения в качестве 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 зависит от частоты изменений и структуры ваших данных.