Pensamiento funcional: Pensando funcionalmente, Parte 2

Explorando la programación funcional y el control

Los lenguajes y estructuras funcionales permiten que el tiempo de ejecución controle los detalles de codificación mundanos como la iteración, la concurrencia y el estado. Pero esto no significa que usted no pueda recuperar el control cuando lo necesite. Un aspecto importante de pensar funcionalmente es saber cuánto control usted desea ceder, y cuándo.

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.



25-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 los 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.

En la primera entrega de esta serie, comencé hablando sobre algunas de las características de la programación funcional, mostrando cómo se manifiestan estas ideas tanto en Java como en otros lenguajes más funcionales. En este artículo continuaré este recorrido por los conceptos, hablando sobre funciones de primera clase, optimizaciones y cierre. Pero el tema subyacente en esta entrega es el control: cuándo lo quiere, cuándo lo necesita y cuándo debería delegarlo.

Funciones de primera clase y el control

Usando la biblioteca Functional Java (vea en Recursos), mostré la última vez la implementación de un número de clasificadores con métodos isFactor() y factorsOf() funcionales, como se muestra en el Listado 1:

Listado 1. Versión funcional del clasificador de números
public class FNumberClassifier {

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

    public List<Integer> factorsOf(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(factorsOf(number)) - number == number;
    }

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

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

En los métodos isFactor() y factorsOf() cedo a la infraestructura el control del algoritmo de bucle — ahora este decide la mejor forma de iterar sobre el rango de números. Si la infraestructura (o el lenguaje — su usted selecciona un lenguaje como Clojure o Scala — ) puede optimizar la implementación subyacente, usted se beneficia automáticamente. Aunque al principio usted puede estar renuente a ceder esta cantidad de control, note que esto sigue una tendencia general en los lenguajes de programación y tiempos de ejecución: Con el tiempo, el desarrollador se abstrae más de los detalles que la plataforma puede manejar con mayor eficiencia. Yo nunca me preocupo por la administración de memoria en la JVM porque la plataforma me permite dejar esto de lado. Desde luego, ocasionalmente hace que algo sea más difícil, pero es una buena compensación por el beneficio que usted recibe en la codificación cotidiana. Las construcciones del lenguaje funcional como las de orden superior y las de primera clase me permiten subir un escalón más de la escalera de la abstracción y enfocarme más en qué hace el código en lugar de en cómo lo hace.

Incluso con la infraestructura Functional Java, la codificación en este estilo en Java es engorrosa porque el lenguaje realmente no tiene sintaxis ni construcciones para sí. ¿Cómo se ve la codificación funcional en un lenguaje y qué hace?

Clasificador en Clojure

Clojure es un Lisp funcional diseñado para la JVM (vea Recursos). Considere el clasificador de números escrito en Clojure, que se muestra en el Listado 2:

Listado 2. Implementación Clojure del clasificador de números
(ns nealford.perfectnumbers)
(use '[clojure.contrib.import-static :only (import-static)])
(import-static java.lang.Math sqrt)

(defn is-factor? [factor number]
  (= 0 (rem number factor)))

(defn factors [number] 
  (set (for [n (range 1 (inc number)) :when (is-factor? n number)] n)))

(defn sum-factors [number] 
    (reduce + (factors number)))

(defn perfect? [number]
  (= number (- (sum-factors number) number)))

(defn abundant? [number]
  (< number (- (sum-factors number) number)))

(defn deficient? [number]
  (> number (- (sum-factors number) number)))

La mayoría del código del Listado 2 es bastante fácil de seguir, incluso si usted no es un desarrollador Lisp conservador — especialmente si puede aprender a leer desde adentro hacia afuera. Por ejemplo, el método is-factor? toma dos parámetros y pregunta si el remanente es igual a 0 cuando number es multiplicado por factor. De forma similar, los métodosperfect?, abundant? y deficient? deberían ser fáciles de descifrar, especialmente si usted revisa la implementación Java en el Listado 1.

El método sum-factors usa el método reduce integrado. sum-factors reduce la lista un elemento a la vez, usando la función (en este caso, +) suministrada como primer parámetro en cada elemento. El método reduce aparece de diferentes formas en diferentes lenguajes e infraestructuras; usted lo pudo ver en la versión Functional Java del Listado 1 como el método foldLeft() . El método factors retorna una lista de números, por lo cual estoy procesando la lista uno a la vez, agregando cada elemento a la suma acumulada, lo cual es el valor retornado por reduce. Usted podrá ver que tan pronto se acostumbre a pensar en términos de funciones de orden superior y de primera clase, podrá reducir (el juego de palabras es intencional) bastante ruido en su código.

El método factors puede parecer una colección aleatoria de símbolos. Pero tiene sentido en cuanto usted ha visto comprensiones de lista, uno de los diferentes y eficientes recursos para manipulación de listas de Clojure. Como antes, es más fácil entender factors desde adentro hacia afuera. No se confunda por los choques de la terminología de lenguaje. La palabra clave for en Clojure no significa un bucle for . En lugar de ello, piénselo como el abuelo de todas las construcciones de filtrado y transformación. En este caso, le estoy solicitando que filtre el rango de números de 1 a (number + 1), usando el predicado is-factor? (que es el método is-factor que definí antes en el Listado 2— note el uso intensivo de funciones de primera clase), retornando los números que coinciden. Lo que retorna esta operación es una lista de números que satisfacen mi criterio de filtrado, lo cual obligo dentro de un conjunto para remover duplicados.

Aunque aprender un nuevo lenguaje es algo complicado, usted obtiene bastante a cambio de poco por parte de los lenguajes funcionales cuando entiende sus recursos.

Optimización

Uno de los beneficios de cambiarse a un estilo funcional es la capacidad para aprovechar el soporte de las funciones de orden superior, proporcionado por el lenguaje o la infraestructura. ¿Pero qué pasa cuando usted no quiere ceder ese control? En mi ejemplo anterior, comparé el comportamiento interno de los mecanismos de iteración con el funcionamiento interno del gestor de memoria: la mayoría del tiempo usted estará feliz por no tenerse que preocupar por esos detalles. Pero ocasionalmente a usted le preocupan, como en el caso de las optimizaciones y ajustes similares.

En las dos versiones Java del clasificador de números que mostré en "Pensando funcionalmente, Parte 1," optimicé el código que determina los factores. La ingenua implementación original usaba el módulo (%) operador que es bastante ineficiente, para revisar cada número desde 2 hasta el número objetivo, para determinar si es un factor. Usted puede optimizar el algoritmo notando que los factores vienen en pares. Si usted está buscando los factores de 28, por ejemplo, cuando usted encuentra 2 también puede tomar el número 14. Si recolecta los factores por parejas, sólo necesitará buscar factores hasta la raíz cuadrada del número objetivo.

La optimización que fue fácil de hacer en la versión Java, parece imposible en la versión Functional Java porque no controlo la implementación del mecanismo de iteración directamente. Pero parte de aprender a pensar funcionalmente requiere de renunciar a nociones sobre ese tipo de control, lo cual le permite ejercer otro tipo de control.

Puedo replantear el problema original funcionalmente: filtrar todos los factores desde 1 hasta number, reteniendo sólo los factores que coincidan con mi predicado isFactor() . Esto se implementa en el Listado 3:

Listado 3. El método isFactor() .
public List<Integer> factorsOf(final int number) {
    return range(1, number+1).filter(new F<Integer, Boolean>() {
        public Boolean f(final Integer i) {
            return number % i == 0;
        }
    });
}

Aunque es elegante desde un punto de vista declarativo, el código del Listado 3 es bastante deficiente porque revisa cada número. Una vez que entiendo la optimización (recolectar factores por parejas hasta la raíz cuadrada), puedo replantear el problema como sigue:

  1. Filtrar todos los factores del número objetivo desde 1 hasta la raíz cuadrada del número.
  2. Dividir el número objetivo por cada uno de estos factores para obtener el factor simétrico, y agregarlo a la lista de factores.

Con esta meta en mente, puedo escribir la versión optimizada del método factorsOf() usando la biblioteca Functional Java, como se muestra en el Listado 4:

Listado 4. Método optimizado de búsqueda de factores
public List<Integer> factorsOfOptimzied(final int number) {
    List<Integer> factors = 
        range(1, (int) round(sqrt(number)+1))
        .filter(new F<Integer, Boolean>() {
            public Boolean f(final Integer i) {
                return number % i == 0;
            }});
    return factors.append(factors.map(new F<Integer, Integer>() {
                                      public Integer f(final Integer i) {
                                          return number / i;
                                      }}))
                                      .nub();
}

El código del Listado 4 está basado en el algoritmo que planteé previamente, con alguna sintaxis curiosa re querida por la infraestructura Functional Java. Primero, tomo el rango de números desde 1 hasta la raíz cuadrada del número objetivo más 1 (para asegurarme de capturar todos los factores). Segundo, filtro los resultados con base en el uso del operador de módulo como en versiones previas, empaquetado en un bloque de código Functional Java. Esta lista filtrada la guardo en la variable factors . Cuarto (leyendo desde adentro hacia afuera), tomo esta lista de factores y ejecuto la función map() , lo cual produce una nueva lista al ejecutar mi bloque de código contra cada elemento (correlacionando cada elemento en un nuevo valor). Mi lista de factores contiene todos los factores de mi número objetivo hasta su raíz cuadrada; necesito dividir cada uno por el número objetivo para obtener su factor simétrico, que es lo que hace el bloque de código enviado al métodomap() .Quinto, ahora que tengo la lista de factores simétricos, la adjunto a la lista original. Como último paso, debo tener en cuenta el hecho de que estoy conservando los factores en una List en lugar de en un Set. List métodos son convenientes para estos tipos de manipulaciones, pero un efecto colateral de mi algoritmo es una entrada duplicada cuando aparece la raíz cuadrada de un número entero. Por ejemplo, si el número objetivo es 16, la raíz del número entero que es 4 aparecerá en la lista de factores dos veces. Para continuar usando los convenientes métodos List , sólo necesita llamar su método nub() al final, lo cual remueve todos los duplicados.

Sólo porque usted puede normalmente renunciar a conocimiento sobre detalles de implementación cuando está usando abstracciones de nivel superior como programación funcional, no significa que no pueda "ensuciarse las manos" si debe hacerlo. La plataforma Java le protege principalmente de cosas de bajo nivel, pero si usted está decidido(a), puede escarbar hasta el nivel que necesite. De forma similar, en construcciones de programación funcional, usted generalmente estará dispuesto(a) a ceder detalles a la abstracción, reservando las veces que no lo hace para cuando realmente importa.

El panorama visual dominante en todo el código Functional Java que he mostrado hasta ahora es la sintaxis de bloque, que utiliza clases internas anónimas y genéricas como un tipo de construcción de pseudobloque de código de tipo cierre. Los cierres son uno de los recursos comunes al lenguaje funcional. ¿Qué les hace tan útiles en este mundo?


¿Qué es lo especial de los cierres?

Un cierre es una función que lleva un enlace implícito hacia todas las variables referenciadas dentro de sí. En otras palabras, la función (o método) encierra un contexto en torno a las cosas a las que hace referencia. Con bastante frecuencia los cierres se utilizan como un mecanismo de ejecución portátil en lenguajes funcionales e infraestructuras, pasados a funciones de orden superior como map() como el código de transformación. Functional Java usa clases internas anónimas para imitar algunos comportamientos de cierre "reales", pero simplemente no puede hacerlo completamente porque Java no soporta cierres. ¿Pero qué significa esto?

El Listado 5 muestra un ejemplo de lo que hace a los cierres tan especiales. Está escrito en Groovy, que soporta cierres mediante su mecanismo de bloque de código.

Listado 5. Código Groovy ilustrando cierres
def makeCounter() {
  def very_local_variable = 0
  return { return very_local_variable += 1 }
}

c1 = makeCounter()
c1()
c1()
c1()
c2 = makeCounter()

println "C1 = ${c1()}, C2 = ${c2()}"
// output: C1 = 4, C2 = 1

El método makeCounter() primero define una variable local con un nombre apropiado y luego retorna un bloque de código que utiliza esa variable. Note que el tipo retornado por el método makeCounter() es un bloque de código, no un valor. Lo que hace ese bloque de código es incrementar el valor de la variable local y retornarla. He puesto llamados return explícitos en este código, ambos opcionales en Groovy, ¡pero el código es aún más críptico sin ellos!

Para ejecutar el método makeCounter() , asigno el bloque de código a una variable C1 , y luego la llamo tres veces. Estoy usando azúcar sintáctica Groovy para ejecutar un bloque de código, lo cual es poner un paréntesis adyacente a la variable del bloque de código. Luego, llamo a makeCounter() de nuevo, asignando una nueva instancia del bloque de código a C2. Por último, ejecuto C1 de nuevo junto con C2. Note que cada uno de los bloques de código ha hecho seguimiento de una instancia separada de very_local_variable. E so es lo que quiere decircontexto de cerramiento. Incluso si se define una variable local dentro del método, el bloque de código está enlazado a esa variable porque la referencia, queriendo decir que debe rastrearla mientras la instancia del bloque de código esté viva.

Lo más cerca a lo que usted puede llegar para el mismo comportamiento en Java aparece en el Listado 6:

Listado 6. MakeCounter en Java
public class Counter {
    private int varField;

    public Counter(int var) {
        varField = var;
    }

    public static Counter makeCounter() {
        return new Counter(0);
    }

    public int execute() {
        return ++varField;
    }
}

Muchas variantes de la clase Counter son posibles, pero usted continuará atascado(a) tratando de manejar el estado usted mismo(a). Esto ilustra por qué el uso de cierres ejemplifica el pensamiento funcional: permita que el tiempo de ejecución maneje el estado. En lugar de forzarle a usted a manejar la creación de campos y el estado de mimar al estado (incluyendo el horrible prospecto de usar su código en un entorno multihilado), permita que el lenguaje o la infraestructura administren invisiblemente ese estado por usted.

Eventualmente obtendremos cierres en un release futuro de Java (una discusión que afortunadamente está por fuera del alcance de este artículo). Su apariencia en Java tendrá que agradecer dos beneficios. Primero, simplificará en gran medida las capacidades de los escritores de infraestructuras y bibliotecas al tiempo que mejora su sintaxis. Segundo, proporcionara un común denominador de nivel inferior para el soporte de cierres en todos los lenguajes que se ejecuten en la JVM. Incluso aunque muchos lenguajes JVM soportan cierres, todos deben implementar sus propias versiones, lo que hace que pasar cierres entre lenguajes sea bastante engorroso. Si el lenguaje Java define un formato individual, todos los demás lenguajes lo pueden aprovechar.


Conclusión

Ceder el control sobre detalles de nivel inferior es una tendencia general en el desarrollo de software. Felizmente hemos delegado la responsabilidad por la recolección de basura, la administración de memoria y las diferencias de hardware. La programación funcional representa el siguiente salto en la abstracción: ceder más detalles mundanos como iteración, concurrencia y estado a tiempos de ejecución, tanto como sea posible. Esto no quiere decir que usted no pueda retomar el control si necesita hacerlo — pero tiene que querer hacerlo, no verse forzado a ello.

En la próxima entrega continuaré mi exploración de las construcciones de programación funcional en Java y sus parientes cercanos mediante la introducción de la aplicación de currying y del método partial.

Recursos

Aprender

Obtener los productos y tecnologías

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=742244
ArticleTitle=Pensamiento funcional: Pensando funcionalmente, Parte 2
publish-date=07252011