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.
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. –
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