Scala список рекурсии производительности
Этот вопрос о том, как Scala выполняет сопоставление с образцом и рекурсию со списками и их производительность.
Если у меня есть функция, которая повторяется по списку, и я делаю это с сопоставлением по минусам, например что-то вроде:
def myFunction(xs) = xs match {
case Nil => Nil
case x :: xs => «something» myFunction(xs)
}
В Хаскеле:
myFunction [] = []
myFunction (x:xs) = «something» : myFunction xs
Я использую ту же семантику, что и, например, с Haskell. Я не думаю, что возникнут какие-либо вопросы по поводу реализации Haskell, поскольку это просто способ работы со списками. Для длинного списка (я бы работал над списком с несколькими тысячами узлов), Haskell не моргнул (хотя я представляю, я никогда не пробовал).
Но из того, что я понимаю со Scala, оператор match будет вызывать метод unapply extractor, чтобы разбить список вокруг минусов и распространить пример на функцию, которая ничего не делает со списком:
def myFunction(xs) = xs match {
case Nil => Nil
case x :: xs => x :: myFunction(xs)
}
В Хаскеле:
myFunction [] = []
myFunction (x:xs) = x : myFunction xs
он вызвал бы метод apply extractor, чтобы объединить его обратно вместе. Для длинного списка я думаю, что это будет очень дорого.
Чтобы проиллюстрировать это, в моем конкретном случае я хочу перебрать список символов и накапливать различные вещи, где входная строка имеет размер до нескольких десятков килобайт.
Действительно ли я буду вызывать конструкторы и экстракторы для каждого шага рекурсии, если я захочу пройтись по длинному списку? Или есть оптимизация? Или лучшие способы сделать это? В этом случае мне понадобится несколько переменных-аккумуляторов, и, очевидно, я не буду просто повторяться по списку, ничего не делая...
(Прошу прощения за мой Хаскелл, я не писал ни одной строки в течение двух лет.)
(И да, я иду на хвостовую рекурсию.)
1 ответ
Во-первых, Haskell не является строгим, поэтому эти вызовы функций на хвосте могут вообще никогда не оцениваться. Scala, с другой стороны, вычислит весь список перед возвратом. Более близкая реализация к тому, что происходит в Haskell, будет такой:
def myFunction[T](l: List[T]): Stream[T] = l match {
case Nil => Stream.empty
case x :: xs => x #:: myFunction(xs)
}
Что получает List
, который является строгим, и возвращает Stream
что не является строгим.
Теперь, если вы хотите избежать сопоставления с образцом и экстракторов (хотя ни один из них не вызывается в этом конкретном случае - см. Ниже), вы можете просто сделать это:
def myFunction[T](xs: List[T]): Stream[T] =
if (xs.isEmpty) Stream.empty else xs.head #:: myFunction(xs.tail)
Я только что понял, что ты собираешься пойти на хвостовую рекурсию. То, что вы написали, не является хвостово-рекурсивным, потому что вы готовите x
к результату рекурсии. При обработке списков вы получите хвостовую рекурсию, если вычислите результаты в обратном порядке, а затем инвертируете:
def myFunction[T](xs: List[T]): List[T] = {
def recursion(input: List[T], output: List[T]): List[T] = input match {
case x :: xs => recursion(xs, x :: output)
case Nil => output
}
recursion(xs, Nil).reverse
}
Наконец, давайте декомпилируем пример, чтобы увидеть, как он работает:
class ListExample {
def test(o: Any): Any = o match {
case Nil => Nil
case x :: xs => xs
case _ => null
}
}
Формирует:
public class ListExample extends java.lang.Object implements scala.ScalaObject{
public ListExample();
Code:
0: aload_0
1: invokespecial #10; //Method java/lang/Object."<init>":()V
4: return
public java.lang.Object test(java.lang.Object);
Code:
0: aload_1
1: astore_2
2: getstatic #18; //Field scala/Nil$.MODULE$:Lscala/Nil$;
5: aload_2
6: invokestatic #24; //Method scala/runtime/BoxesRunTime.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
9: ifeq 18
12: getstatic #18; //Field scala/Nil$.MODULE$:Lscala/Nil$;
15: goto 38
18: aload_2
19: instanceof #26; //class scala/$colon$colon
22: ifeq 35
25: aload_2
26: checkcast #26; //class scala/$colon$colon
29: invokevirtual #30; //Method scala/$colon$colon.tl$1:()Lscala/List;
32: goto 38
35: aconst_null
36: pop
37: aconst_null
38: areturn
public int $tag() throws java.rmi.RemoteException;
Code:
0: aload_0
1: invokestatic #42; //Method scala/ScalaObject$class.$tag:(Lscala/ScalaObject;)I
4: ireturn
}
Расшифровывая, сначала вызывается метод equals
по переданному параметру и объекту Nil
, Возвращает последний, если истина. В противном случае это вызывает instanceOf[::]
на объекте. Если true, он приводит объект к этому и вызывает метод tl
в теме. В противном случае загружает Cosntant null
и возвращает его.
Итак, вы видите, x :: xs
не вызывает никакого экстрактора.
Что касается накопления, есть еще один шаблон, который вы можете рассмотреть:
val list = List.fill(100)(scala.util.Random.nextInt)
case class Accumulator(negative: Int = 0, zero: Int = 0, positive: Int = 0)
val accumulator = list.foldLeft(Accumulator())( (acc, n) =>
n match {
case neg if neg < 0 => acc.copy(negative = acc.negative + 1)
case zero if zero == 0 => acc.copy(zero = acc.zero + 1)
case pos if pos > 0 => acc.copy(positive = acc.positive + 1)
})
Параметры по умолчанию и методы копирования - это функция Scala 2.8, которую я использовал только для упрощения примера, но суть в том, что foldLeft
метод, когда вы хотите накапливать вещи по списку.