Scala: Clases "case" y pattern matching (1/2)

Las clases case (no encuentro un término adecuado en español) son uno de los mecanismos que Scala utiliza para hacer "pattern matching" sobre objetos, sin la necesidad de escribir el código de boilerplate que se requeriría si esta opción no existiera.
Pattern matching es una necesidad en la programación, y en el caso particular de la programación funcional es una técnica natural para resolver muchos problemas que hacen uso de datos recursivos con estructuras arbóreas.
Scala contribuye al pattern matching sobre objetos tratando de lograr cierta uniformidad de la sintaxis funcional y la sintaxis orientada a objetos.
Para describir las clases case y su utilización en el pattern matching necesitamos también hablar de otras cosas como las clases sealed (selladas) y del tipo Option y sus "derivados".
Utilizo aquí los ejemplos del texto de Odersky. Es un caso de estudio sencillo de comprender, y así no se pierde el tiempo. No veo necesario inventarme otros ejemplos.

Veamos primero las clases case

Supongamos que necesitamos escribir código para manipular expresiones aritméticas. Podemos requerir de eso si por ejemplo estamos diseñando un lenguaje específico de dominio (DSL). Queremos cumplir con el requisito mínimo de un DSL de ofrecer en el lenguaje una sintaxis y semántica similar a la sintaxis y semántica del lenguaje "natural" usado en ese dominio.
Nuestros datos de entrada en este caso de estudio son expresiones aritméticas y por tanto lo primero que hacemos es idear una solución para definir esas expresiones aritméticas.
Para propósitos del ejemplo restringimos el universo de las expresiones aritméticas a expresiones compuestas exclusivamente por variables, números y operaciones unarias y binarias. Una operación unaria está compuesta por un operador unario y un operando, mientras que una operación binaria tiene un operador binario y dos operandos.
Siempre es conveniente usar un caso de estudio de juguete cuando se quiere explicar otra cosa. Este es el caso aquí.

Uso de la herencia
Conviene aquí usar la herencia como una técnica buena para definir un concepto de manera incremental, partiendo de las "cosas" generales con menos detalle y seguir entonces con las especificidades y los detalles en las subclases o clases derivadas.
Escribimos entonces el código siguiente:
 

¿Qué hay de nuevo en el código anterior?
Pues el modificador case para las clases que heredan de 

¿Qué efectos tiene añadir el modificador case a una clase?
Son varias las cosas que obtenemos de "gratis" al usar ese modificador. Veamos esas cosas:

  • El compilador añade a la clase un objeto acompañante con el nombre de la clase y con un método de fábrica con los mismos argumentos que recibe el constructor primario de la clase. Lo hace para que podamos crear objetos sin hacer uso directo del operador  . Con esto nos podemos acercar a una sintaxis más natural del dominio.
    Hacer esto
     
    En lugar de esto:
     
  • Todos los argumentos en la lista de parámetros de una clase case obtienen el prefijo val, para poder entonces manipularlos como propiedades:
     
    En una clase "normal", los parámetros de la clase sin el prefijo val NO son propiedades de la clase. Sigue un ejemplo:
     
  • El compilador añade implementaciones "naturales" para los métodos  . La comparación por igualdad se realiza comparando estructuralmente los elementos de la clase. Recordemos que en Scala, el método   delega al método  .
     
  • El compilador añade a la clase un método   para obtener copias modificadas del objeto. Los parámetros del método copy tienen valores de default y también son nombrados. Esto facilita la copia.
    El código de   para BinOp:
     
    Y su uso:
     
  • Después de leer esas bondades de las clases case, uno se pregunta si esas "chuladas" son relevantes. Algunos dirán que sí, otros dirán que no, pero lo relevante no es eso sino la posibilidad de expresar de manera elegante el "pattern matching" en expresiones match y en funciones parciales.
    Pero eso lo dejo para la segunda parte de este post.

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

La segunda parte

La segunda parte la tengo lista mañana

Esta bastante difícil llegar

Esta bastante difícil llegar a una syntaxis que sea al mismo tiempo breve, comprensible y expresiva. Creo que Scala a pesar de lo difícil que parece al principio ( por la extramagia que le pone ) cumple bien el cometido de poder escribir de forma breve algo y que al mismo tiempo no parezca tan criptico. Claro, esta apreciación es totalmente subjetiva y habrá quién piense lo contrario.

Me gustaría saber si el concepto de "case class" esta basado en algo de la PF o si es un invento de Scala? La implementación me queda clara, es como lo que hacen los IDE's con sus macros, más bien me pregunto si eso será algún concepto de la PF

Scala en su naturaleza híbrida logra avanzar en el ámbito de tener clases para modelar el dominio y funciones para trabajar con él. Hay quién opina que este debería de ser llamado un nuevo paradigma de programación llamado Object Functional pero quizá sea demasiado.

Muy interesante el artículo, espero la siguiente entrega.

Solo una aclaración. Es muy común pensar que un DSL lo és solo por parecerse más al lenguaje usado en el dominio o a un lenguaje natural. En realidad no lo es. Un DSL es un lenguaje que pues precisamente solo sirve para un dominio en particular, pero poco o nada tiene que ver son su sintaxis. Un ejemplo es el   de   o el   de ant, son en realidad lenguajes pero que solo sirven para hacer esa tarea que hacen, aunque sean terriblemente difíciles de leer ( los basados en XML en particular ) . Lo contrario son los lenguajes de propósito general, que pueden hacer cualquier cosa. Lo menciono porque en el post se da esa impresión y no es poco común encontrar a varios programadores teniendo la misma impresión de que un DSL lo és porque parece lenguaje natural. El hecho de que Groovy, Ruby y Scala presenten ejemplos de como hacer DSL's cuya principal característica es hacer lo mismo que el lenguaje de propósito general pero sin puntos ( . ) ni parentesis tampoco ayuda mucho.

Una excelente presentación en este respecto la podemos encontrar en InfoQ muy bien explicada por Martin Fowler:

Saludos.

Imagen de bferro

Los DSL sí deben expresar la sintaxis y la semántica del dominio

El comentario de OscarRyz es muy pertinente.
Los lenguajes específicos de dominio, son lo que el término expresa: específicos del dominio, y para que eso se materialice tienen que atender el lenguaje que usan los especialistas en ese dominio. El lenguaje por tanto tiene que considerar la sintaxis y la semántica de las expresiones que se usan en ese dominio.
Cuando expreso que deben considerar el lenguaje "natural" del dominio, no me estoy refiriendo al lenguaje natural, el que usamos para comunicarnos los seres humanos. Me refiero al lenguaje que es natural en el dominio para escribir las soluciones a los problemas que se desean resolver.
MatLab por ejemplo usa un lenguaje específico de dominio para resolver problemas de matemáticas y otros afines, AutoCad hace los mismo para sus cosas y así podríamos mencionar otros dominios.
Por supuesto que hay dominios particulares para cosas particulares que queremos resolver en el espacio de solución (el espacio de la computadora). Es el caso que menciona OscarRyz cuando hace referencia a lenguajes declarativos usados para la construcción (build) de proyectos. En esos casos las herramientas de construcción ofrecen un lenguaje propio para escribir las "recetas" de construcción.

Con respecto a las clases case y la programación funcional. Sigo en mi otro comentario

Imagen de bferro

Las clases case facilitan el pattern matching

Otra vez, lo que comenta OscarRyz preguntando si las clases case tienen su fundamento en la programación funcional es también pertinente.
Lo que tiene su fundamento en la programación funcional es el pattern matching y las clases case tratan de lograr que el pattern matching sobre objetos se pueda realizar sin mucha dificultad.
Es importante también su uso para escribir funciones parciales que aceptan como argumentos objetos instancias de las clases case.
Sobre eso voy a escribir en la segunda parte de este post

Imagen de ezamudio

Muy bueno

Ya nomás faltó un ejemplillo de cómo se usan las case classes en el pattern matching, que es cuando ya todo mundo dice "aaaaaaaaah POR EEEESO". Sobre todo para pasar mensajes a actores, pero también para otros casos de pattern matching un poco más complicados. Alguna vez vi un ejemplo para un patrón de números pares y nones, pero no estoy seguro si era una case class o una clase normal, por lo de que hay que implementar el método  .

Espero la siguiente parte, porque eso de las funciones parciales es algo que todavía no termino de entender.

Imagen de bferro

@ezamudio. Los ejemplos llegan hoy

Espero poder escribir hoy la segunda parte. Las clases   son buenas, pero no serían tan buenas si no es ocuparan para otras cosas.
Esa otra cosa es el pattern matching, técnica obligada en la programación funcional.
Vendrán algunos ejemplos de funciones parciales, que por lo pronto es conveniente recordar que son aquellas funciones que están definidas solamente para "algunos"valores del dominio y no para todos. Cuando tenemos una función que asocia un valor del codominio para cada valor del dominio tenemos entonces una función total.

Imagen de bferro

Scala: Clases case y pattern matching (2/X)

Aquí la segunda parte de este post, donde presentamos el uso de las clases case para hacer pattern matching sobre objetos. Habrán más partes. Ya no me alcanza con dos.
Seguimos el ejemplo de las expresiones aritméticas. Veamos como podemos usar las técnicas de clases case y pattern matching para simplificar expresiones aritméticas.
Consideremos tres reglas de simplificación:

  • Simplificar la negación doble:
     
  • Simplificar la suma con 0:
     
  • Simplificar la multiplicación por la unidad
     

Las propias expresiones para estas tres reglas podrían ser usadas por un algoritmo que determine si una expresión de entrada concreta, por ejemplo   se corresponde (hace match) con la parte izquierda de algunas de las reglas de producción anteriores. Podemos pensar que los antecedentes de esas reglas son expresiones "regulares" que pueden contrastarse con esa expresión concreta para determinar si esta última satisface la "expresión regular".
Definamos entonces una función parcial para realizar ese pattern matching y un ejemplo simple para hacer uso de ella:
 

Al ejecutar el programa el resultado es:
 

Como comentamos con anterioridad, una función parcial está definida para un conjunto limitado de los elementos del dominio, que se asocian con un elemento del codominio. En el ejemplo, los elementos del dominio aceptados son aquellas expresiones aritméticas de tipo  . Para el resto de las expresiones, la función no está definida.
  está definido con un trait y un objeto acompañante:
 
Es una función unaria (function1) donde el dominio no incluye necesariamente todos los valores de tipo A. Como toda función, es contravariante en el tipo del argumento y covariante en el tipo de resultado.
Para ella se definen varios métodos útiles para la composición de funciones y por supuesto el método:
 
que devuelve verdadero si la función está definida para ese argumento o falsa si no está definida:
 
El resultado sería  , mientras que:
 
El resultado sería  
Con los métodos de PartialFunction podemos resolver problemas interesantes de composición de objetos y de pattern matching. Pero eso se merece otro post.
Podríamos escribir un código similar definiendo un método que use una expresión match:
 
Usamos en este caso el patrón wildcard (_) que hace match con cualquier expresión, pero que no introduce un nombre de variable para poder referirnos a ese valor en la parte derecha de la regla de producción. Esa regla cubre entonces todos los valores que antes se excluían del dominio y deja de ser parcial.
  es un método y no una función pero puede usarse en el lugar que se espera una función gracias a que el compilador realiza la coerción necesaria. Podemos por supuesto hacerlo nosotros cuando lo necesitamos.
To be continued .......

Imagen de bferro

Matching objects with Patterns

Para los que quieran profundizar en el pattern matching con objetos el artículo Matching Objects with Patterns está muy bueno.

Imagen de bferro

¿Se acuerdan del post "Clases Case y Pattern Matching?

Este tópico quedó en cierto sentido incompleto. Para completarlo, es necesario continuar con los tipos de pattern matching que Scala soporta, y también hablar de los extractores que generalizan la descomposición y análisis de datos usando pattern matching.
Sigo con el ejemplo de las expresiones aritméticas para describir los tipos de pattern matching. Uso el término en inglés pues los términos en español "apareamiento o casamiento de patrones" no me checan. Cuesta trabajo imaginarse a las estructuras de datos apareándose.

Los tipos de patrones en Scala

Son varios los tipos de patrones que podemos usar en Scala. Una clasificación posible es la siguiente:

  • Patrones wildcard
  • Patrones constantes
  • Patrones variables
  • Patrones constructores
  • Patrones de secuencia
  • Patrones de tuplas
  • Patrones tipados

La anterior no es la única clasificación que se puede hacer,pero es una conveniente para discutir esta técnica.
Recordemos que cuando hacemos pattern matching utilizamos la expresión match. La sintaxis de esta expresión es:
 

Recordemos cosas importantes de match

La expresión match es similar a la sentencia switch de muchos lenguajes de programación (Java entre ellos), pero con algunas diferencias:

  • match es una expresión y no una sentencia: devuelve un valor. Esto es importante para dar soporte al estilo objeto funcional de Scala. Conviene repetir lo que ya hemos dicho en otras ocasiones: En un lenguaje funcional, todas las computaciones se realizan mediante la evaluación de expresiones que producen un valor. Las expresiones son términos sintácticos y los valores que ellas producen al ser evaluadas son entidades abstractas que tomamos como respuestas.
  • Las expresiones alternativas de una expresión match nunca "caen" en el siguiente caso. No hay necesidad entonces de escribir sentencias "break" después de cada alternativa.
  • Si ninguno de los patrones de las alternativas que componen la expresión match se satisface, se dispara una excepción de tipo  .
    Cuando creamos una función parcial compuesta por una secuencia de expresiones  , podemos usar el método   de las funciones parciales como un mecanismo de programación defensiva para evitar el disparo de la excepción.

La secuencia de alternativas de match se escriben como reglas de producción que comienzan con la palabra reservada case:
 
El símbolo => es también una palabra reservada del lenguaje.
Evaluamos la expresión match evaluando cada alternativa en el orden que aparecen. si el patrón en la la regla hace match con el selector de la expresión, se evalúa entonces la expresión del lado derecho de la regla y su valor es devuelto por la expresión match. Repetimos uno de los ejemplos,donde es evidente que la primera regla se satisface.
 

Los patrones wildcard

El patrón wildcard representado por el guión bajo (_) hace match con cualquier valor (objeto). Es útil para expresar el caso de default cuando el resto de las reglas no cubren todos los valores posibles que puede tener el selector de la expresión match. Es una mala práctica aunque útil para propósitos de scripting y depuración cuando trabajamos con instancias de clases case como selectores de una expresión match, y queremos asegurarnos que se incluyen todos los casos posibles, aunque esos casos no han terminado de "programarse".
A veces no podemos hacer eso, debido a que no tenemos un comportamiento de default definido para la expresión match. Necesitamos asegurarnos que las alternativas cubren todos los casos posibles del valor del selector. Las clases selladas (sealed) tienen ese objetivo.
Los patrones wildcard pueden usarse para ignorar partes de un objeto. Por ejemplo, si el selector de la siguiente expresión se asocia con un valor de tipo  , podemos entonces determinar si se trata de una expresión binaria con:
 
¿Qué sucedería en el siguiente código?
 

Los patrones constantes

Un patrón constante hace solamente match consigo mismo. Como constantes en el patrón podemos usar cualquier literal, cualquier val y cualquier objeto singleton. El siguiente ejemplo muestra el uso de algunos patrones constantes:
 
El ejemplo muestra el uso de literales y de dos objetos: Solitario, definido en el ejemplo y Nil, parte de Scala.
Si ejecutamos:
 
Observa que Solitario está definido como un case object, de manera que obtenemos de "gratis" su representación textual sin necesidad de definir el método  . El compilador lo hace por nosotros.
Siempre suceden cosas "extrañas". Se me ocurrió escribir el siguiente código,que compila sin problemas:
 

Y después de escribirlo y entrarme algunas dudas sobre List() y Nil para indicar listas vacías, lo reescribí de la siguiente forma:
 

Y entonces ese código no compiló indicando la línea   como "unreachable code".
Puedes echarle un vistazo a algunas opiniones sobre eso en StackOverFlow.

Patrones variables

Un patrón variable hace match con cualquier objeto o valor. Entonces, ¿cuál es la diferencia con el patrón wildcard que también hace match con cualquier objeto?
La diferencia es importante: Scala hace binding de la variable con lo que sea el objeto y en la parte derecha de la regla podemos hacer uso de ella para "interactuar" con el objeto.
El método  del ejemplo de expresiones aritméticas que estamos usando ilustra esa asociación entre el patrón variable y la parte correspondiente del selector:
 
En el código se desea simplificar expresiones binarias convirtiendo   en   y convirtiendo   en  . Se simplifica también un expresión unaria de tipo   en  .
La variable   del patrón variable se asocia con la parte correspondiente de la representación del objeto.
Sigue otro ejemplo, que por supuesto jamás escribiríamos para obtener el principio y el resto de una lista, pero que ilustra los patrones variables:
 
Si cambiamos el patrón del primer case del ejemplo anterior por  , teniendo ahora el siguiente código:
 
El resultado es el mismo. Conviene saber la diferencia entre   y  . Lo dejo de tarea.

Nota: El método   en el objeto   está "deprecated", pero es irrelevante en estos ejemplos.

¿Cómo identificar los patrones variables de los patrones constantes?

Scala necesita una regla léxica para distinguir las constantes o valores de las variables en los patrones variables y los patrones constantes. El siguiente código no compila:
 
El eror se anuncia como:
 
Si ahora escribimos:
 
el código compila y se obtiene el resultado:
 
La regla léxica entonces es: utilizar nombres que comiencen con una letra minúscula para patrones variables y nombres que comiencen con mayúscula para patrones variables.
El libro de Odersky ilustra esa regla con los valores Pi y E definidos en el objeto paquete scala.math
 
¿Cuál es el resultado?
OOPS, ¿Qué es un objeto paquete (package object)? Lo dejo de tarea.

¿Qué sucede si escribimos los siguiente?
 
¿Cual sería el resultado?

Apenas he descrito los patrones variables y los patrones constantes y ya se hizo muy largo este post. Mejor le paro aquí y sigo con este rollo en otra entrada.

Imagen de bferro

Continuación de clases case y pattern matching

Ayer escribí esto para los interesados. Pienso platicar de pattern matching en Scala en el próximo open talks.

Imagen de ezamudio

Eso!

Qué bien, me parece un buen tema para las opentalks