25 octubre 2008

Una de ruby: cuidadín con ||= (o cómo destruir una central nuclear sin querer)

El operador ||= de ruby, también conocido como operador de asignación condicional (conditional assignment) se suele utilizar para inicializar una variable a un valor por defecto si esta es nil o false.

Una muestra de cómo se puede liar parda si este operador no se usa correctamente:
def modificar_central_nuclear(accion, opciones={})
opciones[:avisar_supervisor] ||= false
opciones[:efectuar_operacion] ||= true
puts opciones[:avisar_supervisor] ? "Se avisa supervisor de #{accion}" : "No se avisa supervisor de #{accion}"
puts opciones[:efectuar_operacion] ? "Se realiza accion #{accion}" : "No se realiza accion #{accion}"
end
  • Esta función realiza una acción en una central nuclear recién inaugurada. Se establece como regla de negocio que siempre que se efectúe una operación, se debe avisar a un supervisor. Si no se realiza no hay que avisarlo (por ejemplo, cuando se hacen pruebas no hay que molestarle). En términos de código esto significa que en el hash opciones, los valores de :avisar_supervisor y :efectuar_operacion tendrán siempre el mismo valor en el momento de llamar a la función.
  • También se indica que, como caso especial, si se llama a esta función sin opciones, hay que efectuar la acción pero no hay que avisar al operador.
Veamos primero este último punto:
>> modificar_central_nuclear(:subir_temperatura_core)
No se avisa supervisor de subir_temperatura_core
Se realiza accion subir_temperatura_core
=> nil
Funciona bien. Ahora veamos el primer punto (suponemos q desde fuera de este código se cumple que se indican siempre las dos opciones y que estas tienen el mismo valor):
>> modificar_central_nuclear(:subir_temperatura_core, {:efectuar_operacion => true, :avisar_supervisor => true})
Se avisa supervisor de subir_temperatura_core
Se realiza accion subir_temperatura_core
=> nil
Perfecto, somos unos cracks! Y ahora hacemos lo mismo pero no queremos bajar la temperatura ni avisar al operador (estamos probando que se crean logs recién introducidos p.ej):
>> modificar_central_nuclear(:subir_temperatura_core, {:efectuar_operacion => false, :avisar_supervisor => false})
No se avisa supervisor de subir_temperatura_core
Se realiza accion subir_temperatura_core
=> nil
Ostras!! Parda habemus liada! Hemos subido la temperatura del core y encima sin avisar al supervisor... Evidentemente lo que esperábamos era que no se subiera la temperatura y que no se avisara al supervisor...

Veamos por qué pasa esto:
x ||= 'valor por defecto'
La sentencia anterior asignará el valor 'valor por defecto' a la variable x si:
  1. La variable no existe (la crea y le asigna 'valor por defecto')
  2. La variable existe y su valor anterior es nil
  3. La variable existe y su valor anterior es false
El tercer punto es el motivo por el cual "falla" el uso que hacemos de ||= con booleanos. Nosotros inicializamos opciones[:efectuar_operacion] con el valor false antes de ejecutar la función, pero una vez dentro de esta, el operador condicional por la regla 3 considera que no está inicializada (existe la variable pero su valor anterior es false) y le da el valor a la derecha del ||= (o sea, true).

Resumiendo: nunca de los jamases usar ||= con booleanos, especialmente si el valor anterior puede ser false. En el caso que nos ocupa es preferible hacer esto:
opciones[:efectuar_operacion] = true if opciones[:efectuar_operacion].nil?
El código final quedaría:

def modificar_central_nuclear(accion, opciones={})
opciones[:avisar_supervisor] = false if opciones[:avisar_supervisor].nil?
opciones[:efectuar_operacion] = true if opciones[:efectuar_operacion].nil?
puts opciones[:avisar_supervisor] ? "Se avisa supervisor de #{accion}" : "No se avisa supervisor de #{accion}"
puts opciones[:efectuar_operacion] ? "Se realiza accion #{accion}" : "No se realiza accion #{accion}"
end
Y ahora sí que obtenemos el resultado esperado:

>>modificar_central_nuclear(:subir_temperatura_core, {:efectuar_operacion => false, :avisar_supervisor => false})
No se avisa supervisor de subir_temperatura_core
No se realiza accion subir_temperatura_core
=> nil