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  ; una invocación (a función o método) es con paréntesis, si van a usar argumentos posicionales:   o  ; 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:  .

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  , pueden abreviarlo como  . Y en vez de hacer  , pueden escribir  .

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:

 

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   (similar al comportamiento en C#):   es lo mismo que  . Para la igualdad referencial, o identidad, es decir, saber si a y b hacen referencia al mismo objeto, se usa el triple igual:   (hay una función en Ceylon llamada   que devuelve   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   (es un tipo algebraico; en Ceylon no hay   porque realmente no se necesitan). Los objetos que implementan la interfaz  , deben implementar el método  , pero para invocarlo se puede usar el operador  , de modo que   es lo mismo que  .

Por supuesto se tiene menor, mayor, menor/igual, mayor/igual y estos realmente también se traducen a llamadas a  , de modo que estos operadores sólo se pueden aplicar a objetos de tipo  . Existen tres objetos globales en Ceylon, llamados  ,   y  , de tipo  , que son el resultado de  . Entonces,   es lo mismo que escribir   mientras que   es lo mismo que  .

Contención

En Ceylon existe una interfaz  , que define un método  . Pero en vez de   se puede escribir  . Algunos tipos que implementan   son   (la base para las colecciones) y  , de modo que puede escribirse  .

Nulos

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

 

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:

 

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:

 

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   nos devuelve una colección con tres elementos, que son el resultado de haber invocado   en cada uno de los elementos de  .

Creación de objetos

En Ceylon no existe la palabra reservada  , 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   y  , que tienen estos constructores:

 

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

 

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

 

Bits, slots

Ceylon toma la sintaxis de los operadores de bits estilo C, para poderlos aplicar a otros tipos que implementen la interfaz  . 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  , se pueden hacer cosas interesantes con ellos. Por ejemplo, la clase  , 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   con sus variantes:

 

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   a String; sigue siendo  .

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   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   y se quiere iterar sólo si es mayor a 0, entonces   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   y   (aunque esto último permite algunos abusos que derivan en sintaxis confusa como cuando se usa   para agregar un elemento a una lista, o   para imprimir una cadena en un stream de salida, etc).

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  , como la firma de item es  , esa llamada devolverá un  . 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:   (y por tanto el retorno de esa expresión es de tipo  .

Cuando haces invocaciones así, o cuando obtienes propiedades (por ejemplo  ), 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   define el método  ; si tu clase implementa Summable, entonces puedes hacer esto con dos instancias de tu clase:  .

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   con dos instancias de cualquier clase, y en tiempo de ejecución verá si   implementa el método   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

 

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:   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  , 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:

 

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.

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:

 

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:

 

Estas son todas invocaciones válidas:

 

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:   y es similar a que en groovy hicieras   o en Scala hicieras  , 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   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   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:

 

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.
 

And call it like this:
 
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  ; 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.