¿Mejores prácticas para globals en clojure,(refs vs alter-var-root)?
(2)
Me he encontrado usando el siguiente lenguaje últimamente en el código de clojure.
(def *some-global-var* (ref {}))
(defn get-global-var []
@*global-var*)
(defn update-global-var [val]
(dosync (ref-set *global-var* val)))
La mayoría de las veces, ni siquiera es un código de subprocesos múltiples que puede necesitar la semántica transaccional que los refs le brindan. Simplemente parece que las referencias son más que un código de hilos, pero básicamente para cualquier global que requiera inmutabilidad. ¿Hay una mejor práctica para esto? Podría intentar refactorizar el código para usar solo el enlace o dejarlo, pero eso puede ser particularmente complicado para algunas aplicaciones.
Siempre uso un átomo en lugar de una referencia cuando veo este tipo de patrón: si no necesita transacciones, solo una ubicación de almacenamiento mutable compartida, entonces los átomos parecen ser el camino a seguir.
Por ejemplo, para un mapa mutable de pares clave / valor, usaría:
(def state (atom {}))
(defn get-state [key]
(@state key))
(defn update-state [key val]
(swap! state assoc key val))
Sus funciones tienen efectos secundarios. Llamarlos dos veces con las mismas entradas puede dar diferentes valores de retorno dependiendo del valor actual de *some-global-var*
. Esto hace que las cosas sean difíciles de probar y razonar, especialmente cuando tienes más de una de estas variables globales flotando alrededor.
Las personas que llaman a sus funciones pueden incluso no saber que sus funciones dependen del valor de la var global, sin inspeccionar la fuente. ¿Y si se olvidan de inicializar la var global? Es fácil de olvidar. ¿Qué sucede si tiene dos conjuntos de códigos que intentan usar una biblioteca que se basa en estas vars globales? Probablemente vayan a pisarse unos a otros, a menos que uses el binding
. También agrega gastos generales cada vez que accede a datos de una referencia.
Si escribes tu código sin efectos secundarios, estos problemas desaparecen. Una función es independiente. Es fácil de probar: pásale algunas entradas, inspecciona las salidas, siempre serán las mismas. Es fácil ver de qué entradas depende una función: están todas en la lista de argumentos. Y ahora su código es seguro para subprocesos. Y probablemente corre más rápido.
Es difícil pensar en el código de esta manera si estás acostumbrado al estilo de programación "mutar un montón de objetos / memoria", pero una vez que aprendes a usarlo, resulta relativamente sencillo organizar tus programas de esta manera. Su código generalmente termina tan simple como o más simple que la versión de mutación global del mismo código.
Aquí hay un ejemplo altamente artificial:
(def *address-book* (ref {}))
(defn add [name addr]
(dosync (alter *address-book* assoc name addr)))
(defn report []
(doseq [[name addr] @*address-book*]
(println name ":" addr)))
(defn do-some-stuff []
(add "Brian" "123 Bovine University Blvd.")
(add "Roger" "456 Main St.")
(report))
Mirando a do-some-stuff
de forma aislada, ¿qué diablos está haciendo? Hay muchas cosas sucediendo implícitamente. Por este camino se encuentran los espaguetis. Una versión posiblemente mejor:
(defn make-address-book [] {})
(defn add [addr-book name addr]
(assoc addr-book name addr))
(defn report [addr-book]
(doseq [[name addr] addr-book]
(println name ":" addr)))
(defn do-some-stuff []
(let [addr-book (make-address-book)]
(-> addr-book
(add "Brian" "123 Bovine University Blvd.")
(add "Roger" "456 Main St.")
(report))))
Ahora está claro lo que do-some-stuff
, incluso de manera aislada. Puedes tener tantas libretas de direcciones flotando como quieras. Varios hilos pueden tener los suyos propios. Puede utilizar este código de múltiples espacios de nombres de forma segura. No puedes olvidar inicializar la libreta de direcciones, porque la pasas como argumento. Puede probar el report
fácilmente: simplemente pase la libreta de direcciones "simulada" deseada y vea qué se imprime. No tiene que preocuparse por ningún estado global ni nada, sino por la función que está probando en este momento.
Si no necesita coordinar actualizaciones a una estructura de datos desde múltiples subprocesos, generalmente no hay necesidad de usar referencias o vars globales.