Función fold En Kotlin

En este tutorial aprenderás acerca de la función fold en Kotlin con el objeto de aplicar una operación a los elementos de una colección y retornar el valor acumulado de todos los resultados. Aprenderás también sobre las variaciones foldRight(), foldIndexed(), foldOrNull() y runningFold().

Función fold()

Al igual que reduce(), la función de extensión fold() (plegar, doblar en español) tiene como objetivo retornar el acumulado de los resultados que se obtienen al aplicar secuencialmente una función de operación. Aspectos a tener en cuenta de fold():

  • Se toma como valor inicial del acumulado al parámetro initial
  • El orden del recorrido de elementos es de izquierda a derecha
  • Si la lista invocadora está vacía se retorna el valor de initial
// fold() en arreglos genéricos
inline fun <T, R> Array<out T>.fold(
    initial: R,
    operation: (acc: R, T) -> R
): R

// fold() en iterables
inline fun <T, R> Iterable<T>.fold(
    initial: R,
    operation: (acc: R, T) -> R
): R

// fold() en instancias Grouping
inline fun <T, K, R> Grouping<T, K>.fold(
    initialValueSelector: (key: K, element: T) -> R,
    operation: (key: K, accumulator: R, element: T) -> R
): Map<K, R>

La siguiente ilustración muestra el mismo ejemplo que viste en el tutorial de reduce(), donde a partir del rango 1..5 se calcula el productorio.

Ejemplo de la función fold en Kotlin

Como ves, el ejemplo muestra como pasamos el literal entero 2 al parámetro initial. Al parámetro operation le pasamos una función lambda que asigna al acumulado acc, su valor actual multiplicado por el entero actual.

Este ejemplo podemos materializarlo en Kotlin con el siguiente programa:

fun main() {
    // fold()
    val oneToFive = 1..5
    val initialValue = 2
    val sequenceProduct = oneToFive.fold(initialValue) { acc, i ->
        println("($acc, $i) -> $acc * $i = ${acc * i} ")
        acc * i
    }

    println("Resultado = $sequenceProduct")
}

Salida:

(2, 1) -> 2 * 1 = 2 
(2, 2) -> 2 * 2 = 4 
(4, 3) -> 4 * 3 = 12 
(12, 4) -> 12 * 4 = 48 
(48, 5) -> 48 * 5 = 240 
Resultado = 240

Función foldRight()

Como su nombre lo indica foldRight() es la variación de fold() pero con un recorrido de derecha a izquierda. Esto significa que el orden de los argumentos de operation se invierte como se ve en la sintaxis para arreglos:

inline fun <T, R> Array<out T>.foldRight(
    initial: R,
    operation: (T, acc: R) -> R
): R

Por ejemplo, dado un arreglo de enteros, construir un String con la representación de la sumatoria de derecha a izquierda a través acompañada de paréntesis de precedencia:

fun main() {
    val numbers = intArrayOf(2,6,7,4)
    val expString = numbers.foldRight("0") { i, acc -> "($acc + $i)" }
    println(expString)
}

Salida:

((((0 + 4) + 7) + 6) + 2)

La solución consiste en invocar foldRight() desde el arreglo numbers. Debido a que se necesita un resultado String, usamos el literal "0" para especificar el resultado de operation. El cuerpo de la función lambda es una plantilla con la expresión de la suma del valor del acumulado y el elemento recorrido.

Función runningFold() o scan()

En el caso de que desees acceder a los resultados parciales de cada acumulación generada en la función fold(), usa la su versión runningFold() o el sinónimo scan(). El resultado es una lista con todos los valores producidos por la función de operación.

Supongamos que deseamos crear una función que genere n números de la sucesión de Fibonacci. Ya que fold() nos permite iterar n veces y además conserva acumulados, podremos llegar a la siguiente solución:

fun main() {
    println(generateFibonacciNumbers(15))
}

fun generateFibonacciNumbers(n: Int): List<Int> {
    return (0..n).scan(0 to 1) { (f1, f2), _ ->
        f2 to (f1 + f2)
    }.map { it.first }
}

Salida:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

La función generateFibonacciNumbers() usa al parámetro n para crear el rango 0..n. Sobre esta colección invocamos a scan(), la cual recibe como valor inicial una instancia de Pair para representar las ecuaciones f0 = 0 y f1 = 1.

Ya que la sucesión produce elementos por sí sola, el segundo argumento pasado en la función lambda puede ser ignorado con un guion bajo ('_'). No necesitamos los elementos del rango, ya que 0 y 1 sirven de semillas para la producción.

La operación generará pares donde con el valor anterior y el siguiente. De esta forma podemos aplicar map() a la lista List<Pair<Int,Int>> arrojada por scan() y solo dejar las propiedades first.

Variaciones Con Índices

Tanto fold() y foldRight() tienen variaciones que reciben al índice como tercer argumento en la función operation. Estas son:

  • foldIndexed()
  • foldRightIndexed()
  • foldRunningIndexed()
  • scanIndexed()

La siguiente sintaxis para iterables de foldIndexed() muestra esta definición:

inline fun <T, R> Iterable<T>.foldIndexed(
    initial: R,
    operation: (index: Int, acc: R, T) -> R
): R

Tomemos como ejemplo la necesidad de construir un código String a partir del carácter que ocupe la posición actual de la palabra en una lista:

fun main() {
    val words = listOf("Patineta", "Lámpara", "Computadora", "Bañera", "Cubo")
    val newWord = words.foldIndexed("_") { index, acc, s ->
        if (s.length > index) acc + s[index] else "$acc@"
    }
    println(newWord)
}

Salida:

_Páme@

La solución invoca a foldIndexed() desde words y aplica una operación de concatenación basado en el acceso con corchetes (s[index]) a través del índice que se recibe como argumento.

Si le palabra tiene un tamaño menor al índice actual, entonces concatenamos el acumulado con el carácter '@'.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!