Scala производительность рекурсии списка

Этот вопрос касается того, как Scala выполняет сопоставление и рекурсию шаблонов со списками и их производительность.

Если у меня есть функция, которая рекурсивно перечитывает список, и я делаю это с совпадением на минусах, например, что-то вроде:

def myFunction(xs) = xs match { 
  case Nil => Nil
  case x :: xs => «something» myFunction(xs)
}

В Haskell:

myFunction [] = []
myFunction (x:xs) = «something» : myFunction xs

Я использую ту же семантику, что и я, например, Haskell. Я не думаю, что возникнет вопрос о реализации Haskell, как о том, как вы относитесь к спискам. Для длинного списка (я бы работал в списке с несколькими тысячами узлов), Haskell не мигнул бы (я думаю, хотя, я никогда не пробовал).

Но из того, что я понимаю с помощью Scala, оператор соответствия вызовет метод unapply extractor для разделения списка вокруг минусов и, чтобы расширить пример до функции, которая ничего не делает для списка:

def myFunction(xs) = xs match { 
  case Nil => Nil
  case x :: xs => x :: myFunction(xs)
}

В Haskell:

myFunction [] = []
myFunction (x:xs) = x : myFunction xs

он назовет применить метод экстрактора, чтобы скрыть его вместе. Для длинного списка я предполагаю, что это будет очень дорого.

Чтобы проиллюстрировать, в моем конкретном случае я хочу переписывать список символов и накапливать различные вещи, где входная строка составляет всего несколько десятков килобайт.

Я действительно буду называть конструкторы и экстракторы для каждого шага рекурсии, если я хочу переписать длинный список? Или есть оптимизация? Или лучшие способы сделать это? В этом случае мне понадобится несколько переменных аккумулятора, и, очевидно, я бы просто не перечислил список, ничего не делая...

(Прошу извинить моего Haskell, я не писал строки в течение двух лет.)

(И да, я собираюсь для рекурсии хвоста.)

Ответы

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