Scala - сворачивание значений, которые являются результатом взаимодействия объекта
В Scala у меня есть список объектов, которые представляют точки и содержат x
а также y
ценности. Список описывает путь, который проходит через все эти точки последовательно. У меня вопрос, как использовать складывание в этом списке, чтобы найти общую длину пути? Или, может быть, есть даже лучший функционал или Scala способ сделать это?
Я придумал вот что:
def distance = (0 /: wps)(Waypoint.distance(_, _))
но, конечно, это совершенно неправильно, потому что distance
возвращается Float
, но принимает два Waypoint
объекты.
ОБНОВИТЬ:
Спасибо за предложенные решения! Они определенно интересны, но я думаю, что это слишком много для вычислений в реальном времени, которые могут стать тяжелыми. До сих пор я вышел с этими строками:
val distances = for(i <- 0 until wps.size) yield wps(i).distanceTo(wps(i + 1))
val distance = (0f /: distances)(_ + _)
Я чувствую, что это справедливый императивный / функциональный микс, который является одновременно быстрым и также оставляет значения расстояний между каждой путевой точкой для дальнейших возможных ссылок, что также полезно в моем случае.
ОБНОВЛЕНИЕ 2: Фактически, чтобы определить, что быстрее, мне нужно будет сделать тесты всех предложенных решений для всех типов последовательностей.
4 ответа
Это должно работать.
(wps, wps drop 1).zipped.map(Waypoint.distance).sum
Не know
Если здесь можно использовать сложение, попробуйте следующее:
wps.sliding(2).map(segment => Waypoint.distance(segment(0), segment(1))).sum
wps.sliding(2)
возвращает список всех последующих пар. Или, если вы предпочитаете сопоставление с образцом:
wps.sliding(2).collect{case start :: end :: Nil => Waypoint.distance(start, end)}.sum
Кстати, рассмотреть вопрос об определении:
def distanceTo(to: Waypoint)
на Waypoint
Класс напрямую, а не на объекте-компаньоне, так как он выглядит более объектно-ориентированным и позволит вам написать хороший DSL-подобный код:
point1.distanceTo(point2)
или даже:
point1 distanceTo point2
wps.sliding(2).collect{
case start :: end :: Nil => start distanceTo end
}.sum
Если вы делаете индексацию любого вида, который хотите использовать Vector, а не List:
scala> def timed(op: => Unit) = { val start = System.nanoTime; op; (System.nanoTime - start) / 1e9 }
timed: (op: => Unit)Double
scala> val l = List.fill(100000)(1)
scala> val v = Vector.fill(100000)(1)
scala> timed { var t = 0; for (i <- 0 until l.length - 1) yield t += l(i) + l(i + 1) }
res2: Double = 16.252194583
scala> timed { var t = 0; for (i <- 0 until v.length - 1) yield t += v(i) + v(i + 1) }
res3: Double = 0.047047654
ListBuffer предлагает быстрые добавления, он не предлагает быстрый произвольный доступ.
Ваш комментарий "слишком много функционала для вычислений в реальном времени, которые могут стать тяжелыми" делает это интересным. Сравнительный анализ и профилирование имеют решающее значение, поскольку вы не хотите писать кучу сложного в обслуживании кода для повышения производительности, а только для того, чтобы понять, что это не критичная для вашего приложения производительность. Или, что еще хуже, узнайте, что оптимизация производительности ухудшает вашу конкретную рабочую нагрузку.
Наиболее эффективная реализация будет зависеть от вашей специфики (Сколько времени занимают пути? Сколько ядер в системе?) Но я думаю, что сочетание императивного и функционального подходов может дать вам худшее из двух миров. Вы можете потерять и удобочитаемость, и производительность, если не будете осторожны!
Я бы очень немного изменил ответ отсутствующего фактора, чтобы вы могли получить прирост производительности от параллельных сборов. Тот факт, что просто добавление .par
может дать вам огромный прирост производительности демонстрирует силу придерживаться функционального программирования!
def distancePar(wps: collection.GenSeq[Waypoint]): Double = {
val parwps = wps.par
parwps.zip(parwps drop 1).map(Function.tupled(distance)).sum
}
Я думаю, что это будет работать лучше всего, если у вас есть несколько ядер для решения проблемы, и wps
имеет тенденцию быть несколько длинным. Если у вас мало ядер или коротких путей, то параллелизм, вероятно, повредит больше, чем поможет.
Другая крайность была бы полностью императивным решением. Написание обязательных реализаций отдельных, критичных к производительности функций обычно приемлемо, если вы избегаете общего изменяемого состояния. Но как только вы привыкнете к FP, вам будет сложно писать и поддерживать такую функцию. И это также не легко распараллелить.
def distanceImp(wps: collection.GenSeq[Waypoint]): Double = {
if (wps.size <= 1) {
0.0
} else {
var r = 0.0
var here = wps.head
var remaining = wps.tail
while (!remaining.isEmpty) {
r += distance(here, remaining.head)
here = remaining.head
remaining = remaining.tail
}
r
}
}
Наконец, если вы ищете золотую середину между FP и императивом, вы можете попробовать рекурсию. Я не профилировал это, но я предполагаю, что это будет примерно эквивалентно императивному решению с точки зрения производительности.
def distanceRec(wps: collection.GenSeq[Waypoint]): Double = {
@annotation.tailrec
def helper(acc: Double, here: Waypoint, remaining: collection.GenSeq[Waypoint]): Double =
if (remaining.isEmpty)
acc
else
helper(acc + distance(here, remaining.head), remaining.head, remaining.tail)
if (wps.size <= 1)
0.0
else
helper(0.0, wps.head, wps.tail)
}