style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Operadores en Ceylon

El capítulo 6 de la especificación de Ceylon trata de los operadores. Recientemente he estado implementando algunos de estos para el compilador de Ceylon a Javascript y me doy cuenta de que realmente son bastante convenientes, y pues quiero compartir algunas de sus peculiaridades, ya que algunos caben dentro de lo que muchos llaman "azúcar sintáctica".

Los más básicos no son nada extraños; por ejemplo para obtener un miembro de un objeto, simplemente es objeto.miembro; una invocación (a función o método) es con paréntesis, si van a usar argumentos posicionales: f() o f(a,b,c); pero si van a usar argumentos con nombre (cosa que se puede hacer cuando la definición de la función o método tiene argumentos con valores por default y simplemente se los quieren saltar), entonces usan llaves: f{a=1;b="2";}.

La asignación a variables (ojo: variables, no valores inmutables) es como en Pascal, con := pero hay un par de variantes interesantes: en vez de hacer var:=var.f, pueden abreviarlo como var.=f. Y en vez de hacer var:=var.f(x), pueden escribir var.=f(x).

Operadores Aritméticos

Estos todos los conocemos: +, -, / *, % (módulo), y además tenemos ** (potencia), y para el caso de variables, tenemos +=, -=, /=, *= y %= (aquí se sustituye el : del := por el operador correspondiente).

Operadores lógicos

Obviamente existen operadores para manejar lógica booleana: || (OR), && (AND), y el operador unario ! (NOT). Para variables booleanas existen &&= y ||=.

Es importante hacer notar que en Ceylon, el NOT lógico tiene una precedencia muy baja, por lo tanto es una expresión como esta:

!x.y*2 == 0.0

El NOT se evalúa hasta el final de todos esos operadores, incluso después del ==.

Igualdad

El operador == en Ceylon es equivalente a invocar el método equals (similar al comportamiento en C#): a==b es lo mismo que a.equals(b). Para la igualdad referencial, o identidad, es decir, saber si a y b hacen referencia al mismo objeto, se usa el triple igual: a===b (hay una función en Ceylon llamada identical que devuelve true si sus dos argumentos apuntan al mismo objeto).

Comparaciones

Hay varias maneras de comparar dos objetos en Ceylon, dependiendo de lo que se necesita. El resultado de una comparación es un objeto de tipo Comparison (es un tipo algebraico; en Ceylon no hay enum porque realmente no se necesitan). Los objetos que implementan la interfaz Comparable, deben implementar el método compare, pero para invocarlo se puede usar el operador <=>, de modo que a<=>b es lo mismo que a.compare(b).

Por supuesto se tiene menor, mayor, menor/igual, mayor/igual y estos realmente también se traducen a llamadas a compare, de modo que estos operadores sólo se pueden aplicar a objetos de tipo Comparable. Existen tres objetos globales en Ceylon, llamados smaller, equal y larger, de tipo Comparison, que son el resultado de Comparable.compare. Entonces, a<b es lo mismo que escribir a.compare(b)==smaller mientras que a<=b es lo mismo que a.compare(b)!=larger.

Contención

En Ceylon existe una interfaz Category, que define un método contains. Pero en vez de a.contains(b) se puede escribir b in a. Algunos tipos que implementan Category son Sequence (la base para las colecciones) y String, de modo que puede escribirse "ava" in "javaMexico".

Nulos

Como mencioné en un post anterior, null en Ceylon es un valor especial de tipo Nothing. Con la unión de tipos, se pueden tener los llamados tipos opcionales, cuando se une cualquier tipo con Nothing. Hay sintaxis para esto: String|Nothing es lo mismo que String?. Y para tratar con estos tipos opcionales hay varios operadores:

String? x = null;
if (exists x) { //se cumple si x != null
  Integer largo = x.size; //Aquí dentro, x es tipo String
}
Integer? largo = x?.size; //Si existe x, se invoca size; si no, es null
                          //por eso 'largo' debe ser opcional
Integer largo2 = x?.size ? -1; //si la primera expresión x?.size es null,
                                //se devuelve la segunda.
Integer largo3 = x?.size else -1;
Boolean? comienza = x?.startsWith("A"); //Invocación condicional
Boolean comienza = x?.startsWith("A") ? false;

Correspondencia/Secuencias

Hay operadores para secuencias y correspondencias. Una secuencia es simplemente una colección de objetos en un orden arbitrario (el orden en que fue creada la secuencia). Una correspondencia es una colección donde cada elemento corresponde a un índice (un objeto que implementa Equality). Los operadores más básicos son:

seq[indice]; //Equivalente a seq.item(indice);
seq?[indice]; //Si existe seq se obtiene el elemento, de lo contrario es null.
seq[del..al]; //Equivalente a seq.span(del, al)
seq[del...]; //Equivalente a seq.span(del), toma a partir del indice indicado hasta el útimo elemento

Aparte de estos, tenemos operadores spread, que realmente hacen la invocación indicada sobre cada elemento de la colección, y se devuelve una colección con el resultado de cada invocación:

seq[].x; //Devuelve una nueva colección con el resultado de obtener "x" de cada elemento

Si este último operador se ejecuta poniendo el nombre de un método, se devolverá una referencia al método en cada elemento, como una función, para poder invocarlo posteriormente. Pero si se pone directamente una invocación, se devolverá el resultado de ejecutar la invocación en cada elemento, de modo que seq[].x(a,b,c) nos devuelve una colección con tres elementos, que son el resultado de haber invocado x(a,b,c) en cada uno de los elementos de seq.

Creación de objetos

En Ceylon no existe la palabra reservada new, como en Java; para crear un objeto, se invoca directamente el nombre de la clase y se le pasan los parámetros al constructor de la misma. Sin embargo, hay un par de casos en que se puede construir un objeto utilizando una sintaxis diferente; estamos hablando de las clases Range y Entry, que tienen estos constructores:

value rango = 1..5; //Equivalente a escribir Range(1,5)
value entrada = 1 -> "A"; //Equivalente a escribir Entry(1, "A")

Esta sintaxis para creación de rangos nos permite simplificar algunas iteraciones, por ejemplo:

for (i in 1..10) { print(i); }

Y la sintaxis para las entradas nos puede facilitar la creación de mapas:

value mapa = HashMap("a"->1, "b"->2, "c"->3);

Bits, slots

Ceylon toma la sintaxis de los operadores de bits estilo C, para poderlos aplicar a otros tipos que implementen la interfaz Slots. Dichos operadores son:

~ (tilde), para negación o complemento (tiene versión unaria y binaria).
| (pipe), para el OR.
& (ampersand) para el AND.
^ (caret) para el XOR.

Y para variables por supuesto están |=, &=, ^= y ~=.

Dado que estos operadores se pueden aplicar a cualquier objeto que implemente Slots, se pueden hacer cosas interesantes con ellos. Por ejemplo, la clase Set, para manejar conjuntos de elementos únicos, puede implementar estos métodos para hacer cosas como unión e intersección de conjuntos, complemento de un conjunto con otro, etc.

Operadores condicionales

Por último, tenemos los operadores condicionales, que nos permiten emular el operador ternario condicion?entonces:de_lo_contrario con sus variantes:

Boolean condicion = true;
String? cadena = condicion then "cadena"; //Equivale a if (condicion) "cadena" else null,
                                          //Similar a condicion?"cadena":null
print(cadena else "no hay cadena"); //Equivale a if (exists cadena) cadena else "no hay cadena",
                                    //similar al "operador Elvis", cadena?:"no hay cadena"
//Combinados
print(exists cadena then "Si hay cadena" else "No hay cadena");

Para este último caso de usar then/else, en el then se evalúa una expresión pero no se hace refinando el tipo de cadena a String; sigue siendo String?.

Pues por el momento estos son todos los operadores en Ceylon. Hay que tomar en cuenta que podrían surgir algunos más en el futuro, pero no son muchos; uno que está en discusión es un operador para definir rangos vacíos, ya que 0..0 da un rango de un solo elemento, pero no hay uno para definir una lista vacía (el ejemplo es que si se tiene un valor fin y se quiere iterar sólo si es mayor a 0, entonces for (i in 0..fin) no funciona porque itera una vez al menos). Otros son los operadores para recorrer bits, que funcionarían sobre enteros, o tal vez haya una interfaz que defina los métodos shiftLeft y shiftRight (aunque esto último permite algunos abusos que derivan en sintaxis confusa como cuando se usa lista << elemento para agregar un elemento a una lista, o output << "hola" para imprimir una cadena en un stream de salida, etc).

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 benek

Que bien, el único que me

Que bien, el único que me causa "ruido" es el operador opcional '?', que algunos otros lenguajes están tomando como null-safe, pero si te quitas un poco eso de la mente hace mucho sentido.

Me gustó bastante el spread sobre colecciones, principalmente la parte de que al invocar el nombre de un método te devuelva una referencia a la función en cada elemento, que después puedes ocupar, y si quieres que sea una llamada al método en cada elemento la hagas como tal, eso se me hizo muy fino.

Por cierto, recuerdo que Gavin mencionó que habrá sobrecarga de operadores en Ceylon, pero de manera controlada, no tan abierta como en otros lenguajes por el problema que implicaría tener varias bibliotecas y/o frameworks que implementen sobrecarga "de chile y de mole". ¿Ya hay algo de eso?

Imagen de ezamudio

Opcional

El opcional es un poquito como el "Elvis" de Groovy. Si la expresión a la izquierda del operador existe, la devuelve; de lo contrario, devuelve la del lado derecho (que por supuesto, debe existir, eso lo revisa el compilador). El nullsafe también está, es la interrogación con el punto.

El spread para referencias a métodos realmente devuelve un Callable cuyo tipo de retorno es una colección del tipo de retorno del método al que hiciste referencia, y con los mismos parámetros. Por ejemplo si tienes una lista de cadenas, o sea String[], entonces al invocar por ejemplo seq[].item, como la firma de item es Character item(Integer), esa llamada devolverá un Callable<Character[], Integer>. Si lo invocas ahí mismo realmente lo que pasa es que ya tienes la referencia a ese Callable y lo estás invocando de inmediato: seq[].item(0) (y por tanto el retorno de esa expresión es de tipo Character[].

Cuando haces invocaciones así, o cuando obtienes propiedades (por ejemplo seq[].uppercased), funciona similar al operador spread de Groovy. Desconozco si en Groovy puedes obtener la referencia a algo como el Callable que te devuelve Ceylon, pero lo que no me gusta es la sintaxis, eso del * como que nomás no me gusta, desde un punto de vista estético y además como que los corchetes son algo que ya un programador relaciona con colecciones; pienso que tiene más sentido por eso que corchetes y punto sean el spread.

Imagen de ezamudio

Sobrecarga

En cuando a la sobrecarga de operadores, realmente lo que tiene Ceylon es polimorfismo de operadores; es decir, hay operadores que se pueden usar como azúcar sintáctica para llamar algunos métodos de interfaces. Por ejemplo, la interfaz Summable define el método plus; si tu clase implementa Summable, entonces puedes hacer esto con dos instancias de tu clase: a+b.

Esto te permite usar algunos operadores que son muy bien conocidos, como el +, pero siempre y cuando tu objeto implemente la interfaz correspondiente. Es similar a lo que hace Groovy, pero Groovy al ser dinámico, permite que hagas a+b con dos instancias de cualquier clase, y en tiempo de ejecución verá si a implementa el método plus o no. Ceylon por ser de tipado estático, podrá resolver eso en tiempo de compilación.

Lo mismo con la interfaz Slots, pero me acaban de decir que por el momento ya la quitaron del módulo del lenguaje, y los operadores que normalmente son de bits, en Ceylon se usan para hacer operaciones con Sets: unión, intersección, diferencia. Como mencioné al final, es algo que está cambiando aún.

Entonces ¿ la sobrecarga

Entonces ¿ la sobrecarga estará limitada a algunos operadores pero no a todos? Supongo que entonces no se pueden crear nuevos operadores.

Tienes algún otro ejemplo de

 var := var.f  
 var.=f

La invocación con corchetes para métodos com parámetros nombrados se me hace rara, sería interesante saber la historia detrás de ello. No parece haber ninguna dificultad evidente con escribir: f(a=1, b="22); al menos claro que esto signifique otra cosa en Ceylon.

Muy interesante todo esto. En espera de más info.

:)

Me extendí más en mi comentario y mejor cree un post nuevo pero concluyo:

Me parece muy bueno que Ceylon tome un camino pragmático en cuanto a los operadores y la sobrecarga de los mismos. Así se busca entonces alcanzar el balance entre flexibilidad y legibilidad.

Imagen de bferro

¿Comparison es un tipo algebraico?

Sería bueno que comentaras eso. No me queda claro.

Imagen de bferro

Razones para cambiar la precedencia de los operadores lógicos

En el álgebra de Boole, las tres operaciones fundamentales son NOT, AND, OR con las siguientes precedencias:
NOT High
AND Medium
Or Low

Me imagino que debe existir alguna razón para cambiar esa precedencia en Ceylon. Si alguien lo sabe sería bueno comentarlo.

Imagen de ezamudio

No hay sobrecarga

No hay sobrecarga de operadores en Ceylon; es polimorfismo de operadores. No, no puedes crear nuevos operadores. Lo que puedes hacer es implementar algunas interfaces, el ejemplo mas simple es Summable, que define el metodo plus, y entonces puedes usar el + como operador instancias de tu clase. Similar a lo que hace Groovy, pero formalizado.

El operador .= parece que va a desaparecer, aunque sigue en la spec.

Imagen de ezamudio

Comparison

Comparison es en efecto un tipo algebraico. Al dia de hoy esta definido asi:

shared abstract class Comparison(String name)
    of larger | smaller | equal {
  shared actual String string {
    return name;
  }
}
shared object equal extends Comparison("equal") {}
shared object smaller extends Comparison("smaller") {}
shared object larger extends Comparison("larger") {}

Solamente puede haber esas tres instancias de esta clase.

En cuanto a la precedencia del NOT, sigue siendo como la mencionas en relacion a AND y OR: primero NOT, luego AND y al final OR. Pero estos operadores se evaluan DESPUES de exists, comparaciones, is, satisfies, igualdad y los operadores aritmeticos; lo unico que hay mas abajo de los logicos es then/else y los de asignacion.

http://ceylon-lang.org/documentation/spec/html/expressions.html#operator...

Imagen de ezamudio

Invocaciones con llaves

EDIT: había varias cosas que mencionar al respecto, así que saqué esto de su comentario original y mejor lo puse aquí.

La idea detrás de usar llaves para los parámetros con nombre cobra sentido cuando la esparces en varias lineas:

value instancia = MiClase(bla, ble);
//Sintaxis de constructor
MiClase instancia {
  parametro1 = bla;
  parametro2 = ble;
} //no lleva punto y coma aquí

value resultado = miFuncion {
  parametro2 = ble;
  parametro1 = bla;
}; //este sí porque es invocación normal

Y cuando tienes métodos o funciones con parámetros por default, esta sintaxis de corchetes te sirve para indicar solamente los parámetros que quieres enviar. Por ejemplo si tienes esta definición de una función:

void f1(String a, String b="B", String c="C") {}

Estas son todas invocaciones válidas:

f1("A");
f1("X", "Y");
f1("X", "Y", "Z");
//pero si quieres pasar solamente a y b, dejando b con el default, entonces
f1{a="X"; c="Z";};
//O en varias lineas si prefieres
f1{
  a="X";
  c="Z";
};

Operadores spread

seq[].x(a,b,c) nos devuelve una colección con tres elementos, que son el resultado de haber invocado x(a,b,c) en cada uno de los elementos de seq.
¿Eso es tener el famoso "map" de los lenguajes funcionales incorporado en el lenguaje o me parece a mi nomás?
Brillante. Cada vez me gusta mas Ceylon.

Imagen de ezamudio

es una variante simple

El spread te permite hacer cosas que normalmente haces con map, pero es más limitado. Las colecciones en Ceylon probablemente incorporarán funcionalidad similar a map, fold, filter, etc en el futuro cercano, aunque el roadmap incluye también comprehensiones, con lo que pueden salir sobrando esos métodos.

Pero ciertamente si en Ceylon tienes por ejemplo una lista de cadenas, puedes hacer esto: cadenas[].initial(1) y es similar a que en groovy hicieras cadenas.collect { it.substring(0,1) } o en Scala hicieras cadenas map {_ take 1}, es decir, al final tienes una lista con cadenas que son la primera letra de cada cadena de la lista original.

Hay dos diferencias importantes: Primero, en las colecciones que tienen map puedes pasar una función que haga cualquier cosa con un elemento y devuelva cualquier otra cosa; por ejemplo si tú haces una función que cuenta las vocales de una cadena, se la puedes pasar a map y entonces obtendrás al final una lista con el número de vocales que tiene cada cadena de la lista original, mientras que en Ceylon con el operador spread solamente puedes invocar un método que implementen todos los elementos de la lista (esto lo sabrá el compilador por el tipo de elementos que tenga declarada dicha lista). Y la otra diferencia es que en Ceylon puedes obtener una referencia a ese método ya esparcido, para poder invocarlo después:

value cadenas = {"Probando", "Primeras", "Palabras"};
value primeras = cadenas[].initial(1) ; //devuelve {"P", "P", "P"};
function iniciales(Integer cuantas) = cadenas[].initial; //Aquí definimos una función que devolverá N número de primeras letras de las cadenas en la lista original
value dos = iniciales(2); //Equivalente a invocar cadenas[].initial(2)
Callable<String[], Integer> ref = cadenas[].initial; //Aquí tenemos la referencia al método esparcido, sin invocarlo
//No podemos invocar ref pero lo podemos pasar a cualquier función o método que espere una función que reciba un String y devuelva una lista de cadenas
Imagen de bferro

Algo no me queda claro con Callable

Enrique, en Tour of Ceylon, se tiene el siguiente ejemplo:


Defining higher order functions

We now have enough machinery to be able to write higher order functions. For example, we could create a repeat() function that repeatedly executes a function.

void repeat(Integer times, Callable<Void,Integer> perform) {
    for (i in 1..times) {
        perform(i);
    }
}

And call it like this:

void printNum(Integer n) { print(n); }
repeat(10, printNum);

Which would print the numbers 1 to 10 to the console.


Que por supuesto no funciona: "receiving expression cannot be invoked: perform is not a method or class". ¿Qué hay con eso y la invocación directa a un Callable.
Imagen de ezamudio

buena pregunta...

creo que eso es un error en el blog y realmente la declaración de repeat debe ser void repeat(Integer times, void perform(Integer x)); es la bronca de haber creado el tour antes de tener varias cosas implementadas. Hace poco hablé con Gavin acerca de este rollo del Callable y también está de acuerdo en que sea solamente una referencia (y pues así es ahorita de facto; el typechecker lo maneja principalmente él y es ahí donde se genera ese error que indicas).

Lo más probable es que falta realmente actualizar el tour ahora que ya se tienen más cosas funcionando. De hecho la idea es integrar la funcionalidad de try.ceylon-lang.org en las páginas del tour, para que todos esos fragmentos de código se puedan editar y ejecutar.

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">