¿Sabes qué retorna esta expresión en Ruby?

Hace algunos días Google subió una actualización de ChromeOS que por culpa de un solo caracter bloqueó los dispositivos. La línea culpable es la siguiente:

Con este cambio arreglaron el problema ¿Notas la diferencia?

Un & en vez de un && dejó tremenda embarrada. Los operadores bitwise (como &) son bastante tricky por lo que es fácil confundirse. Comentando la noticia con mis compañeros, recordé haber leído -o quizás escuchado- que justamente evitar este tipo de errores era una de las razones de por qué en Python se usa and y or en vez de && y || como en otros lenguajes. En Ruby también existe and y or, pero no se ocupa mucho. En este post explicaré por qué, pero antes...

Tomé inspiración del post de Diego (si no lo has leído te recomiendo que lo hagas antes de continuar con este) y te propongo un desafío similar. Esta vez en Ruby:

¿Qué valor se le asigna a la variable misterio?

misterio = "" && "Hola"

‌‌Si te leíste el post anterior entonces probablemente tu respuesta sea algo como “esto ya lo vimos, se asigna "", facilito...”. Sin embargo la respuesta correcta es "Hola".

¿Qué está pasando aquí?

En Ruby, a diferencia de JavaScript (y de muchos lenguajes) el string vacío es truthy. En caso que se te haya olvidado, en expresiones booleanas o condiciones algo truthy (¿verdadoso?) se considera como true y algo falsy (¿falsedoso?) se considera como false. Esto no significa que sean exactamente los booleanos true/false, solo que ante evaluaciones lógicas se comportan de esa manera.

Volviendo al tema, en Ruby un string vacío es truthy. Esto confunde a veces a personas que están recién aprendiendo este lenguaje viniendo de otros lenguajes de programación, por lo que es bueno conocer este tipo de comportamientos.

En la misma línea, el número 0 también es un valor truthy en Ruby. Esto quiere decir que si haces algo como esto:

counter = 2
while counter do
  # algo que quieres que se repita 2 veces
  counter -= 1
end

¡Entrarás en un loop infinito!

Muchos programadores están tan acostumbrados a que cosas como "" o 0 sean falsy, que este comportamiento les parece raro o antinatural. En mi opinión es más por costumbre que por haberlo pensado realmente. ¿Qué tienen de especial el string vacío o el 0 para ser considerados diferentes que cualquier otro string o cualquier otro número?

Ruby, al decidir que los considerará igual obtiene dos ventajas: una para el intérprete y otra para el programador. Para el intérprete, se facilita el trabajo porque le basta saber la clase de una variable para considerarla truthy, mientras que en otros lenguajes se tiene que hacer un caso especial para esto y revisar el valor en concreto que está tomando. Sin embargo, en mi opinión el beneficio más importante es para el programador, porque tiene que pensar mejor qué es lo que quiere evaluar. Si quiero que la condición sea “que el string tenga un largo igual a 0” entonces es mejor que quede explícito (string_variable.length == 0), o mejor aún si podemos aprovechar los métodos que Ruby nos entrega para que nuestro código quede más legible (string_variable.empty?).

Este tipo de filosofía y sintaxis hace que leer código en Ruby sea mucho más fácil, sin embargo hay que tener cuidado con algunos detalles. Te tengo un segundo desafío: ¿Qué se asigna a la variable misterio ahora?

misterio = "" and "Hola"

Si tu reacción es “duh, acabamos de ver que es "Hola"” déjame decirte que...

‌‌La respuesta correcta en este caso es "", sin embargo lo que retorna la expresión es "Hola". ¿Pero cómo, si "" es truthy? ¿Cuál es la diferencia entre && y and?

Si bien ambos operadores usan short circuit evaluation, la diferencia entre ambos es su precedencia. En Ruby, el and (y el or) tiene una precedencia muy baja, por lo que se ejecuta primero la asignación y luego el and, es decir que al escribir misterio = "" and "Hola" lo que está ocurriendo es equivalente a esta expresión: (misterio = "") && "Hola", es decir que misterio queda con el valor "" mientras que la parte and "Hola" ocurre después de la asignación por lo que no tiene efecto sobre el valor de la variable.‌‌

Otro ejemplo, si escribimos algo como:‌‌

puts 0 > 1 or 1 > 0

Esto imprime false pero retorna true.

Mientras que si lo hacemos así:

puts 0 > 1 || 1 > 0

Esto imprime true pero retorna nil                        

‌‌Veamos con calma lo que acaba de pasar. De igual manera que en el ejemplo de la asignación, en el primer caso se ejecuta primero (puts 0 > 1) lo que imprime false y retorna nil puesto que el método puts en Ruby retorna nil. Luego hace nil or 1 > 0 lo que retorna true. En el segundo caso se evalúa la expresión completa primero 0 > 1 || 1 > 0 entregando true, lo que se le pasa al puts el cual lo imprime (y luego retorna nil).

Como es muy fácil que esto produzca confusión y errores, en general se evita el uso de and y or como operador lógico en favor de && y || los cuales tienen un comportamiento más fácil de predecir.

A veces uno se topa con el operador and cumpliendo una función de control de flujo.

Por ejemplo uno puede hacer cosas como esta:

def es_par?(n)
  n % 2 == 0 and return "#{n} es par"
  
  "#{n} es impar"
end

Donde si luego llamamos por ejemplo es_par? 4 nos responde con lo esperable: "4 es par" mientras que si llamamos con un número impar, por ejemplo es_par? 3 la función retorna "3 es impar".

Sin ir más lejos, el and como control de flujo se usa de ejemplo para evitar problemas de doble render en un controlador de Ruby on Rails:

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show" and return
  end
  render action: "regular_show"
end

Sin embargo salvo casos específicos como el de arriba no se recomienda el uso de and ni or incluso para control de flujo, ya que por lo general resulta más entendible usar if o unless. Por ejemplo, la función anterior queda mucho más entendible si la escribimos como:‌‌‌‌

def es_par?(n)
  return "#{n} es par" if n % 2 == 0
  
  "#{n} es impar"
end

‌‌En resumen lo que me gustaría que te lleves de este post son dos cosas. La primera es que en Ruby los únicos valores falsy son nil y false, mientras que los otros objetos son todos truthy (incluidos valores como 0 o ""). La segunda es que evites usar and/or y que en su lugar uses &&/|| para operaciones booleanas e if/unless para control de flujo con el fin de evitar posibles errores, comportamientos poco intuitivos y más de un dolor de cabeza.