2010-07-16 8 views
26

Me he encontrado utilizando el siguiente modismo últimamente en código clojure.Mejores prácticas para globales en clojure, (refs vs alter-var-root)?

(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 esto no es ni siquiera multi-hilo de código que puede ser que necesite la semántica transaccional que le dan referencias. Simplemente parece que los refs son más que un código de subprocesos, pero básicamente para cualquier global que requiera inmutabilidad. ¿Hay una mejor práctica para esto? Podría tratar de refactorizar el código para que solo use binding o let pero eso puede ser particularmente complicado para algunas aplicaciones.

Respuesta

21

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 una vez que tienes más de uno de estos vars globales flotando.

Es posible que las personas que llaman a sus funciones ni siquiera sepan que sus funciones dependen del valor de la var global, sin inspeccionar la fuente. ¿Qué pasa 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 depende de estos valores globales? Probablemente van a pisar uno al otro, a menos que use binding. También agrega gastos generales cada vez que accede a los datos de una ref.

Si escribe el código de efectos secundarios gratis, estos problemas desaparecerán. Una función se sostiene por sí misma. Es fácil de probar: pasarle algunas entradas, inspeccionar 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 tu código es seguro para subprocesos. Y probablemente funcione más rápido.

Es difícil pensar en el código de esta manera si está acostumbrado al estilo de programación "mutar un montón de objetos/memoria", pero una vez que lo domina, es relativamente sencillo organizar sus programas de esta manera camino. Por lo general, su código termina siendo tan simple o simple como la versión de mutación global del mismo código.

Aquí hay un ejemplo muy 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)) 

En cuanto a do-some-stuff de forma aislada, ¿qué diablos está haciendo? Hay muchas cosas sucediendo implícitamente. Por este camino yace el espagueti. Una versión sin duda 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 está haciendo do-some-stuff, incluso en el aislamiento. Puede tener tantas libretas de direcciones flotando como desee. Múltiples hilos podrían tener los suyos. Puede usar este código desde múltiples espacios de nombres de forma segura. No puede olvidarse de inicializar la libreta de direcciones, porque la pasa como argumento. Puede probar report fácilmente: simplemente pase la libreta de direcciones "simulada" y vea lo que imprime. No tiene que preocuparse por ningún estado global ni nada que no sea la función que está probando en este momento.

Si no necesita coordinar las actualizaciones de varios hilos en una estructura de datos, generalmente no es necesario utilizar refs o variables globales.

+6

No soy ajeno al enfoque funcional que describes. Pero a veces la conveniencia de una ubicación global de ese estado es útil. Todos los enfoques funcionales se descomponen en los bordes, siendo el caso más frecuente IO. Podría considerar esto como un caso especial de IO ya que es efectivamente global para todos los hilos. No me malinterprete. Prefiero el enfoque funcional y mi ejemplo de uso de la referencia anterior es demasiado simplista, por lo que en general estoy de acuerdo con usted. –

+0

pasando el valor a todas las funciones sin duda una buena alternativa, pero siento que a veces una variable global es más práctica que pasar un montón de valor a un montón de funciones una y otra vez. Es una cuestión de gusto y tolerancia a los efectos secundarios. – ChrisBlom

26

Siempre uso un átomo en lugar de un ref 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.

p. Ej. para un mapa mutable de pares clave/valor que usaría:

(def state (atom {})) 

(defn get-state [key] 
    (@state key)) 

(defn update-state [key val] 
    (swap! state assoc key val)) 
+0

Esto está cerca de mi enfoque preferido, hago la misma definición de uso de compra para declarar la ubicación compartida, para evitar sobrescribirla, y uso asteriscos en el nombre para aclarar que es una ubicación compartida – ChrisBlom

+0

Usar asteriscos en el símbolo el nombre ahora da como resultado una advertencia del compilador, ya que están reservados para vars dinámicos. – spieden