Pensamiento funcional: Pensando funcionalmente, Parte 1

Aprendiendo a pensar como un programador funcional

La programación funcional ha generado un reciente aumento del interés, argumentando menores errores y mayor productividad. Pero muchos desarrolladores han intentado pero no han logrado entender qué hace a los lenguajes funcionales convincentes para algunos tipos de trabajos. Aprender la sintaxis de un nuevo lenguaje es fácil, pero aprender a pensar de forma diferente es difícil. En la primera entrega de su serie de columnas Pensamiento funcional Neal Ford presenta algunos conceptos de programación funcional y habla sobre cómo usarlos en Java y en ™ Groovy.

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.

Photo of Neal FordNeal Ford es arquitecto de software y Meme Wrangler en ThoughtWorks, una consultora global en TI. Él también diseña y desarrolla aplicaciones, material instructivo, artículos para revistas, courseware y presentaciones en video/DVD, y es autor o editor de libros que abarcan una variedad de tecnologías, incluyendo el más reciente The Productive Programmer. Él se enfoca en diseñar y crear aplicaciones corporativas a gran escala. También es un orador aclamado internacionalmente en conferencias de desarrolladores en todo el mundo. Visite su sitio Web.



18-07-2011

Sobre esta serie

Esta serie está dirigida a re-orientar su perspectiva hacia un pensamiento funcional, ayudándole a observar los problemas comunes de nuevas formas y a encontrar nuevas formas para mejorar su codificación del día a día. Explora los conceptos de programación funcional, estructuras que permiten la programación funcional dentro del lenguaje Java, lenguajes de programación funcional que se ejecutan en la JVM y algunas indicaciones para el aprendizaje futuro del diseño del lenguaje. Esta serie está articulada para desarrolladores que conocen Java y cómo funcionan sus abstracciones, pero que tienen poca o ninguna experiencia usando un lenguaje funcional.

Digamos por un momento que usted es un leñador. Usted tiene la mejor hacha del bosque, lo que le hace el leñador más productivo del campo. Luego, un día se presenta alguien y ensalza las virtudes de un nuevo paradigma para el corte de árboles, lamotosierra. El vendedor es convincente, así que usted compra una motosierra, pero no sabe cómo funciona. Usted intenta sopesándola y blandiéndola contra el árbol con gran fuerza, que es cómo funcionan sus otros paradigmas de corte de árboles. Rápidamente usted concluye que esta ultra-moderna motosierra es simplemente una moda pasajera, y vuelve a derribar árboles con su hacha. Luego, alguien viene y le muestra cómo dar arranque a la motosierra.

Probablemente usted puede relacionarse con esta historia, pero con programación funcional en lugar de una motosierra. El problema de un paradigma de programación completamente nuevo no es aprender el nuevo lenguaje. Después de todo, la sintaxis del lenguaje son es apenas los detalles. La parte engañosa es aprender a pensar de manera diferente. Allí es donde yo entro — quien da arranque a la motosierra y programador funcional.

Bienvenido al Pensamiento funcional. Esta serie explora el objeto de la programación funcional pero no sólo trata sobre lenguajes de programación funcional. Como ilustraré, escribir código de una manera "funcional" tiene que ver con diseño, compensaciones, diferentes bloques de creación reutilizables y gran cantidad de otras perspectivas. Hasta donde sea posible, trataré de mostrar conceptos de programación funcional en Java (o lenguajes cercanos a Java) y pasaré a otros lenguajes para demostrar capacidades que todavía no existen en Java. No abandonaré el fondo para hablar sobre cosas que están de moda como las monads (vea en Recursos) de una vez (aunque llegaremos a ello). En cambio, le mostraré gradualmente una nueva forma de pensar los problemas (que usted ya está aplicando en algunos lugares — sólo que todavía no se ha dado cuenta).

Esta entrega y la siguiente sirven como un tour rápido por algunos temas relacionados con la programación funcional, incluyendo conceptos esenciales. Algunos de estos conceptos se volverán a ver con más detalle a medida que construya más contexto y dé más matices durante la serie. Como punto de partida para el tour, le voy a presentar dos implementaciones diferentes de un problema, una escrita imperativamente y la otra con una inclinación más intelectual.

Clasificador de números

Para hablar sobre diferentes estilos de programación, usted debe tener código para comparación. Mi primer ejemplo es una variación de un problema de codificación que aparece en mi libro The Productive Programmer (vea en Recursos) y en "Test-driven Design, Part 1" y "Test-driven design, Part 2" (dos entregas de mi serie anterior en developerWorks Evolutionary architecture and emergent design). Escogí este código, al menos parcialmente, porque esos dos artículos describen el diseño del código en profundidad. No hay nada malo en el código ensalzado en esos artículos, pero aquí voy a proporcionar razones para un diseño diferente.

Los requisitos establecen que, dado un entero positivo mayor que 1, usted debe clasificarlo o como perfecto, abundante, o deficiente. Un número perfecto es un número donde la suma de sus factores (excluyendo al número mismo como factor) dan como resultado dicho número. De manera similar, la suma de los factores de un número abundante es mayor que el número, y la suma de los factores de un número deficiente es menor.

Clasificador de números imperativo

Una clase imperativa que cumple estos requisitos aparece en el Listado 1:

Listado 1. NumberClassifier, la solución imperativa al problema
public class Classifier6 {
    private Set<Integer> _factors;
    private int _number;

    public Classifier6(int number) {
        if (number < 1)
            throw new InvalidNumberException(
            "Can't classify negative numbers");
        _number = number;
        _factors = new HashSet<Integer>>();
        _factors.add(1);
        _factors.add(_number);
    }

    private boolean isFactor(int factor) {
        return _number % factor == 0;
    }

    public Set<Integer> getFactors() {
        return _factors;
    }

    private void calculateFactors() {
        for (int i = 1; i <= sqrt(_number) + 1; i++)
            if (isFactor(i))

                addFactor(i);
    }

    private void addFactor(int factor) {
        _factors.add(factor);
        _factors.add(_number / factor);
    }

    private int sumOfFactors() {
        calculateFactors();
        int sum = 0;
        for (int i : _factors)
            sum += i;
        return sum;
    }

    public boolean isPerfect() {
        return sumOfFactors() - _number == _number;
    }

    public boolean isAbundant() {
        return sumOfFactors() - _number > _number;
    }

    public boolean isDeficient() {
        return sumOfFactors() - _number < _number;
    }

    public static boolean isPerfect(int number) {
        return new Classifier6(number).isPerfect();
    }
}

Hay varios elementos sobre este código que vale la pena resaltar:

  • Tiene extensas unidades de prueba (en parte porque lo escribí para una discusión sobre desarrollo orientado a pruebas).
  • La clase consta de un gran número de métodos cohesivos, lo cual es un efecto secundario del uso de desarrollo orientado a pruebas en su construcción.
  • Hay una optimización de desarrollo incorporada en calculateFactors() . La sustancia de esta clase consiste en reunir los factores de manera que pueda sumarlos y finalmente clasificarlos. Los factores siempre se pueden recolectar en pares. Por ejemplo, si el número en cuestión es 16, cuando tomo el factor 2 también puedo tomar 8 porque 2 x 8 = 16. Si recolecto los factores por parejas, sólo necesito buscar factores hasta la raíz cuadrada del número objetivo, que es precisamente lo que hace el método calculateFactors() .

Clasificador (levemente más) funcional

Usando las mismas técnicas de desarrollo orientadas a pruebas, creé una versión alternativa del clasificador, la cual aparece en el Listado 2:

Listado 2. Clasificador de números ligeramente más funcional
public class NumberClassifier {

    static public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    static public Set<Integer> factors(int number) {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(number, i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }

    static public int sum(Set<Integer> factors) {
        Iterator it = factors.iterator();
        int sum = 0;
        while (it.hasNext())
            sum += (Integer) it.next();
        return sum;
    }

    static public boolean isPerfect(int number) {
        return sum(factors(number)) - number == number;
    }

    static public boolean isAbundant(int number) {
        return sum(factors(number)) - number > number;
    }

    static public boolean isDeficient(int number) {
        return sum(factors(number)) - number < number;
    }
}

Las diferencias entre estas dos versiones del clasificador son sutiles pero importantes. La principal diferencia es la falta decidida de estado compartido en el Listado 2. La eliminación (o por lo menos disminución) de estado compartido es una de las abstracciones favorecidas en la programación funcional. En lugar de compartir estados a lo largo de los métodos como resultados intermedios(vea el campo factors en el Listado 1), llamo a los métodos directamente, eliminando el estado. Desde un punto de vista de diseño, esto hace al método factors() más largo, pero evita que el campo factors "deje que se escape" el método. Note también que la versión del Listado 2 puede consistir completamente en métodos estáticos. No hay conocimiento compartido entre los métodos, así que necesito menos encapsulamiento vía ámbito. Todos estos métodos funcionan perfectamente bien si usted les da los tipos de parámetros de entrada que esperan. (Este es un ejemplo de función pura, un concepto que investigaré más en una próxima entrega).


Funciones

La programación funcional es un área amplia y en expansión de las ciencias de la computación, que ha visto una explosión de interés recientemente. Hay nuevos lenguajes funcionales en la JVM (como Scala y Clojure), e infraestructuras (como Functional Java y Akka), en escena (vea Recursos), junto con las afirmaciones usuales en cuanto a menos errores, mayor productividad, mejor apariencia, más dinero, y demás. Más que intentar abordar todo el tema de la programación funcional desde el principio, me enfocaré en varios conceptos clave y seguiré algunas interesantes implicaciones derivadas de estos conceptos.

En el núcleo de la programación funcional se halla la función, tal como las clases son la abstracción primaria en los lenguajes orientados a objetos. Las funciones son los bloques de creación para el procesamiento y están empapadas con varios atributos que no se encuentran en los lenguajes tradicionales.

Funciones de orden superior

Las funciones de orden superior pueden tomar a otras funciones como argumentos o retornarlas como resultados. En el lenguaje Java no tenemos esta construcción. Lo más cercano a lo que se puede llegar es a utilizar una clase (con frecuencia una clase anónima) como un "contenedor" de un método que usted necesita ejecutar. Java no tiene funciones independientes (ni métodos), de manera que no pueden retornarse desde funciones ni pasarse como parámetros.

Esta capacidad es importante en lenguajes funcionales por lo menos por dos razones. Primero, tener funciones de orden superior significa que usted puede hacer suposiciones sobre cómo encajarán las partes del lenguaje. Por ejemplo, usted puede eliminar categorías completas de métodos de una jerarquía de clase al crear un mecanismo general que cruce listas y que aplique una (o más) funciones de orden superior a cada elemento. (Pronto le mostraré un ejemplo de esta construcción.) Segundo, al permitir que las funciones retornen valores, usted crea la oportunidad para construir sistemas altamente dinámicos y adaptables.

Los problemas susceptibles a ser solucionados mediante el uso de funciones de orden superior no son exclusivos a los lenguajes funcionales. Sin embargo, la forma en que usted resuelve el problema difiere cuando piensa funcionalmente. Considere el ejemplo del Listado 3 (tomado de una base de código más extensa) sobre un método que realiza acceso a datos protegidos:

Listado 3. Plantilla de código potencialmente reutilizable
public void addOrderFrom(ShoppingCart cart, String userName,
                     Order order) throws Exception {
    setupDataInfrastructure();
    try {
        add(order, userKeyBasedOn(userName));
        addLineItemsFrom(cart, order.getOrderKey());
        completeTransaction();
    } catch (Exception condition) {
        rollbackTransaction();
        throw condition;
    } finally {
        cleanUp();
    }
}

El código del Listado 3 efectúa inicialización, realiza algo de trabajo, completa la transacción si todo está bien, la reversa en cualquier otro caso y finalmente, limpia los recursos. Claramente, la porción de texto modelo de este código podría reutilizarse, y normalmente lo hacemos en lenguajes orientados a objetos al crear estructura. En este caso, combinaré dos de los patrones "Patrones de Diseño de La Pandilla de los Cuatro (vea en Recursos): the Template Method and Command patterns". El patrón Template Method sugiere que debo mover el código de texto modelo común hacia arriba en la jerarquía de herencia, postergando los detalles algorítmicos a las clases hijas. El patrón de diseño Command ofrece una manera de encapsular comportamiento en una clase con semántica de ejecución bien conocida. El Listado 4 muestra el resultado de aplicar estos dos patrones al código del Listado 3:

Listado 4. Código de orden reestructurado
public void wrapInTransaction(Command c) throws Exception {
    setupDataInfrastructure();
    try {
        c.execute();
        completeTransaction();
    } catch (Exception condition) {
        rollbackTransaction();
        throw condition;
    } finally {
        cleanUp();
    }
}

public void addOrderFrom(final ShoppingCart cart, final String userName,
                         final Order order) throws Exception {
    wrapInTransaction(new Command() {
        public void execute() {
            add(order, userKeyBasedOn(userName));
            addLineItemsFrom(cart, order.getOrderKey());
        }
    });                
}

En el Listado 4, extraigo las partes genéricas del código hacia el método wrapInTransaction() (cuya semántica usted puede reconocer — es básicamente una versión simple del TransactionTemplate de Spring), pasando un objeto Command como la unidad de trabajo. El método addOrderFrom() colapsa hacia la definición de la creación de una clase interna anónima de la clase de comando, empaquetando los dos elementos de trabajo.

Empaquetar el comportamiento que necesito en una clase de comando es netamente un artefacto del diseño de Java, lo cual no incluye ningún tipo de comportamiento independiente. Todo el comportamiento en Java debe residir dentro de una clase. Incluso los diseñadores de lenguaje verán rápidamente una deficiencia en este diseño — en retrospectiva, es un poco ingenuo pensar que nunca habrá un comportamiento que no esté atado a una clase. JDK 1.1 rectificó esta deficiencia agregando clases internas anónimas, que al menos proporcionaron azúcar sintáctica para crear grandes cantidades de clases pequeñas con tan solo algunos métodos que son netamente funcionales, no estructurales. Para leer un ensayo salvajemente entretenido y humorístico sobre este aspecto de Java, consulte "Execution in the Kingdom of Nouns" de Steve Yegge (vea Recursos).

Java me obliga a crear una instancia de una clase Command , incluso aunque realmente todo lo que quiero es el método dentro de la clase. La clase en sí no aporta beneficios: no tiene campos, no tiene constructor (además del auto-generado de Java) y no tiene estado. Sirve únicamente como empaquetador para el comportamiento que hay dentro del método. En cambio, en un lenguaje funcional, esto se manejaría mediante una función de orden superior.

Si voy a dejar de lado el lenguaje Java por un momento, puedo acercarme semánticamente al ideal de programación funcional utilizando cerramientos. El Listado 5 muestra el mismo ejemplo reestructurado, pero usando Groovy (vea Recursos) en lugar de Java:

Listado 5. Usando cerramientos Groovy en lugar de clases de comando
def wrapInTransaction(command) {
  setupDataInfrastructure()
  try {
    command()
    completeTransaction()
  } catch (Exception ex) {
    rollbackTransaction()
    throw ex
  } finally {
    cleanUp()
  }
}

def addOrderFrom(cart, userName, order) {
  wrapInTransaction {
    add order, userKeyBasedOn(userName)
    addLineItemsFrom cart, order.getOrderKey()
  }
}

En Groovy, todo lo que hay dentro de corchetes {} es un bloque de código, y los bloques de código pueden pasarse como parámetros, imitando a funciones de orden superior. Detrás de escena, Groovy está implementando el patrón de diseño Command por usted. Cada bloque de cierre en Groovy es en realidad una instancia de un tipo de cierre Groovy, que incluye un métodocall() que es invocado automáticamente cuando usted pone un conjunto vacío de paréntesis después de la variable que contiene la instancia de cierre. Groovy ha habilitado algunos comportamientos similares a programación funcional al crear las estructuras de datos apropiadas, con la correspondiente azúcar sintáctica, en el lenguaje mismo. Como mostraré en futuras entregas, Groovy también incluye otras capacidades de programación funcional que van más allá de Java. También retornaré para hace algunas interesantes comparaciones entre cerramientos y funciones de orden superior, en otra entrega.

Funciones de primera clase

Las funciones de un lenguaje funcional se consideran de primera clase, lo cual significa que las funciones pueden aparecer en cualquier lugar en que cualquier construcción de otro lenguaje (como las variables) pueda aparecer. La presencia de funciones de primera clase permite el uso de funciones de formas inesperadas y obliga a pensar en soluciones de forma diferente, como la aplicación de operaciones relativamente genéricas (con detalles superficiales) a estructuras de datos estándar. A su vez, esto expone un cambio fundamental del pensamiento en los lenguajes funcionales: Enfóquese en los resultados, no en los pasos.

En lenguajes de programación imperativa, debo pensar en cada paso atómico de mi algoritmo. El código del Listado 1 muestra esto. Para resolver el clasificador de números debo discernir exactamente cómo recolectar los factores, lo que a su vez significa que tengo que escribir código específico para hacer bucles por los números para determinar los factores. Pero hacer bucles en las listas, efectuando operaciones para cada elemento, realmente suena como algo común. Considere el código re-implementado de clasificación de números usando la infraestructura Functional Java, que aparece en el Listado 6:

Listado 6. Clasificador de números funcional
public class FNumberClassifier {

    public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    public List<Integer> factors(final int number) {
        return range(1, number+1).filter(new F<Integer, Boolean>() {
            public Boolean f(final Integer i) {
                return number % i == 0;
            }
        });
    }

    public int sum(List<Integer> factors) {
        return factors.foldLeft(fj.function.Integers.add, 0);
    }

    public boolean isPerfect(int number) {
        return sum(factors(number)) - number == number;
    }

    public boolean isAbundant(int number) {
        return sum(factors(number)) - number > number;
    }

    public boolean isDeficiend(int number) {
        return sum(factors(number)) - number < number;
    }
}

Las principales diferencias entre el Listado 6 y el Listado 2 residen en dos métodos: sum() y factors(). El método sum() aprovecha el método en la clase List en Functional Java, la foldLeft() . Esta es una variación específica de un concepto de manipulación de lista llamado un catamorfismo, que es una generalización del plegado de listas. En este caso, un "fold left" significa:

  1. Tomar un valor inicial y combinarlo mediante una operación en el primer elemento de la lista.
  2. Tomar el resultado y aplicar la misma operación al siguiente elemento.
  3. Continuar haciendo esto hasta agotar la lista.

Note que esto es exactamente lo que usted hace cuando suma una lista de números: comienza con cero, añade el primer elemento, toma ese resultado y lo añade al segundo, y continúa haciéndolo hasta agotar la lista. Functional Java proporciona una función de orden superior (en este ejemplo, la enumeración Integers.add ) y se encarga de aplicarla por usted. (Desde luego, Java realmente no tiene funciones de orden superior, pero usted puede escribir una buena analogía si la restringe a una estructura y tipo de datos en particular).

El otro método intrigante en el Listado 6 esfactors(), que ilustra mi consejo "Enfóquese en los resultados, no en los pasos". ¿Cuál es la esencia del problema de descubrir los factores de un número? Dicho de otra forma, dada una lista de todos los posibles números hasta un número objetivo, ¿cómo determino cuáles son factores del número? Esto sugiere una operación de filtrado — puedo filtrar toda la lista de números, eliminando aquellos que no satisfagan mis criterios. El método básicamente se lee como esta descripción: tome el rango de números desde 1 hasta mi número (el rango no es inclusivo), por ello el +1); filtre la lista con base en el código en el método f() , que es la forma de Functional Java para permitirle crear una clase con tipos de datos específicos; y retorne los valores.

Este código también ilustra un concepto mucho más grande, como una tendencia en los lenguajes de programación en general. En otras épocas, los desarrolladores tenían que manejar todo tipo de cosas molestas como asignación de memoria, recolección de basura y apuntadores. Con el tiempo, los lenguajes se han venido encargando más de esas responsabilidades. A medida que los computadores se han tornado más eficientes, hemos descargado más y más tareas mundanas (automatizables) en los lenguajes y los tiempos de ejecución. Como desarrollador Java, me he acostumbrado bastante a ceder todos mis problemas de memoria al lenguaje. La programación funcional está expandiendo ese mandato, abarcando detalles más específicos. Con el paso del tiempo vamos a pasar menos tiempo preocupándonos por los pasos necesarios para resolver un problema y vamos a pensar más en términos de procesos. A medida que esta serie avance, mostraré muchos ejemplos como este.


Conclusión

La programación funcional es más una forma de pensamiento que un conjunto particular de herramientas y lenguajes. En esta primera entrega, comencé cubriendo algunos temas de la programación funcional, desde decisiones de diseño sencillas hasta algo de re-pensamiento ambicioso de problemas. Reescribí una clase Java simple para hacerla más funcional y luego comencé a profundizar en algunos temas que separan al pensamiento funcional del uso de lenguajes imperativos tradicionales.

Dos conceptos importantes y de largo alcance aparecieron aquí por primera vez. Primero, enfocarse en los resultados, no en los detalles. La programación funcional intenta presentar los problemas de forma diferente porque usted tiene diferentes bloques de creación que fomentan soluciones. La segunda tendencia que mostraré durante esta serie es la descarga de detalles mundanos hacia los lenguajes de programación y tiempos de ejecución, lo cual nos permite enfocarnos en los aspectos únicos de nuestros problemas de programación. En la próxima entrega, continuaré observando los aspectos generales de la programación funcional y cómo esta aplica al desarrollo de software de hoy.

Recursos

Aprender

  • The Productive Programmer (Neal Ford, O'Reilly Media, 2008): El libro más reciente de Neal Ford amplía numerosos temas de esta serie.
  • Monads: Monads, un tema legendariamente complicado en lenguajes funcionales, se tratará en una entrega futura de esta serie.
  • Scala: Scala es un lenguaje moderno y funcional en la JVM.
  • Clojure: Clojure es un Lisp moderno y funcional que se ejecuta en la JVM.
  • Podcast: Stuart Halloway on Clojure: Aprenda más sobre Clojure y sepa las dos principales razones por las que ha sido adoptado rápidamente y por qué su popularidad aumenta rápidamente.
  • Akka: Akka es una infraestructura para Java que permite una concurrencia sofisticada basada en actores.
  • Functional Java: Functional Java es una infraestructura que añade varias construcciones de lenguaje funcional a Java.
  • Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et al., Addison-Wesley, 1994): Trabajo clásico de La Pandilla de los Cuatro sobre patrones de diseño.
  • "Execution in the Kingdom of Nouns" (Steve Yegge, marzo del 2006): Un gracioso desahogo sobre algunos aspectos del diseño en lenguaje Java.
  • Navegue la librería de tecnología donde encontrará libros sobre estos y otros temas técnicos.
  • zona de tecnología Java developerWorks: Encuentre cientos de artículos sobre cada aspecto de la programación Java.

Comentar

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=tecnologia Java
ArticleID=732624
ArticleTitle=Pensamiento funcional: Pensando funcionalmente, Parte 1
publish-date=07182011