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 метод, когда вы хотите накапливать вещи по списку.

Другие вопросы по тегам