2011-12-18 13 views
9

La siguiente blog article muestra cómo en F # foldBack se puede hacer la cola recursiva usando el estilo de continuación de paso.¿Es posible usar continuaciones para hacer que foldRight tail recursive?

En Scala, esto significaría que:

def foldBack[T,U](l: List[T], acc: U)(f: (T, U) => U): U = { 
    l match { 
    case x :: xs => f(x, foldBack(xs, acc)(f)) 
    case Nil => acc 
    } 
} 

se pueden hacer cola recursiva al hacer esto:

def foldCont[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    @annotation.tailrec 
    def loop(l: List[T], k: (U) => U): U = { 
    l match { 
     case x :: xs => loop(xs, (racc => k(f(x, racc)))) 
     case Nil => k(acc) 
    } 
    } 
    loop(list, u => u) 
} 

Por desgracia, sigo teniendo un desbordamiento de pila de largas listas. loop es recursivo y optimizado, pero supongo que la acumulación de pila acaba de pasar a las llamadas de continuación.

¿Por qué no es un problema con F #? ¿Y hay alguna forma de solucionar esto con Scala?

Edición: aquí un código que muestra la profundidad de la pila:

def showDepth(s: Any) { 
    println(s.toString + ": " + (new Exception).getStackTrace.size) 
} 

def foldCont[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    @annotation.tailrec 
    def loop(l: List[T], k: (U) => U): U = { 
    showDepth("loop") 
    l match { 
     case x :: xs => loop(xs, (racc => { showDepth("k"); k(f(x, racc)) })) 
     case Nil => k(acc) 
    } 
    } 
    loop(list, u => u) 
} 

foldCont(List.fill(10)(1), 0)(_ + _) 

Esta impresora:

loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
k: 51 
k: 52 
k: 53 
k: 54 
k: 55 
k: 56 
k: 57 
k: 58 
k: 59 
k: 60 
res2: Int = 10 
+0

No tiene sentido. ¿Tienes un caso de prueba simple? –

+0

@ DanielC.Sobral, vea el código que agregué y que imprime. – huynhjl

Respuesta

4

El problema es la función de continuación (racc => k(f(x, racc))) sí. Debería ser optimizado para que todo este negocio funcione, pero no lo es.

Scala no puede realizar optimizaciones de tailcall para llamadas de cola arbitrarias, solo para aquellas que pueden transformarse en bucles (es decir, cuando la función se llama a sí misma, no a otra función).

+0

Eso es lo que supuse. ¿Hay algo que se pueda hacer? ¿Como usar algo como trampolines? – huynhjl

+0

Trampolines probablemente ayuden, pero creo que en este caso particular 'leftFold' resolvería el problema con mucho menos dolor. Si, por algún motivo, debe tener semántica 'foldRight', puede invertir la lista y llamar' foldLeft' al resultado. –

+1

Resulta que realmente no es tan doloroso en este caso, ver mi propia respuesta. – huynhjl

4

¿Por qué no es un problema con F #?

F # tiene todas las llamadas de cola optimizadas.

¿Y hay alguna forma de evitar esto con Scala?

Usted puede hacer TCO utilizando otras técnicas como camas elásticas, pero se pierde de interoperabilidad, ya que cambia la convención de llamada y es ~ 10 × más lento. Esta es una de las tres razones por las que no uso Scala.

EDITAR

Sus resultados de referencia indican que los trampolines de Scala son muchomás rápido de lo que eran la última vez que los probé. Además, es interesante agregar puntos de referencia equivalentes usando F # y para listas más grandes (¡porque no tiene sentido hacer CPS en listas pequeñas!).

Por 1.000 x 1.000 en una lista de elementos en mi netbook con un 1,67 GHz Intel Atom N570, me sale:

List.fold  0.022s 
List.rev+fold 0.116s 
List.foldBack 0.047s 
foldContTC 0.334s 

Para 1x lista de elementos 1.000.000, me sale:

List.fold  0.024s 
List.rev+fold 0.188s 
List.foldBack 0.054s 
foldContTC 0.570s 

Puede que también le interesen las discusiones anteriores sobre esto en la lista caml en el contexto de reemplazar las funciones de lista recursiva no cola de OCaml con cola recursiva optimizada.

+3

¿Cuáles son las otras dos razones por las que no usa Scala? –

+3

@StephenSwensen: La falta de tipos de valores y la falta de inferencia tipo. Tenga en cuenta que la falta de llamadas de cola y tipos de valores es un problema con la JVM y no con Scala. Estas son también las razones por las que elegí desarrollar HLVM en LLVM en lugar de JVM. El proyecto de Geoff Reedy para llevar a Scala a LLVM tiene el potencial de solucionar ambos problemas, lo cual sería absolutamente increíble. –

6

Jon, n.m., gracias por sus respuestas. En base a sus comentarios, pensé en intentarlo y usar trampolín. Un poco de investigación muestra que Scala tiene soporte de biblioteca para trampolines en TailCalls. Esto es lo que ocurrió con un poco después de volverse locos:

def foldContTC[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    import scala.util.control.TailCalls._ 
    @annotation.tailrec 
    def loop(l: List[T], k: (U) => TailRec[U]): TailRec[U] = { 
    l match { 
     case x :: xs => loop(xs, (racc => tailcall(k(f(x, racc))))) 
     case Nil => k(acc) 
    } 
    } 
    loop(list, u => done(u)).result 
} 

yo estaba interesado en ver cómo esto se compara con la solución sin la cama elástica, así como la foldLeft y foldRight implementaciones por defecto. Aquí está el código de referencia y algunos resultados:

val size = 1000 
val list = List.fill(size)(1) 
val warm = 10 
val n = 1000 
bench("foldContTC", warm, lots(n, foldContTC(list, 0)(_ + _))) 
bench("foldCont", warm, lots(n, foldCont(list, 0)(_ + _))) 
bench("foldRight", warm, lots(n, list.foldRight(0)(_ + _))) 
bench("foldLeft", warm, lots(n, list.foldLeft(0)(_ + _))) 
bench("foldLeft.reverse", warm, lots(n, list.reverse.foldLeft(0)(_ + _))) 

Los horarios son:

foldContTC: warming... 
Elapsed: 0.094 
foldCont: warming... 
Elapsed: 0.060 
foldRight: warming... 
Elapsed: 0.160 
foldLeft: warming... 
Elapsed: 0.076 
foldLeft.reverse: warming... 
Elapsed: 0.155 

base a esto, parecería que la cama elástica es en realidad dando un rendimiento bastante bueno. Sospecho que la penalización en la parte superior del boxeo/unboxing es relativamente no tan malo.

Editar: según lo sugerido por los comentarios de Jon, estos son los tiempos en los elementos de 1M que confirman que el rendimiento se degrada con listas más grandes. También descubrí que la aplicación List.foldLeft biblioteca no está anulado, por lo que temporizada con el siguiente foldLeft2:

def foldLeft2[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    list match { 
    case x :: xs => foldLeft2(xs, f(x, acc))(f) 
    case Nil => acc 
    } 
} 

val size = 1000000 
val list = List.fill(size)(1) 
val warm = 10 
val n = 2 
bench("foldContTC", warm, lots(n, foldContTC(list, 0)(_ + _))) 
bench("foldLeft", warm, lots(n, list.foldLeft(0)(_ + _))) 
bench("foldLeft2", warm, lots(n, foldLeft2(list, 0)(_ + _))) 
bench("foldLeft.reverse", warm, lots(n, list.reverse.foldLeft(0)(_ + _))) 
bench("foldLeft2.reverse", warm, lots(n, foldLeft2(list.reverse, 0)(_ + _))) 

rendimientos:

foldContTC: warming... 
Elapsed: 0.801 
foldLeft: warming... 
Elapsed: 0.156 
foldLeft2: warming... 
Elapsed: 0.054 
foldLeft.reverse: warming... 
Elapsed: 0.808 
foldLeft2.reverse: warming... 
Elapsed: 0.221 

Así foldLeft2.reverse es el ganador ...

+0

"bastante buen rendimiento". En efecto. ¡Llamaría a esa actuación sospechosamente buena! Tal vez la implementación del trampolín es lo suficientemente inteligente como para darse cuenta de que no tiene que dar un puntapié porque sus listas son tan cortas. ¿Qué medidas de rendimiento obtiene con las listas de elementos 1M? –

+0

Con temporizaciones que cierran, los problemas de caché y GC también entran en juego, p. revertir la misma lista de elementos 1k una y otra vez es barato con un GC generacional y un caché eficiente, pero es probable que las listas de elementos 1M sobrevivan al vivero o región local de subprocesos que incurrirá en su sobrecarga y degradará la eficacia del caché. –

+0

@JonHarrop, buena llamada, tiempos agregados para elementos de 1M. – huynhjl

3

Llego tarde a esta pregunta, pero quería mostrar cómo se puede escribir un FoldRight recursivo sin usar un trampolín completo; mediante la acumulación de una lista de continuaciones (en lugar de tener ellos llaman entre sí cuando se hace, lo que conduce a un desbordamiento de pila) y plegado sobre ellos al final, tipo de como mantener una pila, pero en el montón:

object FoldRight { 

    def apply[A, B](list: Seq[A])(init: B)(f: (A, B) => B): B = { 
    @scala.annotation.tailrec 
    def step(current: Seq[A], conts: List[B => B]): B = current match { 
     case Seq(last) => conts.foldLeft(f(last, init)) { (acc, next) => next(acc) } 
     case Seq(x, xs @ _*) => step(xs, { acc: B => f(x, acc) } +: conts) 
     case Nil => init 
    } 
    step(list, Nil) 
    } 

} 

El doblez que ocurre al final es recíproco en la cola. Pruébelo in ScalaFiddle

En términos de rendimiento, funciona un poco peor que la versión de llamada final.

[info] Benchmark   (length) Mode Cnt Score Error Units 
[info] FoldRight.conts   100 avgt 30 0.003 ± 0.001 ms/op 
[info] FoldRight.conts   10000 avgt 30 0.197 ± 0.004 ms/op 
[info] FoldRight.conts  1000000 avgt 30 77.292 ± 9.327 ms/op 
[info] FoldRight.standard  100 avgt 30 0.002 ± 0.001 ms/op 
[info] FoldRight.standard  10000 avgt 30 0.154 ± 0.036 ms/op 
[info] FoldRight.standard 1000000 avgt 30 18.796 ± 0.551 ms/op 
[info] FoldRight.tailCalls  100 avgt 30 0.002 ± 0.001 ms/op 
[info] FoldRight.tailCalls  10000 avgt 30 0.176 ± 0.004 ms/op 
[info] FoldRight.tailCalls 1000000 avgt 30 33.525 ± 1.041 ms/op 
Cuestiones relacionadas