Funciones en Ceylon, segunda parte

En mi post anterior, hablé acerca de las funciones de orden superior, la manera en que se pasan referencias a métodos, cómo se invocan, etc. Pues bien, ha habido bastante progreso en Ceylon, en varias áreas, y una de las que considero importantes es precisamente el manejo de funciones; algunas de las cosas que mencioné en ese post han cambiado, mientras que ha surgido funcionalidad nueva que no estaba disponible previamente. Así que veamos los cambios:

Callables

Primero que nada, ya no tenemos la restricción de que un Callable no podía ser invocado; ahora ya se puede, por lo que esto ya es válido:

value obj = MiClase();
Callable<String,Integer> f = obj.metodo;
String s = f(1);

La otra forma sigue siendo igual de válida:

value obj = MiClase();
//Aqui usamos inferencia de tipos
function f1(Integer x) = obj.metodo;
//Aqui ya lo definimos de manera estática
String f2(Integer x) = obj.metodo;
String s1 = f1(1);
String s2 = f2(2);

Argumentos por nombre

En Ceylon hay dos formas de hacer una invocación: usando argumentos posicionales (que es la manera en que todo mundo está acostumbrado a hacerlo) o usando argumentos nombrados (que no todos los lenguajes tienen y los que lo tienen, usan distintas sintaxis).

Este es un ejemplo de una función muy simple, y las dos maneras de invocarla:

Integer sumar(Integer a, Integer b) {
  return a+b;
}
sumar(1,1);
sumar {
  a=1;
  b=1;
};
sumar {
  b=5;
  a=3;
};
//Obviamente se puede hacer dentro de otra llamada y se pueden mezclar estilos
print(sumar {
  a=sumar(1,1);
  b=2;
});
//No es necesario el cambio de línea, solamente es para mejor legibilidad
sumar{b=2;a=2;};

La sintaxis de las definiciones de funciones y también de las invocaciones, se pone interesante cuando uno de los parámetros es una función:

Integer? busca(Integer[] lista, Boolean condicion(Integer elemento)) {
  for (num in lista) {
    if (condicion(num)) {
      return num;
    }
  }
  return null;
}
//Definamos funciones para evaluar números
Boolean esNon(Integer n) {
  return n%2==1;
}
function esPar(Integer n) {
  return n%2==0;
}
//Teniendo una función o método, se puede invocar así
busca({ 1, 3, 4, 5 }, esPar); //Devuelve 4
busca({ 2, 4, 5, 6 }, esNon); //Devuelve 5
//Con argumentos nombrados
busca {
  lista = { 1, 2, 3, 4 };
  condicion = esPar;
}; //devuelve 2

Hasta aquí estamos manejando funciones de orden superior, usando referencias a métodos y/o funciones. Este ejemplo que puse es particularmente interesante porque se presta para que muchos digan "oye, pero es demasiado verboso tener que definir una función tan simple como lo de evaluar si un número es par/non, en otros lenguajes se puede pasar un closure o una función anónima"; pues bien, en Ceylon ya tenemos también funciones anónimas. La restricción es que una función anónima puede ser solamente una expresión, pero para este ejemplo quedan a la medida (y esta restricción es temporal).

Sólo para reforzar el punto, quiero aclarar que una manera alterna de definir la función busca es así:

Integer? busca(Integer[] lista, Callable<Boolean,Integer> condicion) {
  for (num in lista) {
    if (condicion(num)) {
      return num;
    }
  }
  return null;
}

Funciones anónimas

Una función anónima no tiene nombre, y dado que solamente puede contener una expresión, no necesita llaves para definir su cuerpo. Entonces la sintaxis es simplemente (Tipo argumento, Tipo argumento) expresion. Entonces podemos implementar lo de par y non como funciones anónimas:

busca({ 1, 3, 4, 5 }, (Integer x) x%2==0); //Devuelve 4
busca({ 2, 4, 5, 6 }, (Integer n) n%2==1); //Devuelve 5

Funciones inline

La siguiente pregunta es un poco obvia: ¿Qué pasa si queremos definir una función anónima de varias sentencias? La respuesta, estrictamente hablando, es que no se puede. Pero de manera más práctica, lo que se puede hacer es definir una función inline, lo cual es posible utilizando la sintaxis de argumentos nombrados:

busca {
  lista = { 9, 8, 7, 6, 5 };
  function condicion(Integer i) {
    if (i==0) { return false; }
    if (i in {1,2}) {return true;}
    for (x in 2..i-1) {
      if (i%x==0) { return false; }
    }
    return true;
  }
}; //devuelve 7

En este último caso, estamos definiendo una función para determinar si un número es primo, dentro de la invocación a otra función. La función que definimos en la invocación debe tener el mismo nombre que tiene en la definición de la función que estamos invocando, es decir, dado que el parámetro dentro de la función buscar se llama condicion, la función que definimos al invocar buscar debe llamarse condicion también.

Funciones parciales

La manera de implementar funciones parciales en Ceylon es simplemente definir múltiples listas de parámetros. Normalmente, un método o función tiene una sola lista de parámetros; cuando se definen dos o más, el compilador crea una nueva función anidada por cada lista de parámetros; la función más interna contiene el código definido originalmente, así como el tipo de dato definido (o inferido), mientras que las demás funciones que la envuelven, devuelven Callable. Un ejemplo simple puede ser así:

Integer multiplica(Integer a)(Integer b) {
  return a*b;
}
//Lo anterior es equivalente a haber escrito todo esto
Callable<Integer, Integer> multiplicaAnidado(Integer a) {
  Integer multiplicaInterno(Integer b) {
    return a*b;
  }
  return multiplicaInterno;
}

//Parece que invocamos una función normal
//Pero en realidad esto son dos invocaciones encadenadas
Integer dos = multiplica(1)(2);
//O podemos obtener una función parcial
function doble(Integer x) = multiplica(2);
Integer cuatro = doble(2);
Integer ocho = doble(4);

Para entender mejor la utilidad de las múltiples listas de parámetros, pondré un ejemplo más práctico. Imaginen una consulta de SQL parametrizada, que se convierte en un PreparedStatement de JDBC. En Ceylon podríamos tener algo así:

Map<String,Object>[] query(String sql)(Object... params) {
  //Abrir conexión
  //Preparar el statement
  //Obtener resultados
  //Cerrar conexión
  //Devolver resultados
}

//Podemos usarlo de manera simple
value rows1 = query("SELECT * FROM tabla")({});
value rows2 = query("SELECT * FROM tabla WHERE bla>?")({1});

//Pero podemos tener queries predefinidos y reutilizarlos
value blaMayorQue = query("SELECT * FROM tabla WHERE bla>?");
value bleIgualQue = query("SELECT * FROM tabla WHERE ble=?");
value blaMenorQueYBleComo = query("SELECT * FROM tabla WHERE bla<? AND ble LIKE ?");

//y ahora esos queries los usamos pasando solamente los valores
value rows3 = blaMayorQue({ 5 });
value rows4 = bleIgualQue({ "X" });
value rows5 = blaMenorQueYBleComo({ 10, "*ALGO*" });

En invocaciones indirectas, los parámetros secuenciados deben pasarse como un solo argumento de tipo secuencia, es por eso que hay llaves adicionales en las últimas llamadas.

Lo que sigue

Hasta aquí es lo que se tiene implementado en cuanto a funciones, que será incluido en el siguiente release. Pero todavía quedan varias cosas por hacer.

Actualmente hay algunas limitaciones: las funciones anónimas son de una sola expresión, y sólo se pueden usar argumentos nombrados en la primera invocación de una serie de invocaciones concatenadas (cuando la invocación a una función devuelve otra función que es invocada de inmediato). Estas limitaciones son temporales, lo más probable es que se implemente lo necesario para la versión 1.0 o incluso después, ya que hay todavía bastantes otras cosas que implementar como comprehensiones, que ya no debe ser un gran problema ahora que se tiene lo necesario en cuanto a funciones de orden superior para continuar.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de bferro

Buenos avances

Buenos avances. Mis comentarios:

  • Invocar a un Callable de manera directa es un buen feature.
  • No me queda claro eso de que las funciones anónimas están limitadas a un sola expresión y por eso no puede contener varias sentencias. ¿Un bloque no es una expresión?
  • Cuando dices que mediante múltiples listas de argumentos podemos crear funciones parciales, me imagino que lo que quieres decir es aplicación parcial de una función que es algo diferente que las funciones parciales.

Ceylon avanza, que bueno.

@Bferro La limitante entiendo

@Bferro La limitante entiendo es por el estado en el que va la implementación más que a una definición del lenguaje.