Arquitecturas Concurrentes, Episodio 7: Just call me Maybe y otras estructuras

flbulgarelli
Arquitecturas Concurrentes
6 min readNov 21, 2015

En los episodios anteriores surgieron los conceptos de falla, no determinismo y excepción. También vimos dos estructuras que nos permitían abstraernos sobre CPS: promesas y streams observables. Y por último, vieron la luz palabras como functores y mónadas. Nuestra propuesta para entender mejor estas ideas es encararlas desde otro ángulo: el paradigma funcional.

Par empezar, en este episodio, presentaremos algunos aspectos que nos interesan de Haskell, lenguaje funcional por antonomasia, y algunas estructuras funcionales fundamentales que nos acompañarán en nuestro recorrido por las arquitecturas concurrentes.

¿Paradigma Funcional? ¿Qué es eso?

Es irónico que aunque el paradigma funcional es muy anterior al paradigma de objetos, lo que le ha dado la posibilidad de construir sólidas bases, es difícil dar una definición del mismo. Por ejemplo, la definición más obvia reza

funcional es un paradigma en el que las soluciones a los problemas se estructuran en términos de aplicación de funciones

y si bien es correcta, hay tantos elementos fundamentales que se desprenden de ésta y que no son evidentes que resulta de poca utilidad.

Quizás sea más útil pensarlo a partir de las características más frecuentemente evocadas cuando se piensa en éste:

  • Pureza: las funciones, al igual que en matemática, no presentan efectos colaterales, sino que tan sólo reciben, operan y devuelven valores
  • Evaluación diferida: ciertas partes del código no se evaluarán salvo que sea necesario
  • Funciones de primer orden: las funciones son valores, y por tanto pueden ser pasadas por parámetro
  • Pattern matching: los valores pueden ser descompuestos estructuralmente, en un proceso inverso a la construcción: la deconstrucción. Y además podemos usar a esta herramienta como mecanismo de control de flujo: según encaje un valor con un patrón u otro, podremos tomar acciones diferente.
  • Expresiones lambda: Es posible escribir valores de función de forma literal, sin asignarle un nombre.
  • Inmutabilidad: las variables son meras etiquetas, que una vez unificadas contra un valor, no pueden ser cambiadas

Sin embargo, ¿son las anteriores ideas propias del paradigma funcional? Miremos más en detalle:

  • No todos los lenguajes funcionales son realmente puros. LISP y sus derivados, por ejemplo, no lo son normalmente: permiten hacer input-ouput (IO) imperativo, modificar variables, etc.
  • No todos los lenguajes funcionales presentan evaluación diferida. Para ser justos, ni siquiera Haskell: éste ofrece evaluación no-estricta, lo cual es ligeramente diferente.
  • Por un lado muchos lenguajes (funcionales o no) presentan algún tipo de operación de deconstrucción: Ruby, ECMAScript6, Clojure, etc, que es la base para implementar pattern-matching. Y por otro lado, la idea de Pattern matching, no figura en Calculo Lambda, la base teórica de funcional.
  • Virtualmente todos los lenguajes modernos presentan lambdas, closures o bloques de código, que permiten cosificar una porción de código.

Si nada de lo que parece tan propio de funcional es realmente exclusivo del mismo, entonces, volvemos a la pregunta: ¿qué es eso? Simple: es la forma particular en que combinamos estas herramientas, razonando declarativamente en términos de valores y transformaciones sobre los mismos.

Nuevamente, el todo es más que la suma de las partes.

Haskell en 2 minutos

Y ahora nos toca describir a Haskell, probablemente el lenguaje funcional más conocido y mejor diseñado que exista.

Tipado

Presenta tipado “fuerte”

  • estático: una gran cantidad de errores se validan mediante el sistema de tipos antes de la ejecución del programa
  • inferido: en la mayoría de los casos podemos evitar declarar el tipo de las cosas, dejando que el sistema de tipos lo determine por nosotros.

Pureza

Además, es un lenguaje puro. Tanto, que hasta los efectos están modelados como valores de tipo IO, que representa un efecto, el cual puede ser operado como cualquier otro valor: podemos pasar efectos por parámetros, colocarlos en listas, ordenarlos, etc.

De hecho, un programa ejecutable es una función que devuelve un valor de tipo IO. El runtime de Haskell ejecuta el efecto representado por este valor, produciendo así los efectos en el mundo real deseados.

Moraleja: un programa Haskell no tiene efectos, pero es capaz de devolver un valor que los representa, pudiendo así hacer todo lo que un programa imperativo podría hacer, y más.

Simplicidad

La sintaxis e ideas fundamentales de Haskell son realmente simples, y el resto de las ideas más complejas se construyen normalmente sobre las más simples.

Estructuras funcionales

Ahora que recordamos Haskell, vamos a lo que nos interesa: las estructuras (de datos) funcionales clásicas del paradigma.

No tienen nada de extraño, y a las mismas las podríamos haber aprendido en C o Pascal. Pero son particularmente fácil de expresar y utilizar en un lenguaje funcional, dado que además de contar con registros (léase, los structs de C) , contamos con tipos algebraicos de datos (AGDT: algebraic data type), que son una variante mejorada de los tipos unión (unions de C)

Dualidades

En realidad sí hay algo novedoso en esto de las estructuras funcionales: cada una de ellas presenta una dualidad, pudiendo ser ser pensada tanto como una estructura de datos, como una estructura de control. Dicho de otra forma, a las estructuras funcionales podemos verlas tanto como contenedores (cajas que almacenan valores) como computaciones (operaciones que al ejecutarlas producen valores).

¡Vayamos a lo concreto! Conozcamos nuestra primera estructura: la lista.

La lista

A la lista ya la conocemos bien: como contenedor representa un conjunto de valores, ordenado y que admite repetidos. La definición funcional de lista es particularmente simple: una lista es o bien una lista vacía, o bien una lista con un elemento (cabeza) y una sublista (cola).

En Haskell la lista ya viene definida con una sintaxis especial, pero podríamos haberla definido de la siguiente manera:

En la que se ven claramente los dos casos que mencionamos antes: lista vacía (Nil) ó (|) lista llena (Cons)

Y podemos usarla así:

laLista123 = Cons 1 (Cons 2 (Cons 3 Nil))

O usando las listas nativas de Haskell:

laLista123 = 1 : 2 : 3 : []

Que por conveniencia escribimos así:

laLista123 = [1, 2, 3]

¿Y si pensamos a la lista como computación? Diremos que una función que devuelve una lista representa representa una computación no determinística. Por ejemplo, si tenemos una computación no deterministica, que produce a un bastardo sin gloria:

bastardoSinGloria(omar).
bastardoSinGloria(oso).
bastardoSinGlora(raine).

podríamos escribirla así:

Maybe

Nuestra siguiente estructura es Maybe, también conocida como Option u Optional:

¿Qué significa esto? Un Maybe representa algo que puede estar (Just), o no (Nothing). Es decir, si lo pensamos como un contenedor, Maybe es una caja que puede contener cero o un elemento.

Y si lo pensamos como como una computación, una función que devuelva Maybe representa una computación que puede o no arrojar un resultado, es decir, una computación que puede fallar.

Ejemplo:

Either

Como vemos un Either es un tipo de dato que puede presentar valores de dos tipos, y uno y sólo por vez. Se comporta como una caja con dos compartimentos, pero usar uno inhabilita el otro y viceversa.

Entonces, como contenedor es bastante obvio: es aquel permite almacenar uno de dos valores. Y como computación, una función que devuelva Either representa a una computación que puede ser exitosa y entregar un resultado, o bien puede ocurrir un error y arrojar una excepción.

Por convención, el Left es el de error. Regla mnemotécnica: Right también significa correcto en inglés.

Estamos de acuerdo que implementar excepciones mediante Either no parece aún mas incómodo que utilizar excepciones chequeadas en Java: tenemos que en cada función que pueda fallar, declarar el tipo de la excepción. Pero recordemos que Haskell es un lenguaje con inferencia de tipos, así que esto debería perjudicarnos en menor medida.

Pero también perdimos la propagación automática de errores, que es lo que vuelva a la excepciones valiosas. Ya la recuperaremos cuando veamos functores, mónadas y functores aplicativos.

En Haskell hay muchas forms de manejar errores y excepciones. Either es sólo una de ellas

Para leer más

¿Y ahora qué sigue? Aprender sobre functores, functores aplicativos y mónadas. Sin embargo, el próximo episodio viene en formato de guía Mumuki:

Y después, cuando termines, para profundizar te dejamos algunos links interesantes:

y una excelente guía gráfica:

¡Nos vemos!

--

--