Aprende

Aprende sobre la última tecnología.

Construye

Da rienda suelta a tus conocimientos y construye!

Comparte

Que más gente lo aproveche para mejorar!
 

scala in a nutshell I - Collections - List

lunes, 5 de septiembre de 2016



Después de un año volvemos, pero con otra perspectiva que según mi opinión es mucho más completa y directa. Todo el código indicado en este post puede ser ejecutado directamente desde la consola de Scala, con lo que el  único requisito será la instalación de Scala. Sino no tienes la instalación tienes aquí el site de scala y si te da mas pereza tengo un script en mi github que también te puede permitir dicha instalación.

En esta entrada explicaremos a fondo una de las principales colecciones de Scala la Lista, hoy indicaremos alguno de los aspectos más básicos que podemos hacer con este tipo de colección.

La vía mas correcta de crear las variables es definiendo sus tipos, como indicamos a continuación, aunque una vía menos organizada sería sin detallar los mismos. En la siguiente porción de código por ejemplo la linea 1 y la 4 tendrán el mismo valor pero es mucho mas claro la 1:

1
2
3
4
scala> val numbers:List[Int] = List(2, 3, 4, 5, 8, 100, 200, 300)
numbers: List[Int] = List(2, 3, 4, 5, 8, 100, 200, 300)
scala> val numbers = List(2, 3, 4, 5, 8, 100, 200, 300)
numbers: List[Int] = List(2, 3, 4, 5, 8, 100, 200, 300)

Como detalle, aunque podemos profundizar luego al respecto, val es un valor inmutable, que no varía en la ejecución  con lo que en una supuesta iteración seguiría conteniendo el mismo valor independientemente que su código cambie.

Las siguientes lineas de código muestran la creación/resultado en consola de listas de String, Int y Listas[Listas] ó lo que es los mismo listas que a su vez contienen otras listas:

scala> val cities:List[String] = List("Havana","Madrid","Oporto") 
cities: List[String] = List(Havana, Madrid, Oporto)

scala> val numbers:List[Int] = List(1,2,4,5)
numbers: List[Int] = List(1, 2, 4, 5)

scala> val listOfList:List[List[Int]]=List(List(1,2,3),List(0,1,3),List(4,2,1))
listOfList: List[List[Int]] = List(List(1, 2, 3), List(0, 1, 3), List(4, 2, 1))


Alguna de las formas de realizar una encuesta sobre  si una lista esta vacía :

scala> val emptyList = List()
emptyList: List[Nothing] = List()

scala> emptyList == Nil
res3: Boolean = true

scala> emptyList.isEmpty
res0: Boolean = true

Dado que muchos de los métodos de las colecciones retornan un tipo estándar de Scala
para valores opcionales, vamos a explicar dicho tipo de un modo muy simple y ya en otras entradas de este blog realizaremos una explicación más a fondo del mismo. Hablamos del tipo Option, es un valor para modelar valores Opcionales.

 Cuando un valor es de tipo Option indica que puede ser de dos tipos :
  •  Some(x) donde x es el valor actual 
  •  None lo cual indica que no tiene ningún valor o tiene un valor nulo
Una vez que tenemos una ligera idea de los tipos Options pasaremos a explicar las diversas vías para construir una lista. En anteriores secciones de código hemos visto la vía más simple para generar una lista. Así como la manera de comprobar si una lista esta vacía.
Una lista en scala se compone de 2 partes:
  • Bloque de Elementos 
  • Nil
Un ejemplo simple de lo anterior es las 2 lineas de código que siguen, donde vemos una lista vacía a la que luego agregamos 4 valores enteros, así como la diferenciación entre el Nil y el Bloque de Elementos:

scala> val nuevaLista :List[Int]= Nil
nuevaLista: List[Int] = List()

scala> 1::2::3::4::nuevaLista
res0: List[Int] = List(1, 2, 3, 4)

Quienes venimos de otros lenguajes, al método  find de una colección simplemente le pasamos el objeto que queremos comprobar existe en dicha colección, el método find de Scala, es diferente:

def find(p: A => Boolean): Option[A]

El método find recibe como parámetro una expresión booleana que debe cumplir un elemento de la colección para que pueda ser retornado en un Some(x) donde x es el valor encontrado. Dado el caso de que el valor buscado no se encuentre en la colección retornará un None.

1
2
3
4
5
6
scala>  val cities:List[String] = List("Havana","Madrid","Oporto") 
cities: List[String] = List(Havana, Madrid, Oporto)
scala> cities.find(city=>city.contentEquals("Havana"))
res9: Option[String] = Some(Havana)
scala> cities.find(city=>city.contentEquals("Havan1a"))
res10: Option[String] = None

La condición establecida en la linea 3 de la anterior sección indica que para cada elemento de la colección cities se va a preguntar si cumple la condición establecida, el primer elemento que cumpla dicha condición será retornado en un Some(el_elemento_buscado), dado el caso que ningún elemento cumpla la condición, como es el caso de la linea 5, retornará un None.

El método filter de una colección, tal y como su nombre lo indica, dado una expresión booleana retornará todos aquellos elementos de la colección que cumplan dicha condición. Un importante aspecto de este método es que si encuentra elementos que cumplen dicha condición, serán retornados en una colección del mismo tipo que sobre la que realizamos el filtrado en caso de que ningún elemento cumpla con la condición se retornará una colección del tipo encuestada vacía.

1
def filter(p: (A)  Boolean): List[A]

Veamos el siguiente ejemplo donde hay una lista que cumple con la condición  filtrado y otra en la que no.

1
2
3
4
5
6
scala> val cities:List[String] = List("Havana","Madrid","Oporto","Malaga","Matanza") 
cities: List[String] = List(Havana, Madrid, Oporto, Malaga, Matanza)
scala> cities.filter(city=>city.startsWith("M"))
res2: List[String] = List(Madrid, Malaga, Matanza)
scala> cities.filter(city=>city.startsWith("x"))
res3: List[String] = List()

Tal y como la siguiente definición lo indica tenemos el método partition de una colección. Dada una condición booleana aplicada a una colección, todos aquellos miembros de la colección que cumplan la misma son agrupados en la parte izquierda de la tupla y los que no cumplen la misma  a la parte derecha de la tupla. En resumen tendremos una tupla contenida por dos listas.

def partition(p: (A)  Boolean): (List[A], List[A])

Veamos como ejemplo una lista de enteros en el que queremos partir dicha lista en 2 grupos.Los mayores de 20 y el resto.

1
2
3
4
scala> val listaNumerica:List[Int] = List(1,3,4,5,8,20,28,14,12)
listaNumerica: List[Int] = List(1, 3, 4, 5, 8, 20, 28, 14, 12)
scala> listaNumerica.partition(numero=>numero>10)
res3: (List[Int], List[Int]) = (List(20, 28, 14, 12),List(1, 3, 4, 5, 8))

Vemos nuestro resultado en la linea 4 de la anterior sección de código. Supongamos ahora que lo que queremos es almacenar esos resultados, para poder procesarlos. Obviamente debe de ser a través de una tupla, en cada parte de la misma devolveremos una lista. En el elemento izquierdo de la tupla aquellos elementos que cumplen la condición boleana,en nuestro caso los numero > 20  y en el derecho los que no. Una vez más los elementos  serán retornados en una colección del mismo tipo que sobre la que realizamos la operación en cuestión.

1
2
3
scala> val (elementMayorQue10,elementoMenorQue10) = listaNumerica.partition(numero=>numero>10)
elementMayorQue10: List[Int] = List(20, 28, 14, 12)
elementoMenorQue10: List[Int] = List(1, 3, 4, 5, 8)

En la anterior sección dejamos el resultado en una tupla cuyos integrantes (elementMayorQue10 y elementoMenorQue10) podremos usar luego en cualquier parte de nuestro código.

final def span(p: (A)  Boolean): (List[A], List[A])

El método Span de una colección, retorna una tupla de prefijo/sufijo, a partir del primer elemento de la colección que no cumpla con la condición conformará la segunda parte de la tupla, ello sin importar que existan otros elementos restantes que si la cumplan. La manera más simple de explicarlo será a través de un ejemplo.

1
2
3
4
5
scala> val listOfIntegers:List[Int] = List(15, 10, 5, 20, 12,8)
listOfIntegers: List[Int] = List(15, 10, 5, 20, 12, 8)

scala> listOfIntegers.span(_ < 20)
res12: (List[Int], List[Int]) = (List(15, 10, 5),List(20, 12, 8))

Como podemos apreciar en la porción anterior de código, hemos hecho un span con los menores de 20 y ha dividido nuestra lista a partir del 20, el primer elemento que no cumplía con la condición.No importa que luego existan otros elementos que la cumplan, a partir del primero que la incumpla se formara la otra parte de la tupla. También aquí serán retornados el conjunto de elementos en una colección del mismo tipo que sobre la que realizamos la operación en cuestión.

A continuación trataremos el método groupBy a aplicar sobre una colección y que tiene la siguiente firma:

def groupBy[K](f: (A)  K): Map[K, List[A]]

Este método Particiona la lista en un mapa de listas de acuerdo a una determinada función. De modo que por ejemplo si agrupamos una lista de String por su longitud todos aquellos elementos de la lista que tengan la misma longitud estarán en una misma clave K y pertenecerán a la misma Lista[A].

1
2
3
4
5
scala> val seqOfString :Seq[String] =  Seq("prueba","comida","truco","trato","canada","dibujo","pregunta")
seqOfString: Seq[String] = List(prueba, comida, truco, trato, canada, dibujo, pregunta)

scala> seqOfString.groupBy(_ length)
res2: scala.collection.immutable.Map[Int,Seq[String]] = Map(8 -> List(pregunta), 5 -> List(truco, trato), 6 -> List(prueba, comida, canada, dibujo))

El ejemplo anterior tenemos una secuencia de String(linea 1) y en la linea 4 agrupamos los items de la colección por la longitud de los mismos y tendremos como resultado un Map[Int,List[String]] donde cada clave K es una longitud y el valor asociado a la clave es una lista que agrupa a todos los elementos de una misma longitud (K). Como un detalle importante, la clave K es del tipo que devuelve nuestra función usada para discriminar, en nuestro anterior ejemplo usamos la longitud de la cadena para discriminar con lo que la clave es un Integer.

En nuestro siguiente ejemplo imaginemos una lista de números enteros en las que queremos agrupar los mayores de los menores de 20. Con lo cual nuestro resultado será un Map[Boolean,List[Int]] en el que tendremos 2 claves(K) false y true (linea 5). Para la clave K= true tendremos como valor una lista que agrupa todos los items de la colección mayores de 20 y para K=false los menores que 20.

1
2
3
4
5
scala> val listaDeEnteros: List[Int] = List(1,2,34,33,56,56,32,20)
listaDeEnteros: List[Int] = List(1, 2, 34, 33, 56, 56, 32, 20)

scala> listaDeEnteros.groupBy(_ > 20)
res4: scala.collection.immutable.Map[Boolean,List[Int]] = Map(false -> List(1, 2, 20), true -> List(34, 33, 56, 56, 32))

Trataremos ahora probablemente uno de los métodos más importantes y más usados de las colecciones:

1
def map[B](f: (A)  B): List[B]

Construye una nueva colección aplicando la función a cada uno de los elementos de la colección sobre la que queremos hacer el map.También aquí serán retornados los elementos en una colección del mismo tipo que sobre la que realizamos la operación en cuestión.


1
2
3
4
5
6
7
8
scala> val listForMap:List[Int] =List(10,20,30,40,50)
listForMap: List[Int] = List(10, 20, 30, 40, 50)

scala> val listMultiplicada = listForMap.map(_ * 2)
listMultiplicada: List[Int] = List(20, 40, 60, 80, 100)

scala> val listMultiplicada = listForMap.map(element => element * 2)
listMultiplicada: List[Int] = List(20, 40, 60, 80, 100)

El anterior ejemplo de código muestra como se construye una nueva lista aplicándole una función a cada uno de los elementos de la colección. En la linea 4 de la anterior sección de código se multiplica cada elemento de la lista x 2, el resultado de esta linea y el de la linea 7 es el mismo solo que en la linea 4 usamos lo que denominamos placeholder, más adelante veremos como dependiendo de una utilidad u otra será mas conveniente usar una opción u otra.

Veamos de una manera más directa el uso de funciones. Pondremos el ejemplo más simple posible de aplicar funciones, pero puede ser aplicado a cualquier función. Recordar que siempre retornamos una colección del  mismo tipo que la colección sobre la que realizamos la operación.

En la siguiente porción de código la Linea 1 y la linea 4  se muestran dos maneras diferentes de definir una función que multiplica el parámetro de entrada x 2. Recordar que listForMap ya fue definida en la anterior porción de código y ahora vemos como se le aplican las diferentes funciones  a cada uno de los elementos de la colección.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
scala> def flong(x:Int):Int= x*2
flong: (x: Int)Int

scala> def  f: Int => Int = x  => x*2
f: Int => Int

scala> val listMultiplicada = listForMap.map(element => f(element))
listMultiplicada: List[Int] = List(20, 40, 60, 80, 100)

scala> val listMultiplicada = listForMap.map(element => flong(element))
listMultiplicada: List[Int] = List(20, 40, 60, 80, 100)

En futuras entradas del blog pondré ejemplos de una mayor complejidad en el uso de este método aunque lo aquí explicado puede ser extrapolado a cualquier situación.

Trataremos otro de los métodos de gran utilidad en una colección. Nos referimos a flatMap. Este método construye una nueva colección aplicando una función a todos los elementos de la lista y usando los elementos de la colección resultante. De manera general tiene sentido en colecciones anidadas, hablamos de colecciones que contienen a su vez otras colecciones y queremos que nuestra colección resultante sea la colección más externa, aunque también puede ser que busquemos en ocasiones que sea lo contrario o sea que la colección resultante sea la más interna. De cualquier modo la mejor manera de verlo será a través de ejemplos.

1
def flatMap[B](f: (A)  GenTraversableOnce[B]): List[B]

El siguiente ejemplo define una lista que contiene a su vez una lista de ciudades o sea una lista en la que cada uno de sus items es a su vez otra lista de String. Tal y como el siguiente ejemplo indica:

1
2
3
4
5
scala> val ciudades: List[List[String]]=List(List("madrid","barcelona"),List("la habana","cienfuegos"),List("london","mancehster"))
ciudades: List[List[String]] = List(List(madrid, barcelona), List(la habana, cienfuegos), List(london, mancehster))

scala> ciudades.flatMap(x=>x.map(cities=>cities.toUpperCase))
res13: List[String] = List(MADRID, BARCELONA, LA HABANA, CIENFUEGOS, LONDON, MANCEHSTER)

Aplicamos una función a cada uno de los elementos(ciudades) de la lista y el resultado lo deja en una nueva colección. Fijaros como se han procesado las colecciones internas y sus elementos se han sacado a la colección contenedora, es siempre el mismo principio con los flatMaps, si en lugar de  que cada Items fuera una lista de String estuviéramos ante un objeto determinado, podríamos actuar sobre cada uno de los objetos y devolver en la colección contenedora los elementos resultantes de la aplicación de una determinada función sobre los items de la colección.

Veamos otro ejemplo para verlo desde otro punto de vista :

1
2
3
4
5
scala> val listaDeString: List[String] = List("madrid","londres","paris")
listaDeStrings: List[String] = List(madrid, londres, paris)

scala> listaDeStrings.flatMap(char=>char.toUpperCase)
res9: List[Char] = List(M, A, D, R, I, D, L, O, N, D, R, E, S, P, A, R, I, S)

Procesamos los elementos y los sacamos a una colección contenedora. Aquí cada uno de los elementos serán cada carácter que componen el array de string que forman nuestras palabras. Recordar que siempre retornamos una colección del  mismo tipo que sobre la que realizamos la operación.

Ahora que hemos visto tanto el flatMap como el map, veamos un ejemplo que combina a ambos, donde de una Lista de Maps nos retornará una Lista de un Map, todos han sido unidos en la colección contenedora(en otras entradas será explicado el Map como colección, de un modo rápido hablamos de una estructura clave-valor):

scala> val seqMapNumeros: Seq[Map[String,Int]] = List(Map("seis"->6,"cicnco"->5,"cuatro"->4),Map("nueve"->9,"ocho"->8,"siete"->7))
seqMapNumeros: Seq[Map[String,Int]] = List(Map(seis -> 6, cicnco -> 5, cuatro -> 4), Map(nueve -> 9, ocho -> 8, siete -> 7))

scala> seqMapNumeros.flatMap((x=>x))
res2: Seq[(String, Int)] = List((seis,6), (cicnco,5), (cuatro,4), (nueve,9), (ocho,8), (siete,7))

scala> seqMapNumeros.flatMap((x=>x)).map(y=>y._1)
res3: Seq[String] = List(seis, cicnco, cuatro, nueve, ocho, siete)

scala> seqMapNumeros.flatMap((mapa=>mapa)).map(mapa=>mapa._1)
res4: Seq[String] = List(seis, cicnco, cuatro, nueve, ocho, siete)

scala> seqMapNumeros.flatMap((mapa=>mapa)).map(mapa=>mapa._2)
res5: Seq[Int] = List(6, 5, 4, 9, 8, 7)

Según la anterior sección tenemos como entrada una Seq[Map[String,Int]] en otras palabras una secuencia donde  cada Item es un Map y aplicarle el flatMap tenemos como resultado una lista de Tuplas o lo que es lo mismo una Lista  de Un Map. Han sido sacados todos los items que estaban en maps individuales y pueden ahora ser procesados para un Map común.

Veamos la siguiente sección de código:

1
2
3
4
5
6
scala> seqMap.flatMap(x=>x).toMap
res3: scala.collection.immutable.Map[String,Int] = Map(ocho -> 8, siete -> 7, nueve -> 9, cicnco -> 5, seis -> 6, cuatro -> 4)
scala> seqMapNumeros.flatMap(x=>x)
res4: Seq[(String, Int)] = List((seis,6), (cicnco,5), (cuatro,4), (nueve,9), (ocho,8), (siete,7))
scala> seqMapNumeros.flatMap(x=>x).toMap
res5: scala.collection.immutable.Map[String,Int] = Map(ocho -> 8, siete -> 7, nueve -> 9, cicnco -> 5, seis -> 6, cuatro -> 4)

Respecto a lo que anteriormente explicábamos veamos como en la linea 5 aplicando un map a nuestro flatMap resultante obtenemos un Map de todos los elementos. Hemos partido de Seq[Map[String,Int]] y hemos obtenido Map[String,Int]], en este caso hemos abierto el contenido a nuestra colección mas interna.

Tenemos headOption, método que nos devuelve el primer elemento de la colección, se solía usar en combinación con filter, de modo que filtramos una colección y si a la colección filtrada le aplicamos el headOption devuelve el primer elemento de la colección en un Some(primer _elemento) o un None dado que ningún elemento cumpla con los requisitos.

def headOption: Option[A]

Imaginemos en la siguiente sección de código una lista de enteros (linea 1). Ahora filtramos la lista para aquellos elementos de la misma que cumplan determinada condición y posteriormente obtenemos el primer elemento del subconjunto ya filtrado (linea 4). Como un detalle, si la lista esta vacía (linea 7) y en lugar de un headOption tuviéramos un head nos daría una exception, es esta la gran ventaja de nuestro headOption.

1
2
3
4
5
6
7
8
scala> val listOfNumber: List[Int] = List(10,30,45,76,66,20)
listOfNumber: List[Int] = List(10, 30, 45, 76, 66, 20)

scala> val optionOfHeadOfNumber: Option[Int] = listOfNumber.filter(_ > 30).headOption 
optionOfHeadOfNumber: Option[Int] = Some(45)

scala> val optionOfHeadOfNumber: Option[Int] = listOfNumber.filter(_ > 100).headOption 
optionOfHeadOfNumber: Option[Int] = None

Una solución similar a la anterior pero más completa nos viene con el método find.

def find(p: (A)  Boolean): Option[A]

Encuentra el primer elemento, si existe y que satisfaga la condición booleana. Se hace en un solo método nuestra anterior combinación filter + headOption , consiguiendo el mismo resultado y la consecuente ventaja de su uso en una linea hacerlo prácticamente todo.

scala> val optionOfHeadOfNumber: Option[Int] = listOfNumber.find(_ > 30)
optionOfHeadOfNumber: Option[Int] = Some(45)

Veamos ahora el funcionamiento de fold,  del que podríamos decir que  "hace un merge o sintetiza" los elementos de la colección usando un operador asociativo (que puede ser ejecutado en un orden arbitrario [suma,multiplicación]) muy importante también es destacar que el fold ha de contar siempre con un valor inicial ó acumulador.

def fold[A1 >: A](z: A1)(op: (A1, A1)  A1): A1

Es importante destacar que en el fold las operaciones sobre el conjunto de elementos de la colección no tienen un orden y además el acumulador(z) puede ser aplicado como parámetro al operador(op) una cantidad indeterminada de veces, tal y como indica la especificación.

Veamos el siguiente ejemplo :

scala> List(1,2,3).fold("gdg")( _.toString() + _.toString())

1era iteración -> "gdg" + "1" -> "gdg1"
2da iteración -> "gdg1" + "2" -> "gdg12"
3era iteración -> "gdg12" + "3" -> "gdg123"

["posible cuarta iteracion"] -> "gdg123" + "gdg" -> "gdg123gdg"

Y así sucesivamente. En todas las pruebas que he realizado el comportamiento ha sido similar hasta la "3era iteración" pero la especificación da lugar a que el resultado puede ser como el indicado hasta la cuarta iteración e incluso que se sigan saliendo iteraciones. Además de que el orden puede NO ser el indicado en el anterior tópico con respecto a las operaciones. Es por ello que nuestro acumulador es recomendable que sea un valor neutral 0 en el caso de Integer por ejemplo o una cadena vacía para el caso de los String.

Veamos en el siguiente ejemplo como se "mergea" o sintetiza  una lista partiendo de "cero" de manera que nuestro resultado final sea la suma de  todos los elementos de la lista. Nuestro acumulador constituye una base estas Ops con lo que como resultado final obtendremos la suma de los elementos de la lista en cuestión mas la suma de nuestro acumulador una ó n veces, solo que tal y como recomienda la especificación  si este elemento es neutral (como es el caso) no afectará al resultado final.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
scala> val listOfInteger: List[Int] = List (1,2,3,4,5) 
listOfInteger: List[Int] = List(1, 2, 3, 4, 5)

scala> (listOfInteger fold 0) (_ * _)
res11: Int = 0

scala> (listOfInteger fold 0) (_ + _)
res12: Int = 15

scala> listOfInteger.fold (0) (_ + _)
res13: Int = 15

scala> listOfInteger.fold (10) (_ + _)

Las lineas 7 y 10 son dos formas  de expresar lo mismo, que en nuestro caso se reduce a ir sumando los elementos de una lista de entero.

Tenemos varios detalles a destacar:
Siempre el resultado de la suma anterior se suma al elemento siguiente y nuestro acumulador que en las lineas que nos ocupa es 0 puede ser pasado como parámetro de la operación en cualquier punto del proceso. Ver como sin embargo el resultado de una operación como la presente en la linea 13 seria 25 (10[nuestro acumulador] + 15). En ambos casos (linea 10 y 13) no obtenemos colección, tal y como la firma del método indica obtenemos la lista "mergeada" o "sintetizada" como resultado de una operación asociativa(en este caso la suma).

Veamos el siguiente ejemplo para que nos deje de una manera mas clara la funcionalidad de dicho método .

1
2
3
4
5
scala> ((listOfInteger) fold "") ((s1, s2) => s"$s1 - $s2")
res15: Any = " - 1 - 2 - 3 - 4 - 5"

scala> ((listOfInteger) fold "prueba") ((s1, s2) => s"$s1 - $s2")
res16: Any = prueba - 1 - 2 - 3 - 4 - 5

Como podemos ver en la porción de código anterior, ahora nuestro acumulador es una cadena. Ver la diferencia cuando el acumulador es una cadena vacía y cuando tiene el valor prueba. Tenemos pendiente los distintos tipos de datos en Scala con lo que de momento no dar importancia al tipo Any que aparece las lineas 2 y 5, a pesar de que sean de gran importancia en este caso teniendo en cuenta la firma del método. Como un detalle más debemos destacar que el hecho de que el acumulador(prueba) no sea una valor neutral  puede dar como resultado otros valores aleatorios diferentes a lo mostrado en la sección anterior de código.

La siguiente operación generará un error debido a que es una operación en la que si decisivo el orden de las  operaciones a realizar:

scala> List("1","2","3").fold(0)(_ + _.toInt)
<console>:11: error: value toInt is not a member of Any
       List("1","2","3").fold(0)(_ + _.toInt)

Es importante destacar que en situaciones como esta (donde el orden de las operaciones cobre importancia) no podemos considerar el uso de fold. Basándonos en la anterior linea de código sería un error el siguiente análisis :

1era iteración -> op(0,"1") -> 0 + "1".toInt -> 1
2da iteración -> op(1,"2") -> 1 + "2".toInt -> 3
3era iteración -> op(3,"3") -> 3 + "3".toInt -> 6

Partiendo del hecho en el cual la propia naturaleza del fold indica que no hay un orden establecido para las operaciones entonces NADA indica que la primera iteración sea  op(0,"1") -> 0 + "1".toInt -> 1 podría simplemente ser cualquier otra combinación en la que ya por ejemplo sea  op(0,"1") -> 1 + 0.toInt -> 1 y ya tendríamos nuestro primer error.

Hemos de saber que un detalle muy importante de la funcionalidad de fold es que partiendo sobre donde apliquemos dicho método  no será lo mismo el concepto de "sintetizar o mergear" en todos los tipos de Colecciones u Objetos sobre los que se pueda aplicar el fold, por lo que supondremos aquí que a todos nos está claro el concepto de polimorfismo.

Algunos de los anteriores errores pueden ser solventados con nuestro siguiente método.

def foldLeft[B](z: B)(op: (B, A)  B): B

Tal y como nos indica la especificación op(...op(z, x_1), x_2, ..., x_n) lo que haremos será aplicar el operador en una dirección de Izq -> Der
De manera que de IZQ a DER :

1era iteración op(acumulador,primer_elemento_de_la_colección)-> Resultado_1
2da iteración op(Resultado_1,segundo_elemento_de_la_colección)-> Resultado_2
3era iteración op(Resultado_2,tercer_elemento_de_la_colección)-> Resultado_3

y así sucesivamente hasta que termine la colección.

Una de las grandes diferencias de foldLeft con fold es que en fold no hay ninguna garantía sobre el orden en el que los elementos son procesados mientras que en foldLeft sabemos que comenzamos de IZQ -> DER.

Veamos una caso práctico donde usamos foldLeft y sin embargo no puede ser usado fold:

scala> List("1","2","3").foldLeft(0)(_ + _.toInt)
res59: Int = 6

El orden de secuencia de las operaciones aquí ejecutadas es el siguiente:

1era iteración (0 + "1".toInt) -> 1
2da iteración (1 + "2".toInt) -> 3
3era iteración (3 + "3".toInt) -> 6 [el resultado final]

Siempre  vamos a tener la garantía que cualquier foldLeft que realicemos sobre una colección la primera operación aplicada sobre la colección será op(acumulador,1er_item_de_la_colección)

Nuestro siguiente método será el foldRight cuyo funcionamiento, hablando del modo más simple
será similar a foldLeft pero tanto las iteraciones como el orden del acumulador inicial será contrario
a foldLeft, osea comenzaremos por la derecha.

def foldRight[B](z: B)(op: (A, B)  B): B

Tal y como podemos apreciar la firma del método es la misma aunque la manera de hacer las iteraciones es de DER -> IZQ

Tenemos ahora mismo un ejemplo bastante simple pero que nos podrá dar una idea más clara:

scala> List(1,2,3,4,5,6,7,8,9,10).foldRight(1)((a,b) => {println(s"$a,$b");a})
10,1
9,10
8,9
7,8
6,7
5,6
4,5
3,4
2,3
1,2
res12: Int = 1

Veamos otro ejemplo similar al visto en la sección de foldLeft:

scala> List("1","2","3").foldRight(0)(  _.toInt + _)
res14: Int = 6

Vemos como ha sido necesario cambiar el orden de los parámetros.Y ahora veamos el orden de las operaciones.

Importante: Ver el orden o posición que ocupa en nuestro caso el acumulador(a la derecha).

1era iteración op(primer_elemento_de_la_colección,acumulador)-> Resultado_1
2da iteración op(segundo_elemento_de_la_colección,Resultado_1)-> Resultado_2
3era iteración op(tercer_elemento_de_la_colección,Resultado_2)-> Resultado_3

Un funcionamiento común de todos los folds es que si la colección está vacía retornamos el acumulador, que de manera general debemos intentar que sea NEUTRAL.

Que queremos decir con NEUTRAL :
Si es una lista -> Nil
Si es un operador de multiplicación -> 1
Si es un operador de String -> ""

Y así sucesivamente.

Si podemos adaptar nuestro código para usar fold sería perfecto aunque muchas veces no podremos. Si podemos adaptar nuestro código para en lugar de usar foldRight usar foldLeft ello seria mucho mejor. Sobre todo debido a la eficiencia que tiene el foldLeft sobre foldRight.(Algo que explicaremos en futuras entradas)

Trataremos ahora el método reduce, cuya gran diferencia con el método fold es que no tiene un valor inicial, con lo cual hemos de asegurarnos que la colección sobre la que le usemos no es una colección vacía. También aquí se reduce o "mergea" una colección usando operadores asociativos.

def reduce[A1 >: A](op: (A1, A1)  A1): A1

Pongamos algún ejemplo como es el caso de cuando queremos generar ficheros csv compuestos
obviamente por varias entradas CSV. Dada la siguiente colección veamos como resuelve el foldLeft el hecho de querer un fichero cuyas lineas posean valores separados por comas, por ejemplo:

cala> val valores: List[String] = List("ValorCSV1", "ValorCSV2", "ValorCVS3","ValorCSV4","ValorCSV5") 
valores: List[String] = List(ValorCSV1, ValorCSV2, ValorCVS3, ValorCSV4, ValorCSV5)

scala> valores.fold("")(_ + "," + _)
res15: String = ,ValorCSV1,ValorCSV2,ValorCVS3,ValorCSV4,ValorCSV5

Si procesamos nuestro fold podremos eliminar la "," pero si no queremos realizar este procesamiento
podríamos ejecutar un reduce sobre la colección:

scala> valores.reduce(_ + "," + _)
res16: String = ValorCSV1,ValorCSV2,ValorCVS3,ValorCSV4,ValorCSV5

scala> valores.reduceLeft(_ + "," + _)
res17: String = ValorCSV1,ValorCSV2,ValorCVS3,ValorCSV4,ValorCSV5

Lo único que hay que garantizar es que valores no sea una lista vacía. En aquellas ocasiones que reduceLeft y reduceRight nos lleven a un mismo resultado lo más recomendable es usar reduce situación que es la que se da en la mayoría de los casos. Como mismo explicamos en su momento para el caso de fold, dado el caso que necesitemos que el orden de las operaciones sea un orden específico tendremos que usar el reduce(left ó right) adecuado.

scala> listNumber.reduce(_ min _)
res31: Int = 2

scala> listNumber.reduce(_ max _)
res32: Int = 20

scala> listNumber.reduceLeft(_ max _)
res33: Int = 20

scala> listNumber.reduceRight(_ max _)
res34: Int = 20

scala> listNumber.max
res35: Int = 20

scala> listNumber.min

Este es un caso en el que sería mejor usar reduce o incluso ni eso. Pero imaginemos que nuestra op en lugar de ser max o min,  es una división, en este caso si nos importa el orden de los parámetros y tendríamos entonces que ver qué reduce(left o right) usar.

En esta entrada hemos descrito los métodos más importantes de List, el resto de los mismos lo podrás encontrar en la api del lenguaje , solo mencionar métodos a recomendar y que están muy bien explicados en la api :

find,flatten,diff,contains,exists
filterNot,foreach,last,tail,collect y sobre todo todas las operaciones de aritméticas sobre lista (::,:::,+:,++:,++ y resto de op de esta indole)

De todas formas, antes de hacer cualquier operación buscar en la Api, en la mayoría de las ocasiones hay siempre distintas vías, unas mas optimas que otras.