Funciones

Esta el undécima parte del tour de Ceylon. En la parada anterior anterior hemos visto acerca de los paquetes y los módulos. Esta parada cubre los temas de clases de primero orden y de orden superior.

Clases de primer orden y de orden superior

Ceylon no es un lenguaje de programación funcional: debido a que tiene métodos que pueden tener efectos secundarios.Pero tiene algo en común con los lenguajes de programación funcional y es que permiten tratar a las funciones como valores, lo cual a los ojos de algunas personas hace del lenguaje algún tipo de híbrido. En verdad, esto de tratar funciones como valores en un lenguaje orientado a objetos no tiene nada nuevo, por ejemplo, Smalltalk es uno de los primeros y aun uno de los lenguajes de programación de objetos mas puro, fue construido en torno a esta idea. De cualquier manera, Ceylon, como smalltalk y un numero de otros lenguajes orientados a objetos, permite pasar a una función como un objeto y llevarlo a través del sistema.

En esta parte, hablaremos acerca del soporte de Ceylon para funciones de primer orden y de orden superior.

  • El soporte a clases de primer orden significa la habilidad de tratar a funciones como valores, asignarla a variables y pasarlas como argumento.
  • Una función de orden superior es una función que acepta funciones como argumentos o devuelve una función.

Esta claro que estas dos ideas van de la mano, así que usaremos el termino “funciones de orden superior” desde ahora en adelante.

Representado el tipo de una función

Ceylon es un lenguaje fuertemente tipado. Así que si vamos a tratar a las funciones como valores, la primera pregunta a contestar es: ¿Cuál es el tipo de una función? Necesitamos una manera de representar el tipo de retorno y los tipos de los parámetros de una función dentro del sistema de tipos.

Recuerda que Ceylon, no tiene tipos de datos “primitivos”. Un principio solido del diseño es que cada tipo deberá ser representado dentro del sistema de tipos como una declaración de una clase o una interfaz.

En Ceylon, un simple tipo Callable abstrae todas las funciones. Su declaración es la siguiente:

shared interface Callable<out Return, in Arguments>
        given Arguments satisfies Anything[] {}

El parámetro de tipo Return representa el tipo de retorno de la función. El parámetro de tipo secuencial Arguments, deberá ser una secuencia de tipo, representa los tipos de los parámetros de la función. Podemos representar una lista de parámetros como un tipo tupla. Por ejemplo, la lista de parámetros (String s, Float x) es representada como la tupla [String, Float].

Así, que tomando la siguiente función:

function sum(Interger x, Integer y) => x+y;

El tipo de la función sum() es:

Callabla<Integer,[Integer,Integer]>

¿Qué hay acerca de las funciones void? Bueno, El tipo de retorno de una función void es considerado a ser Anything. Tal como el tipo de la función print() es:

Callable<Anything,[Anything]>

Habrá algunas personas que tienen un background en lenguajes como ML que tal ves estén esperando que void pueda ser identificado con algún tipo “unit”, por ejemplos, Null o tal ves []. Pero este enfoque deberá significar que un método que no sea void deberá no estar disponible para refinar un método void y que una función que no sea de tipo void no estará disponible para ser asignado a un parámetro funcional void. Sin embargo, código perfectamente razonable deberá ser rechazado por el compilador.

Note que una función void con una implementación concreta devuelve implícitamente el valor null. Esto es completamente diferente a una función declarada a devolver el tipo Anything, que puede devolver cualquier calor, pero deberá hacerlo explícitamente, a través de la declaración return. Las siguientes funciones tiene el mismo tipo, Anything, pero no hacen exactamente la misma cosa:

Anything hello() {
    print("Hello")
    return "hello";
}

void hello() {
    print("hello");
    //retorno implicito de null
}

No deberás de confiar en una función que es declarada void, debido a que puede ser un método que es refinado por una método no void o una referencia a una función no void.

Podemos abreviar tipos Callable con un poco de azúcar sintáctica:

  • Integer(Integer,Integer) significa Callable<Integer,[Integer,Integer]>

    e igualmente,

  • Anything(String) significa Callable<Anything,[String]>.

Definiendo funciones de orden superior

Ahora tenemos suficiente conocimiento para poder escribir funciones de orden superior. Por ejemplo, podemos crear la función repeat() que ejecute repetidamente una función.

void repeat(Integer times,
        Anything(Integer) perform) {
    for (i in 1..times) {
        perform(i);
    }
}

Ahora, ejecutemos:

void printNum(Integer n) => print(n);
repeat(10, printNum);

Esto deberá de imprimir los números del 1 al 10 en consola.

Existe un problema con esto. En Ceylon, como veremos después, frecuentemente llamamos funciones usando argumentos con nombre, pero el tipo Callable no necesita codificar el nombre de los parámetros de la función. Así Ceylon tiene una alternativa, mas elegante, sintaxis para declarar un parámetro de tipo Callable:

void repeat(Integer timer,
        void perform(Integer n) ) {
    for (i in 1..times) {
        perform { n=i; };
    }
}

Esta versión es un poco mas legible, así que el la sintaxis preferida.

Referencias a funciones

Cuando el nombre de una función aparece sin argumentos, como printNum en el anterior ejercicio, es llamada una referencia a una función. Una referencia a una función es el objeto que realmente es de tipo Callable. En este caso, printNum tiene el tipo Callable<Anything,Integer>.

Ahora, ¿recuerdas como dijimos que Anything es el tipo de retorno de una función void y también la raíz de la jerarquía de tipos? Bueno, esto es útil aquí, esto significa que podemos asignar una función de cualquier tipo a un parámetro que espera una función de tipo void, siempre y cuando coincidan las lista de parámetros:

Boolean attemptPrint(Integer n) {
    try {
        print(n);
        return true;
    }
    catch (Exception n) {
        return false;
    }
}

Y podemos mandarla a llamar así:

repeat(10, attemptPrint);

Otra forma en la que podemos producir una referencia a una función es por medio de aplicar parcialmente un método a una expresión (que recibe).

class Hello(String name) {
    shared void say(Integer n) {
        print("Hello, ``name``, for the ``n``th time!");
    }
}

Y lo ejecutaremos de la siguiente manera:

repeat(10, Hello("Gavin").say);

En la expresión anterior Hello("Gavin").say tiene el mismo tipo que print, es decir es de tipo Anything(Integer).

Curring

Un método o función puede ser declarado en una forma llamada curried, permitiendo al método o función ser parcialmente aplicado a sus argumentos. Una función curried tiene múltiples listas de parámetros:

Float adder(Float x)(Float y) => x+y;

La función adder() tiene el tipo Float(Float)(Float). Podemos invocarla con un solo argumento para obtener la referencia a una función de tipo Float(Float), y mantener esta referencia como una función, como esta:

Float addOne(Float y);
addOne = adder(1.0);

O como un valor, como en el siguiente caso:

Float(Float) addOne = adder(1.0);

(La única diferencia entre estos dos enfoques es que en el primer caso le asignamos un nombre a el parámetro de addOne().)

Entonces subsecuente mente invocamos a addOne(), el actual cuerpo de adder() es finalmente ejecutado, produciendo un Float.

Funciones anonimas

Las funciones mas famosas del estilo de orden superior son un trio de funciones que transforman, filtran, coleccionan secuencias de valores. En Ceylon, estas tres funciones, map(), filter(), y fold() son métodos de la interfaz Iterable, (Incluso hay una cuarta, una amiga un poco menos glamurosa llamada find(), también un método de Iterable.)

Como probablemente habrás notado todas las definiciones de funciones han sido declaradas con un nombre, usando la sintaxis tradicional estilo C. No hay nada errado con pasar un nombre de una función a map() o filter() y de hecho es útil:

Float max = measurements.fold(0.0, largest<Float>);

Sin embargo, común mente, es un inconveniente tener que declarar una función dándole nombre completa solo para pasárselo a map(), filter(), fold() o find(). En vez de ello, podemos declarar una función anónima, como parte de la lista de argumentos.

Float max = measurements.fold(0.0,
                (Float max, Float num) =>
                    num > max then num  else max);

Una función anónima consta de:

  • opcionalmente, la palabra function o void
  • una lista de parámetros, encerrados entre paréntesis, seguidos por
  • una flecha gorda, =>, con una expresión o
  • un bloque.

Así que podemos reescribir lo anterior usando un bloque.

Float max = measurements.fold(0.0,
        (Float max, Float num) {
            return num>max then num else max;
        });

Note que es un poco mas difícil dar una buena vista a las funciones anónimas con bloque, así que usualmente es mejor darle el nombre a una función y usarla como referencia.

Mas acerca de funciones de orden superior

Veamos un ejemplo practico, que mezcle ambas maneras de representar el tipo de una función.

Supongamos que tenemos algún tipo de interfaz de usuario que puede ser observado por objetos en el sistema. Podemos usar algo como el patrón de Java Observer/Observable:

interface Observer {
    shared formal void observe(Event event);
}

abstract class Component() {
    variable {Observer*} observers = {};

    shared void addObserver(Observer observer) {
        observers = {observer, *observers};
    }

    shared void fire(Event event) {
        for (o in observers) {
            o.observe(event);
        }
    }
}

Pero ahora todos los objetos tienen que implementar la interfaz Observer, que solo tiene un método. ¿Porqué no simplemente dejamos fuera a la interfaz y permitimos a los observadores de eventos solo registrar un objeto función como su evento de escucha? En el siguiente código, definimos que el método addObserver acepte una función como parámetro.

abstract class Component() {
    variable {Anything((Event)*} observers = {};

    shared void addObserver(void observe(Event event)) {
        observers = {observe, *observers};
    }

    shared void fire(Event event) {
        for (observe in observers) {
            observe(event);
        }
    }
}

Aquí podemos ver la diferencia entre las dos maneras de especificar el el tipo de una función:

  • void observe(Event event) es mas legible en lista de parámetro, donde observe es el nombre del parámetro, pero
  • Anything(Event) es útil en el tipo del contenedor tal como un iterable.

Ahora, cualquier observador de evento puede solo pasar una referencia a uno de sus métodos a addObserver():

class Listener(Component component) {

    void onEvent(Event e){
        //responde al evento
        // ...
    }

    component.addObserver(onEvent);

    // ...
}

Cuando el nombre de el método aparece en una expresión sin una lista de argumentos después de el, es una referencia a un método, no una invocación al método. Aquí la expresión de tipo Anything(Event) que refiere al método onEvent().

Si onEvent() fuese shared, podemos incluso hilar Component y Listener desde algún otro código, para eliminar la dependencia de Listener sobre Component.

class Listener() {

    shared void onEvent() {
        // respuesta a el evento
        // ...
    }
}

void listen(Component component, Listener listener) {
    component.addObserver(listener.onEvent);
}

Aquí la sintaxis de listener.onEvent() es un tipo de aplicación parcial del método onEvent(). Esto no causa que el método sea ejecutado(debido a que no hemos provisto una lista de parámetros aún). En vez, resulta en una función que empaqueta juntos a el método referencia onEvent y el método recibidor listener.

Es también posible declarar un método que devuelva una función. Vamos a considerarla habilidad para remover observadores desde un Component. Podemos usar una interfaz subscription:

 interface Subscription {
    shared formal void cancel();
 }

 abstract class Component() {
     variable {Anything(Event)*} observers = {};

     shared Subscription addObserver(void observe(Event event)) {
         observers = {observe,*observers};
         object subscription satisfies Subscription {
             cancel() => observers =
                     { for (o in observers) if (o!=observe) o };
         }
         return subscription;
     }

     shared void fire(Event event) {
         for (observe in observers) {
             observe(event);
         }
     }
}

Pero una solución simple puede ser solo eliminar la interfaz y devolver el método cancel() directamente:

 abstract class Component() {
     variable {Anything(Event)*} observers = {};

     shared Anything() addObserver(void observe(Event event)) {
         observers = {observe,*observers};
         return void () => observers =
                     { for (o in observers) if (o!=observe) o };
     }

     shared void fire(Event event) {
         for (observe in observers) {
             observe(event);
         }
     }
}

Aquí, hemos definido una función anónima dentro de el método addObserver(), y retornar una referencia a esta función fuera del método. La referencia a la función anónima devuelta por addObserver() puede ser llamada por cualquier código que obtenga la referencia..

En caso que te estés preguntando el tipo de la función que se encuentra dentro del método addObserver() es Anything()(Anything(Event).

Note que la función anónima esta habilitada para usar el parámetro observe de addObserver(). Diremos que el método anidado recibe una closure de las no variable locales y parámetros desde afuera del método - Justo como un método de una clase recibe una closure de la clase inicializando parámetros y locales de la clase inicializador. En general, cualquier declaración de clase anidad, método o atributo siempre recibe la closure de la declaración de los miembros de la clase, método o atributo en que este es encerrado. Este es un ejemplo de que tan regular es el lenguaje.

Podemos invocar nuestro método de la siguiente manera:

addObserver(onEvent)();

Pero si estamos planeando usar el método de esta manera, probablemente no se buena razón para darle dos listas de parámetros. Esto es mucho mas probable que cuando estábamos planeando mantener o pasar la referencia a el método anidado en algún lugar método antes de invocarlo.

Anthing() cancel = addObserver(onEvent);

//...
cancel();

La primera linea demuestra como la referencia de una función puede ser mantenida. La segunda linea de código simplemente invoca la referencia devuelta a cancel().

Composición y curry

La función compose() lleva acaba composición de funciones. Por ejemplo, dadas las funciones print() y plus() en ceylon.language, con la siguiente forma:

shared void print(Anything line) { ... }

shared Value plus<Value>(Value x, Value y) { ... }

Podemos ver que el tipo de referencia de la función print() es Anything(Anything), y el tipo de la referencia a la función plus<Float> es Float(Float, Float). Entonces podemos escribir lo siguiente:

Anything(Float, Float) printSum = compose(print,plus<Float>);
printSum(2.0,2.0); //imprime 4.0

La función curry() produce una función con múltiples listas de parámetros, dada una función con múltiples lista de parámetros:

Anything(Float)(Float) printSumCurried = curry(printSum);
Anything(Float) printPlus2 = printSumCurried(2.0);
printPlus(2.0); //imprime 4.0

La función uncurry() hace lo opuesto, dándonos la forma original.

Anything(Float,Float) printSumUncurried = uncurry(printSumCurried);

Note que compose(), curry y uncurry() son funciones ordinarias escritas en Ceylon.

El operador spread

Ya hemos visto unos pocos ejemplos de el operador spread. Hemos visto como usarlo para instanciar un iterable:

{ "hello", *names }

O una tupla:

[x, y, *labels]

También podemos usarlo cuando llamamos una función. Considere la siguiente función.

 String formatDate(String format,
                   Integer day,
                   Integer|String month,
                   Integer year) {
     ...
}

Y supóngase que tenemos una tupla representando una fecha:

value date = [15, "January", 2010];

Entonces podemos pasar la fecha a nuestra función como lo siguiente:

formatDate("dd MMMMMM yyyy", *date);

Note que el tipo de la tupla ["dd MMMMMM yyyy", *date] es:

[String,Integer,String,Integer]

Ahora considerar el tipo de la función formatDate:

String(String,Integer,Integer|String,Integer)

O también:

Callable<String,[String,Integer,Integer|String,Integer]>

Desde que la tupla tipo [String,Integer,String,Integer] es un subtipo de [String,Integer,Integer|String,Integer], la invocación esta bien tipada. ¡Esto demuestra la relación entre tuplas y argumentos de función!.

Aún hay más

Podrás encontrar una discusión mas detallada de como Ceylon represente tipos de funciones usando tuplas aqui incluyendo una discusión a detalle de compose() y curry().

Ahora es turno de la sintaxis para lista de argumentos con nombre y para definir interfaces de usuario e información estructurada.