# Оптимизация Scala-кода

## View и withFilter для оптимизации коллекций
1. **View** - это особый вид коллекции в **Scala**, который берет базовую коллекцию и лениво выполняет методы преобразования в этой коллекции.  

   Мы можем преобразовать каждую коллекцию Scala в отложенное представление и обратно с помощью `view` метода. Вместо создания новой коллекции после каждой операции, они применяют сразу всю цепочку преобразований к каждому элементу оригинальной коллекции, благодаря чему цепочка преобразований выполняется за один проход.

    Преобразование виртуальной коллекции в реальную выполняется либо одним из преобразующих методов из серии to{Название коллекции}, либо методом force.

Пример:
```scala
// До
seq.map(f).flatMap(g).filter(p)

// После
seq.view.map(f).flatMap(g).filter(p).force
```

2. `withFilter` работает так же, как и `view` — создает временный объект, который ограничивает область последующих преобразований коллекции (так, что он реорганизует возможные побочные эффекты). Однако, нет нужды явно преобразовывать коллекцию к (или наоборот) от временного представления (вызвав view и force).
  Если `filter` используется перед `map`, `flatMap` или `foreach`, то для лучшей производительности вместо него должен использоваться `withFilter`.

Пример:
```scala
// До
Stm_OrderInDetApi()
      .byParent(rop)
      .filter(f => f.get(_.idStock).isNotNull)
      .foreach { ropDet =>
        if (Stk_StockApi().load(ropDet.get(_.idStock)).get(_.idBisObj).isDistinct(value))
          Stm_OrderInDetApi().setidStock(ropDet, None.nl)
      }

// После
Stm_OrderInDetApi()
      .byParent(rop)
      .withFilter(f => f.get(_.idStock).isNotNull)
      .foreach { ropDet =>
        if (Stk_StockApi().load(ropDet.get(_.idStock)).get(_.idBisObj).isDistinct(value))
          Stm_OrderInDetApi().setidStock(ropDet, None.nl)
      }
```

## Объединение условий фильтрации
Пример:
```scala
// До
seq.filter(p1).filter(p2)

// После
seq.filter(x => p1(x) && p2(x))
```

## Отложенные вычисления с lazy val
Компилятор не сразу вычисляет связанное выражение отложенного значения val. Он вычисляет переменную только при первом обращении к ней.

Пример:
```scala
// До
val idgForming = Btk_ClassStateApi().findByNameAndIdClass("Forming", idClass)

// После
lazy val idgForming = Btk_ClassStateApi().findByNameAndIdClass("Forming", idClass)
```

## Выбор коллекции по сложности поиска

Пример:
```scala
  //до:
    def registerByParent(ropDoc: RopDoc, idap: CSeq[NLong]): Unit = {
      val idavAlreadyRegistered = byParent(ropDoc).map(_.get(_.idGdsType)).toList

      idap.foreach { idv =>
        if (idv.isNotNull) {
          if (!idavAlreadyRegistered.contains(idv)) {
            val rop = // ...
          }
        }
      }
    }

  //после:
    def registerByParent(ropDoc: RopDoc, idap: CSeq[NLong]): Unit = {
      val idavAlreadyRegistered = byParent(ropDoc).map(_.get(_.idGdsType)).toSet

      idap.foreach { idv =>
        if (idv.isNotNull) {
          if (!idavAlreadyRegistered(idv)) {
            val rop = // ...
          }
        }
      }
    }
```

## Защита циклов от зацикливания
Пример:
```scala
// До
while (dotsNeedUpdate.nonEmpty) {
  val newDotsNeedUpdate = scala.collection.mutable.HashSet[NNumber]()
  ???
  dotsNeedUpdate = newDotsNeedUpdate.toSet
}
// После
var count = 0
while (dotsNeedUpdate.nonEmpty && count < 10000) {
  val newDotsNeedUpdate = scala.collection.mutable.HashSet[NNumber]()
  ???
  dotsNeedUpdate = newDotsNeedUpdate.toSet
  count += 1
}
```

## Встроенные методы вместо эмуляции

Пример:
```scala
// До
seq.map(f).flatten
seq.toSet.toSeq
seq.reverse.iterator
seq.reverse.map(f)
seq.collect{case P => ???}.headOption

// После
seq.flatMap(f)
seq.distinct
seq.reverse.iterator
seq.reverseMap(f)
seq.collectFirst{case P => ???}
```

## ArrayBuffer вместо ListBuffer
`ArrayBuffer` выигрывает в производительности `ListBuffer` при операциях добавления и итерации, а это 90% всего использования. Также `ArrayBuffer` занимает меньший объем в оперативной памяти, потому что `ListBuffer` основан на связанном списке.

- [Scala Collections Performance](https://tobyhobson.com/posts/scala/collections-performance/)


## exists вместо filter для проверки

Пример:
```scala
// До
seq.filter(p).nonEmpty
seq.filter(p).isEmpty

// После
seq.exists(p)
!seq.exists(p)
```

## isEmpty вместо length для пустоты

Пример:
```scala
// До
seq.length > 0
seq.length != 0
seq.length == 0

// После
seq.nonEmpty
seq.nonEmpty
seq.isEmpty
```

## getVar вместо запросов к БД в CWA

Так как CWA вызывается на большое количество событий, выполняемые в нем запросы к БД могут существенно замедлить работу интерфейса пользователя. Вычисления дополнительных полей для CWA можно осуществлять в onRefreshExt, selectStatement или на события при открытии карточки (onLoadMeta, beforeOpen, afterOpen и т.д.).  
Пример:
```scala
//До
override def checkWorkability(): Unit = {
  super.checkWorkability()
  if (thisApi.load(getVar("id").asNLong).get(_.idStateMC) >= 300.nn)
    opers("delete").isEnabled = false
}

//После
override def checkWorkability(): Unit = {
  super.checkWorkability()
  if (getSelfVar("idStateMC").asNNumber >= 300.nn)
    opers("delete").isEnabled = false

}
```

## Избегание copyAro для экономии памяти
При использовании copyAro все данные из rop копируются, что увеличивает объем потребляемой оперативной памяти на сервере и может привести к ошибке потребляемой памяти в сессии.
Пример:
```scala
// До
val rvWorkDoc = Mct_DocumentApi().loadByGid(rop.get(_.gidMctDocument)).copyAro()
val idvPrj = rvWorkDoc.idPrj
val idvPrjVer = rvWorkDoc.idPrjVer
// После
val ropWorkDoc = Mct_DocumentApi().loadByGid(rop.get(_.gidMctDocument))
val idvPrj = ropWorkDoc.get(_.idPrj)
val idvPrjVer = ropWorkDoc.get(_.idPrjVer)
```


## Разделение логики Avi и Api
Пример:
```scala
// До
override protected def onInsertItem(): Unit = {
  super.onInsertItem()
  if (getSelfVar("idCandidat#").asNLong.isNotNull) thisApi().setidCandidate(thisRop(), getSelfVar("idCandidat#").asNLong)
  if (getSelfVar("idVacancy#").asNLong.isNotNull) thisApi().setidVacancy(thisRop(), getSelfVar("idVacancy#").asNLong)
  if (getSelfVar("nSalaryProbAcc#").asNNumber.isNotNull) thisApi().setnSalaryProb(thisRop(), getSelfVar("nSalaryProbAcc#").asNNumber)
  if (getSelfVar("nProbationAcc#").asNNumber.isNotNull) thisApi().setnProbation(thisRop(), getSelfVar("nProbationAcc#").asNNumber)
  if (getSelfVar("nSalaryAcc#").asNNumber.isNotNull) thisApi().setnSalary(thisRop(), getSelfVar("nSalaryAcc#").asNNumber)
  if (getSelfVar("idCurAcc#").asNLong.isNotNull) thisApi().setidCur(thisRop(), getSelfVar("idCurAcc#").asNLong)
  if (getSelfVar("idEmployment#").asNLong.isNotNull) thisApi().setidEmployment(thisRop(), getSelfVar("idEmployment#").asNLong)
  if (getSelfVar("idWorkGraph#").asNLong.isNotNull) thisApi().setidWorkGraph(thisRop(), getSelfVar("idWorkGraph#").asNLong)
  if (getSelfVar("idWorkMode#").asNLong.isNotNull) thisApi().setidWorkMode(thisRop(), getSelfVar("idWorkMode#").asNLong)
  if (getSelfVar("idMentor#").asNLong.isNotNull) thisApi().setidCurator(thisRop(), getSelfVar("idMentor#").asNLong)
  if (getSelfVar("idHREmpl#").asNLong.isNotNull) thisApi().setidHREmpl(thisRop(), getSelfVar("idHREmpl#").asNLong)
  if (getSelfVar("dPlan#").asNDate.isNotNull) thisApi().setdPlan(thisRop(), getSelfVar("dPlan#").asNDate)
}
// После
override protected def onInsertItem(): Unit = {
  super.onInsertItem()
  if (getSelfVar("idOffer#").notNull())
    thisApi().fillByOffer(thisRop(), getSelfVar("idOffer#").asNLong)
}
```

## Форматирование длинных выражений
Длинные выражения в одну строку затрудняют чтение кода и отнимает много времени на понимание  
Пример:
```scala
// До
Stk_InternalWarrantDetApi().byParent(rop).withFilter(f => (gidapDet.contains(f.gid) || gidapDet.isEmpty)).foreach { ropDet =>
    ???
  }

// После
Stk_InternalWarrantDetApi().byParent(rop)
  .withFilter(f => (gidapDet.contains(f.gid) || gidapDet.isEmpty))
  .foreach { ropDet => 
     ???
  }
```

Пример:
```sql
-- До
select gidObj, nPercent, nQty from Bs_CostDistr d where d.gidSrc = :gid
-- После
select d.gidObj
       ,d.nPercent 
       ,d.nQty
from   Bs_CostDistr d 
where  d.gidSrc = :gid
```

## Общие trait для повторного использования
Чтобы избежать дублирование кода и упростить дальнейшую поддержку  
Пример:
```scala
trait List_gidDocGeneral extends Default with super.List {
  ???
}

trait List_gidDocTax extends List_gidDocGeneral {
  override def idAccKind: NLong = Bs_AccKindApi().idTax
}

trait List_gidDocMng extends List_gidDocGeneral {
  override def idAccKind: NLong = Bs_AccKindApi().idMng
}

trait List_gidDocAit extends List_gidDocGeneral {
  override def idAccKind: NLong = Bs_AccKindApi().idAit
}
```

## Библиотеки общих методов для выборок
Чтобы избежать дублирование кода и упростить дальнейшую поддержку  
Пример:
```scala
Pm_Lib
Btk_FileLib
```

## Экстракторы вместо полей Tuple
Пример:
```scala
  val t = ("Vlad", 23, "M")
//До
setName(t._1)
setAge(t._2)
setSex(t._3)

//После
val (name, age, sex) = t
setName(name)
setAge(age)
setSex(sex)

Seq(t)
  .foreach { case (name, age, sex) =>
    setName(name)
    setAge(age)
    setSex(sex)
  }
```

## Безопасные методы коллекций с Option
Использование `.last, .head, .init, .tail, .reduce` может приводит к исключению в случае если коллекция пустая
Пример:
```scala
val unknownSizeList: List[_] = ???
//До
val sum = unknownSizeList.reduce(_ + _) // есть шанс ошибки
//После
val sum = unknownSizeList.reduceOption(_ + _).nn // мы получим NNumber(null) в случае если список пустой
```

## Строковая интерполяция вместо конкатенации

Пример:
```scala
val hello = "hello"
val world = "world"
//До
val str = hello + " " + world

//После
val str = s"$hello $world"
```

## StringBuilder для сложных строк

Пример:
```scala
val ropa: List[???] = ???
//До
var str = "Names: "

val result =
  for (rop <- ropa) yield {
    str += rop.sName + ";" // каждый раз создаётся новая строка
    load(rop.idSmth)
  }

str += " end"
//После
val stringBuilder = 
  new StringBuilder()
    .append("Names: ")
val result =
  for (rop <- ropa) yield {
    stringBuilder
      .append(rop.sName)
      .append(";")
    load(rop.idSmth)
  }

val names = stringBuilder.append(" end").toString()
// или если есть возможность сформируйте список
val (result, names) = ropa.map { rop =>
  (load(rop.idSmth), rop.sName)
}.unzip

val str =  names.mkString("Names: ", ";", " end")
```

## Option для обработки null-значений

Пример:
```scala
val rop = ???
val oldJavaLib = ???
//До
val ropSmth = load(rop.idSmth) // забыли обработать null

var result = _
val thing = oldJavaLib.getThing
if(thing != null) {
  val smth = thing.getSmth
  if(smth != null) {
    try {
      result = smth.doWork()
    } catch {
      case _: Exception => {}
    }
  }
}
if(result == null) throw AppException("Не получилось")
//После
val ropSmth = get(rop.idSmth)
  .getOrElse(throw AppException("Чего то нету"))

val result = (for {
  thing <- Option(oldJavaLib.getThing)
  smth <- Option(thing.getSmth)
  result <- Try(smth.doWork()).toOption
} yield result).getOrElse(throw AppException("Не получилось"))
```


## Запросы вне вложенных циклов

Пример:
```scala
  case class TaskDoc(/* атрибуты */, aDocDet: CSeq[TaskDocDet])

  case class TaskDocDet(/* атрибуты */, aFile: CSeq[TaskDocDetFile])

  case class TaskDocDetFile(/* атрибуты */)

//до:
    def getTasksData(dpFrom: NDate, dpTo: NDate): CSeq[Task] = {
      val avResult = mutable.ArrayBuffer.empty[TaskDoc]

      for (rvx <- new ASelect {
        // документы Task
      }) {
        // ...

        for (rvxDet <- new ASelect {
          // позиции Task
        }) {
          // ...

          for (rvxDetFile <- new ASelect {
            // файлы позиций Task
          }) {
            // ...
          }
        }
      }

      avResult
    }

//после:
    def getTasksData(dpFrom: NDate, dpTo: NDate): CSeq[Task] = {
      val avResult = mutable.ArrayBuffer.empty[TaskDoc]

      val avIdToTask = new ASelect {
        val id = asNLong("id")
        // остальные атрибуты
      }.map(rvx => rvx.id() -> TaskDoc(/* ... */)).toMap

      val avIdDocAndTaskDets = new ASelect {
        SQL"""
          select t.idDoc
                ,t.id
                ,t.nRow
              -- ..
            from Md_TaskDet t
           where t.idDoc in (select unnest(${LongPgArray(avIdToTask.keys)}) )
          """
      }.map { rvx =>
        (rvx.idDoc(), rvx.id(), TaskDocDet(/* ... */))
      }.toSeq

      val gidavTaskDet = avIdDocAndTaskDets.map(vEntry => Md_TaskDetApi().getGid(vEntry._2))
      val avIdTaskDetAndFiles = new ASelect {
        SQL"""
          --
          where t.gidSrc in (select unnest(${GidPgArray(gidavTaskDet)}) )
          """
      }.map { rvx =>
        (rvx.id(), TaskDocDetFile(/* ... */))
      }

      // агрегация с циклом по Task, поиском TaskDet, подбором File для Det

      avResult
    }
```


Пример с поисковым ключом:
```scala
case class objRem(
                         idGds: NLong
                         , idStock: NLong
                         , gidSlitAccount: NGid
                         , gidSTKAcc: NGid
                         , gidMaster: NGid
                         , gidSuite: NGid
                         , idCons: NLong
                         , var nQtyBase: NNumber
                       )
      val avRem = collection.mutable.Map[NString, ArrayBuffer[objRem]]()
      for (rv <- new ASelect {
        val idGds = asNLong("idGds")
        val idCons = asNLong("idCons")
        val idStock = asNLong("idStock")
        val gidMaster = asNGid("gidMaster")
        val gidSTKAcc = asNGid("gidSTKAcc")
        val gidSuite = asNGid("gidSuite")
        val gidSlitAccount = asNGid("gidSlitAccount")
        val nQtyBaseMsr = asNNumber("nQtyBaseMsr")
        SQL"""
    SELECT sum(-r.nQtyBaseMsr) nQtyBaseMsr
           ,r.idGds
           ,r.idStock
           ,r.gidMaster
           ,r.gidSTKAcc
           ,r.gidSuite
           ,r.gidSlitAccount
           ,r.idCons
    from   (select dr.idgds
                  ,dr.gidMaster
                  ,dr.gidObjNeed
                  ,sum(dr.nQtyBase) nQtyBase
            from (
                select *
                from unnest(${idavGds}
                       ,${gidavMaster}
                       ,${gidaObjNeed}
                       ,${naQtyBase}
                       ) as x(idGds, gidMaster, gidObjNeed, nQtyBase)
                 ) dr
            group by dr.idgds
                  ,dr.gidmaster
                  ,dr.gidObjNeed
           ) d
    join   Stk_RegistryGdsMov r on r.idGds = d.idGds and r.gidSlitAccount = d.gidObjNeed and r.gidMaster = d.gidMaster
    where  r.idDepOwner = $idpDepOwner
    and    r.sType = 'res'
    and    r.idStock in (select st.idchild
                         from   stk_stocktree st
                         where  st.idparent = $idpStock)
    group by r.idGds
           ,r.idStock
           ,r.gidMaster
           ,r.gidSTKAcc
           ,r.gidSuite
           ,r.gidSlitAccount
           ,r.idCons
    having sum(-r.nQtyBaseMsr) > 0"""
      }) {
        val sKey = rv.idGds() + "_" + rv.gidMaster().nvl("0".ng) + "_" + rv.gidSlitAccount().nvl("0".ng)
        avRem.getOrElseUpdate(sKey, ArrayBuffer[objRem]()) += objRem(
          idGds = rv.idGds()
          , idStock = rv.idStock()
          , gidSlitAccount = rv.gidSlitAccount()
          , gidSTKAcc = rv.gidSTKAcc()
          , gidMaster = rv.gidMaster()
          , gidSuite = rv.gidSuite()
          , nQtyBase = rv.nQtyBaseMsr()
          , idCons = rv.idCons()
        )
      }

      apObjNeed.foreach { vObjNeed =>
        val sKey = vObjNeed.idGds + "_" + vObjNeed.gidMaster.nvl("0".ng) + "_" + vObjNeed.gidObjNeed.nvl("0".ng)
        if (avRem.contains(sKey)) {
          avRem(sKey).filter(f => f.nQtyBase > 0.nn).foreach { rem =>
            lazy val ropGds = Bs_GoodsApi().load(vObjNeed.idGds)

            val nvQtyBase = vObjNeed.nQtyBase.min(rem.nQtyBase)
            avOperationData += OperationData(
              gidParent = vObjNeed.gidDet
              , gidDet = vObjNeed.gidDet
              , gidSrcObject = vObjNeed.gidSrcObject
              , idGds = vObjNeed.idGds
              , idCons = rem.idCons
              , gidSuite = rem.gidSuite
              , idStock = rem.idStock
              , gidMaster = rem.gidMaster
              , gidSTKAcc = rem.gidSTKAcc
              , gidSlitAccountRes = rem.gidSlitAccount
              , nQtyBase = nvQtyBase
              , nQtyBase2 = {
                if (vObjNeed.nConvertBaseToBase2 > 0.nn) {
                  (nvQtyBase / vObjNeed.nConvertBaseToBase2).round(ropGds.get(_.nRoundPlaces2))
                } else None.nn
              }
              , idReplacedGds = vObjNeed.idReplacedGds
              , nRateReplacedGds = vObjNeed.nConvertReplacedGds
              , nPrimeCostSum = (nvQtyBase * vObjNeed.nPrimeCostPrc).round(2.nn)
              , nPrimeCostSumTax = (nvQtyBase * vObjNeed.nPrimeCostPrcTax).round(2.nn)
              , nPrimeCostSumMng = (nvQtyBase * vObjNeed.nPrimeCostPrcMng).round(2.nn)
              , dDateTime = dpExec
              , idAddCondGenOper = Stk_AddCondGenOperApi().idvStornResByDetNeed
            )
            rem.nQtyBase -= nvQtyBase
            vObjNeed.nQtyBase -= nvQtyBase
          }
        }

        if (vObjNeed.nQtyBase > 0.nn && params.bValidateRemStorno) {
          throw AppException(s"""По ТМЦ ${Bs_GoodsApi().getMnemoCode(vObjNeed.idGds)} ${Bs_GoodsApi().getHeadLine(vObjNeed.idGds)} нет достаточного количества для сторнирования резерва!""")
        }

      }
```

## Параллельные вычисления для независимых задач
[Руководство разработчика: Параллельные вычисления](/080_addition/060_параллельные_вычисления.md#параллельные-вычисления).