El poder de Option: Más allá del pattern matching

Una de las primeras cosas a las que le tomamos gusto cuando aprende Scala, sobre todo si venimos de Java, es al pattern matching, aunque una de las cosas que desconciertan un poco es la manera en que funcionan los mapas.

En Java, si tenemos un   simplemente le pedimos el valor para una llave, y nos devuelve el valor, o null si no lo tiene. O si el mapa acepta nulos, entonces puede devolvernos null si es que tiene null guardado bajo la llave que indicamos. ¿Cómo podemos diferenciar entre el caso en que el mapa no tiene la llave, o si tiene almacenado null bajo esa llave? Podemos verificar usando  .

Entonces, tenemos los siguientes casos:

 

¿De verdad nos gusta hacer esto en Java? En la práctica, casi no tenemos mapas donde guardemos null, incluso algunos lo consideran una mala práctica. Pero puede darse el caso... pero normalmente tenemos solamente algo como esto:

 

Y a partir de ahí ya estamos seguros que tenemos un X. Pero bueno, los que llevamos tiempo en Java ya estamos acostumbrados a lidiar así con los mapas. Y luego llegamos a Scala y resulta que es algo completamente distinto:

 

Me devuelve el valor almacenado bajo la llave "X", o arroja una excepción si el mapa no tiene esa llave. Qué molesto tener que poner cada acceso a un mapa en un  ! Pero hay otra manera:

 

Pero resulta que el método get del mapa en Scala no devuelve el valor directamente, sino una cosa llamada Option.

Option básico

Esta clase abstracta   puede tener dos encarnaciones en la práctica: Some y None. None pues significa que no hay nada, y Some es un contenedor de un objeto (que puede ser null, por cierto). De modo que con esa última línea de código, x puede ser None o puede ser Some(algo), donde algo es el objeto almacenado en el mapa, el que realmente nos interesa.

Entonces, leyendo un poco nos encontramos con que podemos hacer algo así, si queremos obtener el mismo comportamiento del último ejemplo que puse en Java, donde obtenemos el valor y si no está lo creamos y almacenamos:

 

Si el mapa tiene el valor, entraremos al caso de   donde simplemente devolvemos el valor contenido en el Some. Si no tiene el valor, entramos al caso  , en donde creamos el objeto que necesitamos, lo ponemos en el mapa y lo devolvemos, usando el método  , el cual no arrojará excepción en este caso porque acabamos de poner el valor en el mapa.

Más allá del pattern matching

Pero   no nada más es un simple contenedor de valores. Tiene varios métodos muy útiles, que nos permiten encadenar la ejecución de distintas funciones, cada una operando sobre el resultado de la anterior. Un ejemplo es el método  . Supongamos que tenemos un mapa cuyos valores son cadenas y queremos obtener una de esas cadenas, pero en mayúscula y alrevés. Al final haremos pattern matching pero sólo sobre el resultado final, no tenemos que estar manejando condiciones intermedias:

 

Si el mapa tiene una cadena bajo la llave indicada, obtendremos un Option, sobre el cual invocamos   con la función anónima  , la cual invocará sobre la cadena contenida en el Some; el resultado de eso será otro   pero que ahora contiene la cadena en mayúsculas; a ese le invocaremos   con la función anónima   y el resultado será otro   que contiene la cadena alrevés. De este modo, si la cadena original era "hola", al final tenemos "ALOH" y sobre eso se hace el match.

Si el mapa no tuviera una cadena bajo la llave indicada, entonces devuelve None. Cuando se invoca   sobre None, no se ejecuta la función que recibe como parámetro, simplemente se devuelve  .

Las funciones que se pasan al método   pueden incluso devolver un objeto de una clase distinta. Por ejemplo si hacemos algo así:

 

Obtendremos al final un Option con la longitud de la cadena (un Int).

Option tiene varios métodos que resultan muy útiles para operar directamente sobre los valores sin tener que estar comprobando si los tenemos o no, lo cual nos ahorra el insertar varios  's. Algunos ejemplos son  ,  ,  ,  :

 

Este tipo de cosas es lo que nos permite usar un estilo de programación más declarativo, en vez de imperativo. Regresando al ejemplo de obtener un objeto de un mapa y agregarlo en caso de no estar, resulta que los mapas en Scala son animales bastante más sofisticados que los mapas de Java. En Scala, un mapa también tiene varios métodos muy útiles como collect, exists, filter, find, getOrElse y getOrElseUpdate, entre otros. Este último nos sirve precisamente para realizar esa operación que ya vimos en Java y también en Scala con pattern matching, pero en un solo paso:

 

Si el mapa contiene la llave "X", devolverá el valor de la misma, ya no como un Option sino directamente el valor; pero si no tiene un valor bajo esa llave, entonces ejecuta la función que se le pasa como segundo parámetro, almacena el resultado de la misma bajo la llave solicitada, y lo devuelve. Esta última versión no sólo es más breve que la de 4 líneas en Java y sobre todo que la de 5 líneas en Scala, sino que por el nombre del método queda más claro lo que está haciendo.

En fin, este tipo de cosas son las que he ido descubriendo en mis exploraciones de Scala, espero les sean de utilidad.

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de bferro

Los ejemplos de Option de@ezamudio me dan pie para:

Muy buen post sobre Option, Some y None. Y muy adecuados para introducir "CallBy Name" en Scala.

Scala nos ofrece dos alternativas para evaluar los argumentos que se le pasan a una función: Call by Value y Call by Name.

Típicamente los parámetros de las funciones son parámetros "by value". Esto significa que la expresión que es pasada como argumento a una función se determina o evalúa antes que comience a ejecutarse la función.

En ocasiones necesitamos escribir funciones que aceptan una expresión como parámetro que no queremos que se evalúe hasta que ella sea llamada dentro del cuerpo de la función. Scala ofrece esa alternativa definiendo que los parámetros sean "by name" y para eso ofrece una sintaxis especial.

Un parámetro "by name" se especifica omitiendo los paréntesis que normalmente acompañan un parámetro de una función:
 

Ese mecanismo es el que utiliza por ejemplo el método   de la clase Option para hacer lo que hace. El código de ese método es el siguiente:
 

La expresión   no es evaluada hasta tanto no sea usada dentro del cuerpo de la función.

A continuación un ejemplo bonito tomado de "Scala in Depth".

Seguramente hemos tenido necesidad en Java de crear directorios temporales para diferentes programas que escribimos. Se puede dar el caso que especificamos ese directorio temporal o que nos satisface usar el directorio temporal que está especificado en la propiedad correspondiente de la máquina virtual.
Podemos entonces escribir una función que acepte como argumento un Option, cuyo valor puede ser None y en ese caso NO estamos especificando el directorio , o cuyo valor sea un contenedor Some que especifique el directorio.
La siguiente función resuelve ese problema:
 

La expresión que recibe como argumento la función   será evaluada dentro del cuerpo de la función solo en el caso que   sea None.

Evaluamos esa función en el REPL. Tengo en mi máquina un directorio "/borrame" pero NO tengo un directorio "/eraseme".

 

Muy mona esa mónada.

Entendiendo como funciona   se comienzan a abrir más y más caminos hacia la programación funcional y nos empezamos a alejar de la programación Orientada a Objetos / procedural.

Lo bueno de Scala es que permite hacer este cambio gradual y no te pone en una posición de todo o nada. Es por eso que me parece que la parte OO de Scala es solo un gancho que permite atraer a los programadores y es claro que fue una buena decision.

La clase   es una mónada ( ja ja casi suena antinatural decir eso :P ) que permite el estilo de programación funcional como lo muestra el post, sin tener que andar haciendo validaciones como revisar si el valor es null y eso. Lo que me parece algo raro pero voy entendiendo cada vez más, es que este tipo de construcciones se pusieron en las bibliotecas base y no tanto en el lenguaje mismo, me pregunto si la razón fue para acelerar la implementación del lenguaje?

Les dejo un ink en StackOverflow sobre el uso de   en esta pregunta: Why Option[T]?

Para entender más sobre las mónadas les recomiendo este artículo que de todos los que he encontrado me ha parecido el más fácil de entender.

Monads are not metaphors

+1 por el artículo y esperamos más para seguir aprendiendo Scala y programación funcional con cosas como currying, tipos algebraicos, tipos de alto nivel ( higher kinds ), polimorfismo paramétrico y todas esas cosas que si bien hay mucha literatura en internet, a veces parecen más intimidantes de lo que en realidad son y resultan más interesantes cuando alguien escribe un ejemplo como en tus posts.

Imagen de bferro

Sigo aprovechando la discusión de Option

Aprovecho este post para abundar un poco más sobre algunas "cosas" interesantes del lenguaje Scala.Espero continuar con la parte 3 de Scala en algún momento. El tiempo libre no abunda.

Las clases (objetos)   son relativamente sencillas y podemos entender su código con relativa facilidad. Se aprende a programar, programando y leyendo programas, por lo que conviene aquí copiar el código fuente de estas clases y revisarlo, para ver algunos conceptos importantes.

  es una clase abstracta sellada que viene acompañada con su objeto "acompañante" (companion object). Ambas cosas se incluyen en el archivo   en la distribución del código fuente de Scala. En ese archivo también se incluye las definiciones de  .

A continuación ese código (conviene leerlo con paciencia).

 

Toca ahora comentar ese código.

Se definen en ese archivo varias cosas:

  • El objeto   singleton que acompaña a la clase  .
  • La clase   abstracta y sellada (sealed)
  • La clase anidada   que es usada por el método   de la clase  .
  • La clase   final que hereda de la clase   y define los métodos abstractos   en la clase  .
  • El objeto   singleton que hereda de la clase   y define los métodos abstractos   en la clase  .

El método:

 

Es el método de conversión implícita para convertir un valor de tipo   en un valor de tipo  . Cuando el compilador encuentra un valor de tipo   en el lugar que le corresponde a un valor de tipo  , aplica de manera automática este método, para poder entonces usar toda la funcionalidad del trait y el objeto singleton  , sobre un valor de tipo  .
  es un trait base para todas las colecciones de Scala que brindan un método  .
El código de   convierte el valor de tipo   en una lista (que por supuesto es iterable).

El método:

 

Es el método para fabricar objetos de tipo  . Es el método que se aplica cuando usamos la sintaxis dulce  . Sigue un ejemplo:

 

El método:
 

Es un método de fábrica utilitario que crea un valor de tipo  . Se incluye este método para lograr consistencia con la jerarquía de colecciones de Scala. Todas las colecciones en Scala tienen este método. Sigue un ejemplo:
 

Aparece en el código anterior el tipo  . Es un subtipo de todos los tipos en Scala, incluyendo los tipos valores que descienden de   y de los tipos referencias que descienden de  . No existen instancias del tipo  .

La clase  

La definición de esta clase:
 

Varias cosas:

  • Option es una clase abstracta
  • Option es una clase parametrizada covariante. Sigue un ejemplo de covarianza:
     
  • La clase Option hereda del trait   que es el trait base para todos los tipos productos,incluyendo los tipos  . Sigue un ejemplo:

     

  • Option es una clase sellada (sealed). Las clases selladas son importantes cuando se quiere asegurar que la jerarquía de clases case que derivan de una clase es fija y no puede ampliarse,de forma que nadie puede crear una nueva subclase case. Para esto, el lenguaje obliga a que todas las clases y objetos case se definan en el mismo archivo fuente de la clase sellada. Es el caso de la clase  , de la clase case   y del objeto case  . El compilador conoce todos los valores que pueden aparecer en una expresión match con un valor de tipo  . En este caso esos valores pueden ser   y  .

Los métodos de la clase  
Varios de los métodos de la clase Option son fáciles de entender. Otros requieren discutir conceptos importantes de Scala. Teniendo en cuenta que este post ya se "alargó", prefiero entonces dejar la discusión de esos métodos complicados para el siguiente post y pasar entonces a comentar a Some y None.

Puede observarse del código, que los métodos   en la clase   son abstractos. Su definición la brinda la clase   que hereda de   y el objeto   que también hereda de  .
En ambos casos el código es trivial y no es necesaria una explicación detallada.

Continúo en el siguiente post.

Imagen de bferro

Programación orientada a expresiones

Enrique (@ezamudio) escribe en el post que comienza este hilo:
Este tipo de cosas es lo que nos permite usar un estilo de programación más declarativo, en vez de imperativo.
El término que se usa para describir lo que Enrique menciona como programación declarativa es "programación orientada a expresiones" que es una característica de los lenguajes funcionales, donde lo que abunda son las expresiones que es algo que se evalúa y por tanto regresa un valor, y lo que escasea son las sentencias (statements) que es algo que se ejecuta y no regresa ningún valor.
Casi todos los bloques de control son expresiones en los lenguajes funcionales. Ya en algún momento discutimos sobre eso para las expresiones   en el lenguaje Scala y la No necesidad de disponer en Scala del operador ternario  que en Java y otros lenguajes es necesario para lograr lo que una expresión   logra.

Curiosamente, el estilo de programación imperativa ligado con expresiones en ocasiones provoca errores. En C por ejemplo, donde no existe un tipo boolean, la expresión de condición para un if puede ser cualquier expresión. Es común para los que empiezan con ese lenguaje escribir un operador de asignación en lugar de un operador == en la expresión que sirve como condición a un if. Algo así:
 
La asignación en C es una expresión que devuelve un valor, en este caso el valor 5 por lo que la condición siempre será verdadera.

A pesar de eso, como expresa @ezamudio en su post la orientación a expresiones ofrece algunas ventajas que deben tenerse en cuenta a la hora de expresar la computación que se desea realizar y es el estilo de la programación funcional. Aprovecharla vale la pena.