viernes, 30 de enero de 2009

Lecciones funcionales para imperativos (2): Inmutabilidad

Se puede objetar que la definición de funciones puras en un lenguaje orientado a objetos sólo aplicaría a métodos static, de los que ponemos en clases de "utilitarios". Y los static no son nada representativos de lo que es el paradigma de objetos. Esta observación parece indicar que el paradigma funcional no se lleva bien con el de objetos, y que cualquier intento de fusión sería forzado y torpe.
Las clases más elementales de una aplicación típicamente definen getters y setters: el clásico "JavaBean". Los getters no son puros porque pueden retornar algo diferente cada vez. Los setters producen efectos colaterales al cambiar el estado del objeto.
Habíamos dicho que las funciones puras arrojan un resultado que depende solamente de los parámetros. Como los métodos de instancia se ejecutan en el contexto de un objeto, siempre tienen disponible su referencia, y podemos entender a esa referencia como un parámetro implícito. ¿Por qué, entonces, si un simple getter sólo depende del estado del parámetro implícito (el objeto al que pertenece), no tiene transparencia referencial?
El motivo es que la mutabilidad del objeto conspira contra este fin. Para que un getter tenga transparencia referencial, es necesario que el atributo retornado no tenga posibilidad de cambiar: que su valor inicial permanezca durante la vida del objeto. Por ende no debería existir el correspondiente setter:


class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() { return this.name; }
public int getAge() { return this.age; }
}


De hecho, en la programación funcional pura no existen las variables. Todo valor es inmutable. En Java es fácil crear un primitivo inmutable, para eso utilizamos la palabra clave final. Pero para hacer inmutable un objeto, eso no alcanza: final garantiza que la referencia no apuntará a otro objeto, pero no impide que mute el estado del objeto referenciado. Para lograr esto tenemos que seguir el patrón del ejemplo de arriba.
Para quien haya programado siempre en lenguajes imperativos, la idea de trabajar sin mutabilidad puede parecer un sinsentido. Pero pensemos en algo de uso corriente: la clase java.lang.String es inmutable. En Java no podemos modificar un objeto String, y sin embargo podemos utilizar esta clase sin problemas.


String s = " test ";
s.trim();


En este ejemplo vemos un ingenuo intento de modificar la variable s aplicando el método trim(). Los que alguna vez cometimos este error, aprendimos que los métodos de String retornan una referencia a un nuevo objeto, el resultado del método. Si no guardamos la referencia, de nada sirvió invocar al método. Java no nos advierte de esta situación porque sus diseñadores siguieron el criterio de C y C++: se considera válido invocar a un método y descartar su resultado, porque se asume que se lo quiso llamar como procedimiento, y que sólo nos interesan los efectos colaterales.
Otro ejemplo interesante de uso intensivo de inmutabilidad es la librería Joda Time.
Entonces, resumiendo, lo que normalmente hacemos con mutabilidad, lo podemos lograr invocando funciones que producen un nuevo resultado que consiste en una transformación del valor original.

Un objeto inmutable es, por definición, thread-safe. La inmutabilidad nos da thread-safety en un sentido absoluto, sin depender de un mecanismo obtuso como el de la "sincronización".

Volviendo al ejemplo de la clase Person, ¿cómo definiríamos un método equivalente a modificar la edad de una persona?


public Person withAge(int newAge) { return new Person(this.name, newAge); }


Nada más simple.
El objetivo no es aplicar el paradigma funcional indiscriminadamente en un lenguaje como Java, porque no está en su naturaleza. Pero sí se puede tomar conceptos útiles y aplicarlos hasta donde sea razonable. Por ejemplo, podemos hacer una función con transparencia referencial cuya implementación usa variables:


public int sumChars(String s) {
int a = 0;
for(int i = 0; i < s.length(); i++)
{
a += s.charAt(i);
}
return a;
}


En un caso así no hay efectos colaterales, toda la mutabilidad está manejada en variables locales.

jueves, 29 de enero de 2009

Un cuestionable legado de C / C++

Muchos lenguajes diferencian los conceptos de procedimiento y función, pero los creadores de C decidieron unificarlos, supongo que en un intento de simplificación, llamándolos indistintamente funciones. El procedimiento pasó a ser una función que no retorna ningún resultado (o lo que es lo mismo, con tipo de retorno void). La unificación va más allá, en tanto que C permite invocar cualquier función como si no retornara un resultado. Es decir, cuando sólo interesa lo que la función hace y no lo que retorna, se invoca a la función como procedimiento y el resultado es descartado automáticamente. Quien haya intentado esto con otros lenguajes, como BASIC, se habrá visto obligado a guardar explícitamente un resultado irrelevante, y probablemente considere feliz la iniciativa de C.

C++ mantuvo el criterio de C, aunque empezamos a hablar de funciones miembro, o métodos. Más tarde llegaron Java y C#, donde seguimos hablando de métodos. Todos reconocemos que detrás del concepto de método subyacen los de procedimiento y función, pero es como si nos hubiésemos puesto de acuerdo en mantener medio difusa esa distinción.

Idealmente, a una función hay que llamarla por el resultado que arroja y no por lo que hace. La llamada a una función debe poder ocupar el lugar de un valor, una función es un valor. Por otro lado, a los procedimientos se los llama por lo que hacen. La propuesta es: escribir métodos que son claramente una cosa o la otra, manteniendo los efectos colaterales bajo control. 

Lecciones funcionales para imperativos (1): Transparencia referencial

La transparencia referencial es una propiedad de las funciones. Aplica a toda función matemática, y las de los lenguajes de programación funcional pura. Sin embargo no es una noción ajena a la programación imperativa u orientada a objetos: podemos definir métodos como funciones con transparencia referencial, o "funciones puras". Esta es, quizás, la lección más básica que podemos tomar de la programación funcional, para aplicar en programación imperativa.

Una función tiene transparencia referencial si su resultado depende única y exclusivamente de los parámetros provistos, y no produce efectos colaterales. La implicancia de esto es que, dado un juego de parámetros, no importa cuántas veces llamemos a la función ni desde dónde, el resultado será siempre el mismo.

¿De qué otras cosas podría depender una función que de sus parámetros? De la fecha y hora del sistema, de globales, de valores aleatorios o ingresados por el usuario. En un lenguaje OO, un método también puede depender de atributos mutables, y de métodos sin transparencia referencial, del mismo objeto o de otros. Recordemos además que los singletons y los elementos static son globales.


// Funciones con transparencia referencial

public String capitalize(String x) {
return x.substring(0,1).toUpperCase() + x.substring(1);
}
public int foo(int a, int b) {
int r;
if(a % 2 == 0)
r = b + a * 2;
else
r = b * 5;
return r;
}
public float pi() {
return 3.14;
}


Efectos colaterales

Los efectos colaterales son cualquier cambio en el estado del sistema, como podría ser: modificar globales u otros elementos, y operaciones de salida a dispositivos, incluyendo la pantalla. Cualquier cosa que cambie en el "mundo exterior" a la función es un efecto colateral: una función con transparencia referencial no genera otra novedad en el mundo que el cómputo de su resultado.
Ejemplos de efectos colaterales:


public Dog createDog(List<animal> animals) {
Dog dog = new Dog();
animals.add(dog); // altera una estructura que existe fuera de la función
return dog;
}

Este método produce un efecto colateral al mutar el estado del objeto recibido por parámetro.


public int foo(int a) {
this.someAttr += a; // altera un atributo del mismo objeto
return a > 0? a * a : a;
}

Este otro método es determinístico en su resultado, pero al mutar un atributo, el estado del objeto no será igual cuando se llame al método varias veces con el mismo parámetro.


public int sum(int a, int b) {
System.out.println("sum"); // genera salida por pantalla
return a + b;
}

Este caso parece particularmente inocuo. Es como que para el programa da igual si se imprimió "sum" o no. Sin embargo, consideremos los siguientes programas:


// Programa #1
int x = sum(1, 1);
System.out.println(x);


// Programa #2
int x = sum(1, 1);
x = sum(1, 1);
System.out.println(x);


El resultado de ejecutar uno y el otro es diferente, no es lo mismo imprimir algo una vez que dos veces. La pantalla es parte del "mundo exterior" sobre el que actuó la función.
La propuesta de hacer funciones sin efectos colaterales puede sonar ridícula, y de hecho no puede haber un programa útil sin efectos colaterales. La idea es que estos efectos estén controlados. Cuando hacemos una función, conviene plantear si los efectos colaterales son necesarios, y si no lo son, quizás sea candidata a tener transparencia referencial.
Si determinamos que cierto efecto colateral es necesario, igualmente conviene en lo posible cumplir con el otro aspecto de la transparencia referencial: que el resultado dependa sólo de los parámetros.

La transparencia referencial ayuda a que el código sea más claro y testeable.

Los efectos colaterales tienen mayor sentido en los procedimientos. A los procedimientos se los llama para hacer algo, para cambiar el estado del sistema, para producir un efecto; en cambio una función es un valor.
Los programas en lenguajes de programación funcional consisten en una composición de funciones. En lenguajes imperativos, podemos tener módulos basados en composición de funciones puras, aunque el programa sea impuro. Todo lo que se pueda acotar con una implementación pura será más mantenible y menos proclive a bugs.

martes, 27 de enero de 2009

Qué cuernos es Scala y por qué esa obsesión

Ya lo dije en otras partes pero me voy a tomar un post acá para resumirlo. Scala es un lenguaje de incipiente adopción, señalado a veces como "el próximo Java" o como uno de sus herederos. Aparentemente Java llegó a un techo en su evolución, por cuestiones intrínsecas (su diseño) y extrínsecas (determinación de Sun). Una forma elegante de evadir completamente este problema es pasándose a otro lenguaje mejor diseñado, sin cambiar de plataforma. Scala es uno de esos. Incorpora bastante más que lo que algunos están reclamando como torpe mejora en Java.
Algunas características:

  • Compila a bytecode (JVM)
  • Es interoperable con Java (o sea, se pueden seguir usando librerías y frameworks existentes en Java)
  • Es de tipo estático
  • Inferencia de tipos
  • Fusiona exitosamente programación funcional con OOP
  • Tiene una forma de herencia múltiple llamada composición mixin
  • "Conversiones implícitas": permiten extender librerías externas y hacer una especie de prototyping
  • Incluye un intérprete ("REPL")
  • Soporta XML como un tipo más
  • Viene con librerías interesantísimas: concurrencia tipo Erlang, parser combinators, etc.
  • ¿Qué más querés?
El sistema de tipos tiene cierta complejidad, que domino a medias, pero con eso en general alcanza. Más allá de todas las ideas incorporadas, tomadas de varios lenguajes e integradas por Martin Odersky en Scala, el reto mayor para quien viene de Java u otro lenguaje imperativo es asimilar el estilo de programación funcional

Scala permite codificar en estilo imperativo, como si fuera un Java con diferente sintaxis. Eso puede estar bueno para ir metiéndose de a poco, pero de nada servirá si uno no busca avanzar a un código más funcional. El estilo funcional lamentablemente estuvo siempre muy divorciado de la industria y muy casado con la academia, pero esto ya está cambiando. Está presente en Python, Ruby, JavaScript; entró de lleno en .NET con C#/LINQ y F#, etc., y es probable que esta tendencia se profundice, aunque haya que terminar desechando Java por completo.

Se dice por ahí que "aprender programación funcional nos convierte en mejores programadores aunque nos dediquemos a la programación imperativa". Hay dos o tres cosas que me gustaría rescatar en ese sentido, para un próximo post, aplicables a cualquier lenguaje. Son cosas bien sencillas que se nos escaparon por mucho tiempo, y pueden ser un buen punto de entrada a los beneficios del paradigma funcional.
¡Salud!

Hola Mundo

A ver...

object MyApp {
def main(s:Array[String]) = println("Hello World")
}

Buenísimo, encontré cómo formatear código Scala para Blogger. Ahora vamos a ver qué puede salir de esto.

UPDATE: me pasé a la versión 2.0 de SyntaxHighlighter, ya viene con soporte de Scala.