Paralelismo de tareas Scan¶
La operación de Scan con los resultados parciales de la operación de reducción es una operación donde el valor de la posición i depende del valor de la posición i-1.
scala> Array(1,2,3).scan(100)((acc,x) => acc + x)
val res0: Array[Int] = Array(100, 101, 103, 106)
Operaciones de scan¶
Existen dos tipos principales de operaciones scan:
- scanLeft que asocia por la izquierda
- scanRight que asocia por la derecha
scala> Array(1,2,3).scanRight(100)((acc,x) => acc + x)
val res1: Array[Int] = Array(106, 105, 103, 100)
En este caso ambas operaciones dan resultados distintos porque los valores intermedios se calculan en orden distinto, aunque el resultado final sea el mismo.
Concepto teórico: Paralelización de Scan¶
¿Se puede paralelizar Scan suponiendo que \(f\) es asociativa?
A priori pareciera que no, ya que dependemos de valores previos. Parece que es necesario que un valor dado espere que los anteriores hayan sido calculados.
Idea: Utilizar resultados intermedios de tal forma que rompamos la dependencia, sin embargo, esto requiere hacer más cálculos.
El algoritmo de scan paralelo funciona en dos fases: 1. Fase ascendente: Calcula resultados parciales en el árbol 2. Fase descendente: Propaga los acumuladores hacia abajo

Implementación¶
/*
* This Scala source file was generated by the Gradle 'init' task.
*/
package taller
import common._
object App {
// Fase ascendente: calcula resultados parciales del árbol
// Recorre el árbol de abajo hacia arriba, calculando la reducción de cada subárbol
def llenarSubiendo[A](t: Tree[A], f:(A,A) => A): TreeRes[A] = {
t match{
case Leaf(v) => LeafRes(v) // Caso base: hoja con valor v
case Node(left, right) => {
// Procesa recursivamente los subárboles izquierdo y derecho en paralelo
val (lr, rr) = parallel(
llenarSubiendo(left, f),
llenarSubiendo(right, f)
)
// Crea nodo con resultado parcial = f(resultado_izquierdo, resultado_derecho)
NodeRes(lr, f(lr.res, rr.res), rr)
}
}
}
// Fase descendente: propaga el acumulador hacia abajo
// Distribuye los valores acumulados a través del árbol
def llenarBajando[A](t: TreeRes[A], acc: A, f:(A,A) => A): Tree[A] = {
t match{
case LeafRes(v) => Leaf(f(acc, v)) // Caso base: aplica función al acumulador y valor de la hoja
case NodeRes(leaf, v, right) => {
// Procesa recursivamente en paralelo:
// - Subárbol izquierdo con el acumulador actual
// - Subárbol derecho con acumulador actual + resultado del subárbol izquierdo
val (l, r) = parallel(
llenarBajando(leaf, acc, f),
llenarBajando(right, f(acc, leaf.res), f)
)
Node(l, r)
}
}
}
def main(args: Array[String]): Unit = {
// Árbol de ejemplo para probar la implementación
val tree: Tree[Int] = Node(
Node(Leaf(10), Leaf(12)),
Node(Leaf(3), Leaf(4))
)
// Fase 1: Calcular resultados parciales (fase ascendente)
val treeRes: TreeRes[Int] = llenarSubiendo[Int](tree, (acc: Int, x: Int) => acc + x)
println(treeRes)
// Fase 2: Propagar acumuladores (fase descendente) con valor inicial 100
val res:Tree[Int] = llenarBajando[Int](treeRes, 100, (acc: Int, x: Int) => acc + x)
println(res)
}
}
Explicación del algoritmo¶
El método llenarSubiendo genera el árbol de resultados parciales, que consiste en calcular la función en los subárboles izquierdo y derecho de forma recursiva.
El método llenarBajando toma el árbol de resultados parciales y propaga el acumulador hacia abajo. En los hijos derechos acumula también el resultado del hermano izquierdo, lo que permite romper la dependencia secuencial.
Condición importante: Para que este algoritmo funcione correctamente, la operación \(f\) debe ser asociativa.