martes, 19 de febrero de 2013

Compare, compare (1 de 2)


El objetivo

Resuelto en el capítulo anterior el tema de los atributos, fijémonos ahora en los nombres de las etiquetas. Como ya se dijo, a HTML no le preocupa demasiado eso de las diferencias entre mayúsculas y minúsculas.

Para tenerlo en cuenta, vamos a crear ahora un subtipo de String que redefina los métodos de comparación de acuerdo con nuestros intereses. Pero... ¿cuáles son estos métodos? Y... ¿cómo funcionan?

Buenas preguntas. Porque el enfoque de Ruby es cuando menos... peculiar. Empezando porque una cosa es comprobar si dos objetos son iguales y otra determinar si son distintos.

Comencemos por el último caso.

Comparaciones de desigualdad

Las comprobaciones de desigualdad nos indican si un objeto es, o no, mayor o menor que otro. Y para realizarlas tenemos los conocidos operadores “>”, “>=”,  “<” y “<=”.

En Ruby, estos operadores se implementan como métodos de las clases sobre las que se realizan las comprobaciones. Y, por fortuna, el comportamiento de todos ellos  para una determinada clase se puede modificar redefiniendo un único método denominado “<=>”. De hecho, se pueden realizar comparaciones directamente con él, como en las siguientes pruebas con irb:

irb(main):001:0> 23 <=> 15
=> 1
irb(main):002:0> 23 <=> 1333
=> -1
irb(main):003:0> 23 <=> 23
=> 0
irb(main):004:0>


Si “<=>” retorna un valor positivo, eso quiere decir que el primer operando comparado es mayor que el segundo. Si es negativo, el mayor es el segundo. Y si son iguales, retorna cero.

Por ahora, la cosa va siendo sencillita. Pero tengo una mala noticia: al redefinir “<=>” sólo se modifica el comportamiento de las comparaciones de desigualdad. Las comparaciones de igualdad van por otro camino.

Y es un camino lleno de curvas y baches...

¿Iguales?

Si para las cuatro comparaciones de desigualdad basta con un método, para la de igualdad hay no menos de cuatro métodos. Cosas de la vida. Unos tanto y otros tan poco. Pero vayamos por partes.

El primer método es “==”. Como en:

irb(main):006:0> 23 == 23
=> true


Como se indica en la documentación de Ruby, cuando se trata la clase Object, en principio, “==” retorna un valor cierto si y solo si los dos objetos comparados son el mismo. Éste es el comportamiento por defecto de todos los operadores de comparación de igualdad a este nivel.

Lo que ocurre es que, después, cuando se definen clases concretas, lo normal es que algunos de estos métodos se redefinan para darle un significado más concreto y acorde con la clase concreta. Así, para números y cadenas, “==” retorna cierto si y solo sí los valores comparados son iguales, aunque se trate de objetos distintos.

Otro operador de comparación es “===” (tres signos "=" seguidos). Con él pasa lo mismo que con “==”. La principal diferencia es que “===” se usa en las comparaciones en las sentencias “case” (similares a las “switch” o las “case” de otros lenguajes de programación).

Y hay más: “eql?” tiene como objetivo comprobar si los valores representados por dos objetos son iguales o no. Y, aunque no sea demasiado frecuente, puede estar redefinida de forma distinta a “==”. Así ocurre en la clase Fixnum cuando la comparación se realiza con un número en coma flotante (clase Float): mientras “==” realiza las conversiones de tipo, “eql?” no lo hace, con lo que a veces los resultados de este último método son poco “intuitivos”:

irb(main):008:0> 2 == 2.0
=> true
irb(main):009:0> 2.eql?(2.0)
=> false


Sí, en el último caso, 2 no es lo mismo que 2.0 porque uno es un número entero y el otro uno en coma flotante.

Y, para acabar, está “equal?”. Este método no debería nunca ser redefinido y retorna un valor cierto sí y solo sí los dos objetos comparados son el mismo.

Para más detalles, puede consultarse:
http://ruby-doc.org/core-1.9.3/Object.html

Donde lo de 1.9.3 es la versión de Ruby sobre la que se consulta la documentación. Otras versiones tendrán otras URLs.

La clase vacía

Dicho todo esto, vamos a empezar a crear nuestra nueva clase. En ella, los métodos “==”, “===” y “eql?” se comportarán todos de la misma forma: comparando los valores de dos cadenas sin tener en cuenta mayúsculas y minúsculas.

El esqueleto de la clase sería
# Cadena con comparaciones no sensibles a mayúsculas y minúsculas
class Nocase < String
 # Compara dos valores
 def <=>(x)
 end

 # Comparar igualdades
 def ==(x)
 end
 
 alias_method :===, :==
 alias_method :eql?, :==
end



Aliases

La principal novedad está al final de la clase:

 alias_method :===, :==
 alias_method :eql?, :==


Con alias_method estamos creando un nuevo método que hace exactamente lo mismo que otro. Así, si nos fijamos en la primera de las dos líneas anteriores, para Nocase (y para sus subclases, en tanto no redefinan demasiadas cosas), el método “===” se comportará exactamente igual que “==”. Dos nombres distintos para la misma cosa.

A efectos prácticos, alias_method nos ahorra aquí el tener que repetir el mismo código varias veces. De ese modo, reducimos el riesgo de equivocarnos o de mantener incorrectamente la especificación de la clase. Y nos ahorramos de escribir, que para mí eso también es un aliciente.

Fíjate también: los nombres de los métodos se indican mediante Symbols. Eso suele ser frecuente, así que no lo pierdas de vista.

A lo que nos interesa

¡Vamos a rellenar esos métodos! Comencemos por “<=>”. Ya existe uno para la clase String, pero queremos que el nuestro no tenga en cuenta los “tamaños” de las letras. Si fuera una comparación de cadenas, y si nos atenemos a lo que ya sabemos, nos valdría con algo como:
cadena1.downcase <=> cadena2.downcase

Pero no podemos definir el método “<=>” de Nocase en función de sí mismo. Eso produciría una serie infinita de llamadas recursivas. Bueno, infinita no. Sólo hasta que nos carguemos la (algo reducida) pila que el entorno de ejecución de Ruby proporciona. Y, entonces, nuestro programa cascaría con un mensaje de error.

Afortunadamente, sí que podemos hacer uso del método “<=>” de la clase String.  Porque podemos convertir nuestro objeto Nocase en una cadena de las normalitas utilizando el método “to_s”. Así que podría escribirse:

 def <=>(x)
  return self.to_s.downcase <=> x.downcase
 end


Habría muchas otras formas de conseguir este mismo resultado. Lo bueno de usar programación orientada a objetos es que lo que importa son los resultados. Eso hace que podamos cambiar cómo funciona un método o una clase por dentro sin que nada se rompa, siempre y cuando los valores retornados y las acciones realizadas sean las mismas.

Una alternativa que funciona por lo general mucho más rápido que la anterior es usar el método “casecmp”, definido para la clase String. Y que realiza, mira por donde, la misma función que “<=>” pero sin distinguir mayúsculas de minúsculas:

irb(main):013:0> 'ABCDE'.casecmp('abcda')
=> 1


¡Justo lo que queríamos! Con ello nos podría quedar una definición de “<=>” muy sencillita:
 alias_method :<=>, :casecmp

Quédate con la implementación que más te guste. La que usa “downcase” es más lenta. La que establece “<=>” como un alias de “casecmp” podría tener problemas de recursividad si alguna vez “casecmp” estuviera definida a nivel interno usando “<=>”.

Yo, por ahora, me quedo con la segunda. Y mi clase queda así:

# Cadena con comparaciones no sensibles a mayúsculas y minúsculas
class Nocase < String
 # Compara dos valores
 alias_method :<=>, :casecmp


 # Comparar igualdades
 def ==(x)
  (casecmp(x) == 0)

 end
 
 alias_method :===, :==
 alias_method :eql?, :==
end



No hay comentarios:

Publicar un comentario