starting learn clojure lisp symbols

starting - learn clojure



¿Por qué Clojure distingue entre símbolos y vars? (8)

Ya vi esta pregunta , pero no explica lo que estoy pensando.

Cuando recién llegué a Clojure de Common Lisp, me sorprendió por qué trata los símbolos y las palabras clave como tipos separados, pero luego lo descubrí, y ahora creo que es una idea maravillosa. Ahora intento descifrar por qué los símbolos y los vars son objetos separados.

Hasta donde sé, las implementaciones de Common Lisp generalmente representan un "símbolo" usando una estructura que tiene 1) una cadena para el nombre, 2) un puntero al valor del símbolo cuando se evalúa en posición de llamada de función, 3) un puntero a su valor cuando posición de llamada externa evaluada, y 4) lista de propiedades, etc.

Ignorando la distinción Lisp-1 / Lisp-2, el hecho es que en CL, un objeto "símbolo" apunta directamente a su valor. En otras palabras, CL combina lo que Clojure llama un "símbolo" y una "var" en un solo objeto.

En Clojure, para evaluar un símbolo, primero se debe buscar la var correspondiente, luego se debe desreferenciar la var. ¿Por qué Clojure funciona de esta manera? ¿Qué beneficio podría haber de tal diseño? Entiendo que los vars tienen ciertas propiedades especiales (pueden ser privadas, o const, o dinámicas ...), pero ¿no podrían esas propiedades simplemente aplicarse al símbolo mismo?


Clojure es mi primer (y único) ceceo hasta la fecha, por lo que esta respuesta es una especie de suposición. Dicho esto, la siguiente discusión del sitio web de clojure parece pertinente (el énfasis es mío):

Clojure es un lenguaje práctico que reconoce la necesidad ocasional de mantener una referencia persistente a un valor cambiante y proporciona 4 mecanismos distintos para hacerlo de manera controlada: Vars, Refs, Agents y Atoms. Los Vars proporcionan un mecanismo para hacer referencia a una ubicación de almacenamiento mutable que puede rebotearse dinámicamente (a una nueva ubicación de almacenamiento) por subproceso . Cada Var puede (pero no es necesario) tener un enlace raíz, que es un enlace compartido por todos los hilos que no tienen un enlace por hilo. Por lo tanto, el valor de un Var es el valor de su enlace por subproceso, o, si no está enlazado en el subproceso que solicita el valor, el valor del enlace de raíz, si lo hay.

Por lo tanto, los símbolos indirectos para Vars permiten un reencaminamiento dinámico seguro de hilos (quizás esto se puede hacer de otras formas, pero no sé). Considero que esto forma parte de la filosofía central de clojure de distinguir de manera rigurosa y omnipresente entre identidad y estado para permitir una concurrencia robusta.

Sospecho que esta instalación ofrece un verdadero beneficio solo en raras ocasiones, si es que lo hace, en comparación con el replanteamiento de un problema para no requerir encuadernación dinámica específica para hilos, pero está ahí si la necesita.


Después de pensar mucho en esta cuestión, puedo pensar en varias razones para diferenciar entre símbolos y vars, o como bien dijo Omri, usar "dos niveles de direccionamiento indirecto para asignar símbolos a sus valores subyacentes". Guardaré el mejor para el final ...

1: Al separar los conceptos de "una variable" y "un identificador que puede hacer referencia a una variable", Clojure hace que las cosas sean un poco más limpias conceptualmente. En CL, cuando el lector ve a , devuelve un objeto de símbolo que lleva punteros a enlaces de nivel superior, incluso si a está vinculado localmente en el ámbito actual. (En cuyo caso, el evaluador no hará uso de esas uniones de nivel superior.) En Clojure, un símbolo es solo un identificador, nada más.

Esto conecta con el punto que algunos carteles hicieron, que los símbolos también pueden referirse a las clases de Java en Clojure. Si los símbolos llevaban enlaces con ellos, esos enlaces podrían simplemente ignorarse en contextos donde el símbolo se refiere a una clase Java, pero sería desordenado conceptualmente.

2: En algunos casos, las personas pueden querer usar símbolos como claves de mapa y tal. Si los símbolos fueran objetos mutables (como lo son en CL), no encajarían bien con las estructuras de datos inmutables de Clojure.

3: en casos (probablemente raros) en que los símbolos se utilizan como claves de mapa, etc., y tal vez incluso devueltos por una API, la semántica de igualdad de los símbolos de Clojure es más intuitiva que los símbolos de CL. (Ver la respuesta de @ amalloy).

4: dado que Clojure enfatiza la programación funcional, se realiza mucho trabajo utilizando funciones de orden superior como partial , comp , juxt , etc. Incluso si no los usa, aún puede tomar funciones como argumentos para sus propias funciones, etc.

Ahora, cuando pasa my-func a una función de orden superior, no conserva ninguna referencia a la variable que se llama "my-func". Simplemente captura el valor tal como está ahora. Si redefine my-func más tarde, el cambio no se "propagará" a otras entidades que se definieron utilizando el valor de my-func .

Incluso en tales situaciones, al usar #''my-func , puede solicitar explícitamente que se busque el valor actual de my-func cada vez que se llame a la función derivada. (Presumiblemente a costa de un pequeño golpe de rendimiento)

En CL o Scheme, si necesitaba este tipo de direccionamiento indirecto, puedo imaginarme almacenar un objeto función en un cons o vector o struct, y recuperarlo desde allí cada vez que se lo llamara. En realidad, cada vez que necesitaba un objeto de "referencia mutable" que pudiera compartirse entre diferentes partes del código, podía usar un inconveniente u otra estructura mutable. Pero en Clojure, listas / vectores / etc. son todos inmutables, por lo que necesita alguna forma de referirse explícitamente a "algo que es mutable".


El principal beneficio es que es una capa extra de abstracción que es útil en varios casos.

Como ejemplo específico, los símbolos pueden existir felizmente antes de la creación de la var a la que hacen referencia:

(def my-code `(foo 1 2)) ;; create a list containing symbol user/foo => #''user/my-code my-code ;; confirm my-code contains the symbol user/foo => (user/foo 1 2) (eval my-code) ;; fails because user/foo not bound to a var => CompilerException java.lang.RuntimeException: No such var: user/foo... (def foo +) ;; define user/foo => #''user/foo (eval my-code) ;; now it works! => 3

El beneficio en términos de metaprogramación debe ser claro: puede construir y manipular código antes de que necesite crear instancias y ejecutarlo en un espacio de nombre completo.


Es extraño que nadie lo mencione, pero a pesar de que seguramente hay más de una razón para esta var indirección, una gran razón es la posibilidad de cambiar la referencia durante el tiempo de ejecución durante el desarrollo en el repl. Por lo tanto, puede ver los efectos modificados en el programa en ejecución mientras lo modifica, lo que permite un estilo de desarrollo con comentarios instantáneos (o cosas como la codificación en vivo).

Este chico lo explica mucho mejor que yo: https://www.youtube.com/watch?v=8NUI07y1SlQ (es cierto que casi dos años después de que se publicó esta pregunta). También analiza algunas de las implicaciones de rendimiento y da un ejemplo en el que esta indirección adicional cuesta alrededor del 10% de rendimiento. Esto no es tan malo, considerando lo que le devuelven. Sin embargo, la mayor penalización surge en forma de uso de montón adicional y un largo tiempo de arranque de Clojure, que sigue siendo un gran problema, creo.


He conjeturado la siguiente pregunta de tu publicación (dime si estoy fuera de la base):
¿Por qué hay dos niveles de direccionamiento indirecto para asignar símbolos a sus valores subyacentes?

Cuando fui por primera vez a responder esta pregunta, después de un tiempo, se me ocurrió dos razones posibles: "volver a definir" sobre la marcha, y la noción relacionada de alcance dinámico . Sin embargo, lo siguiente me ha convencido de que ninguno de estos es una razón para tener este doble indirecto:

=> (identical? (def a 0) (def a 10)) => true => (declare ^:dynamic bar) => (binding [bar "bar1"] (identical? (var bar) (binding [bar "bar2"] (var bar)))) => true

Para mí, esto muestra que ni el "re-defing" ni el alcance dinámico producen ninguna alteración en la relación entre un símbolo calificado de espacio de nombres y la variable a la que apunta.

En este punto, voy a hacer una nueva pregunta:
¿Un símbolo calificado de espacio de nombres siempre es sinónimo de la var a la que se refiere?

Si la respuesta a esta pregunta es sí, entonces simplemente no entiendo por qué debería haber otro nivel de indirección.

Si la respuesta es no, me gustaría saber en qué circunstancias un símbolo calificado de espacio de nombres apuntaría a diferentes valores durante una única ejecución del mismo programa.

Supongo, en resumen, gran pregunta: P


Otras preguntas han tocado muchos aspectos verdaderos de los símbolos, pero intentaré explicarlo desde otro ángulo.

Los símbolos son nombres

A diferencia de la mayoría de los lenguajes de programación, Clojure hace una distinción entre las cosas y los nombres de las cosas. En la mayoría de los idiomas, si digo algo como var x = 1 , entonces es correcto y completo decir "x es 1" o "el valor de x es 1". Pero en Clojure, si digo (def x 1) , he hecho dos cosas: he creado un Var (una entidad que mantiene el valor), y lo he nombrado con el símbolo x . Decir "el valor de x es 1" no dice toda la historia en Clojure. Una declaración más precisa (aunque engorrosa) sería "el valor de la var nombrada por el símbolo x es 1".

Los símbolos mismos son solo nombres, mientras que los vars son las entidades portadoras de valor y no tienen nombres. Si amplío el ejemplo anterior y digo (def yx) , no he creado una nueva var, solo le he dado a mi var existente un segundo nombre. Los dos símbolos y son ambos nombres para la misma var, que tiene el valor de 1.

Una analogía: mi nombre es "Luke", pero eso no es idéntico a mí, con lo que soy como persona. Es solo una palabra. No es imposible que en algún momento pueda cambiar mi nombre, y hay muchas otras personas que comparten mi nombre. Pero en el contexto de mi círculo de amigos (en mi espacio de nombres, si quieres), la palabra "Luke" significa mí. Y en una fantasía Clojure-land, podría ser una var que tenga un valor para ti.

¿Pero por qué?

Entonces, ¿por qué este concepto adicional de nombres es distinto de las variables, en lugar de combinar ambos como lo hacen la mayoría de los lenguajes?

Por un lado, no todos los símbolos están vinculados a vars. En contextos locales, como argumentos de funciones o dejar enlaces, el valor al que hace referencia un símbolo en el código no es realmente una var, es solo un enlace local que se optimizará y se transformará en código de bytes sin formato cuando llegue al compilador.

Sin embargo, lo más importante es que forma parte de la filosofía de "código es datos" de Clojure. La línea de código (def x 1) no es solo una expresión, sino también datos, específicamente una lista que consta de los valores def , x y 1 . Esto es muy importante, especialmente para macros, que manipulan código como datos.

Pero si (def x 1) es una lista, ¿cuáles son los valores en la lista? Y particularmente, ¿cuáles son los tipos de esos valores? Obviamente 1 es un número. ¿Pero qué hay de def y x ? ¿Cuál es su tipo , cuando los estoy manipulando como datos? La respuesta, por supuesto, símbolos.

Y esa es la razón principal por la que los símbolos son una entidad distinta en Clojure. En algunos contextos, como macros, desea tomar nombres y manipularlos, divorciados de cualquier significado particular o enlace otorgado por el tiempo de ejecución o el compilador. Y los nombres deben ser una especie de cosa, y el tipo de cosas que son es símbolos.



(ns a) (defn foo [] ''foo) (prn (foo)) (ns b) (defn foo [] ''foo)) (prn (foo))

El símbolo foo es exactamente el mismo símbolo en ambos contextos (es decir, (= ''foo (a/foo) (b/foo)) es verdadero), pero en los dos contextos debe llevar un valor diferente (en este caso, un puntero a una de dos funciones).