Lisp y Erlang Atoms, Ruby y Scheme Symbols. ¿Qué tan útiles son?
(13)
Los átomos son literales, constantes con su propio nombre para el valor. Lo que ves es lo que obtienes y no esperas más. El gato atómico significa "gato" y eso es todo. No puedes jugar con eso, no puedes cambiarlo, no puedes romperlo en pedazos; es un gato Tratar con él.
Comparé átomos con constantes que tienen su nombre como sus valores. Es posible que haya trabajado con código que usó constantes antes: como ejemplo, digamos que tengo valores para los colores de ojos:
BLUE -> 1, BROWN -> 2, GREEN -> 3, OTHER -> 4
. Debe hacer coincidir el nombre de la constante con algún valor subyacente. Los átomos te permiten olvidarte de los valores subyacentes: mis colores de ojos pueden ser simplemente ''azul'', ''marrón'', ''verde'' y ''otros''. Estos colores se pueden usar en cualquier parte de cualquier código: ¡los valores subyacentes nunca chocarán y es imposible que dicha constante esté indefinida!
tomado de http://learnyousomeerlang.com/starting-out-for-real#atoms
Dicho esto, los átomos terminan siendo un mejor ajuste semántico para describir los datos en su código en lugares en los que otros idiomas se verían obligados a usar cadenas, enumeraciones o definiciones. Son más seguros y amigables para usar con los mismos resultados previstos.
¿Qué tan útil es la característica de tener un tipo de datos átomo en un lenguaje de programación?
Algunos lenguajes de programación tienen el concepto de átomo o símbolo para representar una especie de constante. Hay algunas diferencias entre los idiomas que he encontrado (Lisp, Ruby y Erlang), pero me parece que el concepto general es el mismo. Estoy interesado en el diseño de lenguaje de programación, y me preguntaba qué valor tiene tener un tipo de átomo en la vida real. Otros lenguajes como Python, Java, C # parecen estar funcionando bastante bien sin él.
No tengo una experiencia real de Lisp o Ruby (conozco las sintaxis, pero tampoco las he usado en un proyecto real). He usado Erlang lo suficiente como para estar acostumbrado al concepto allí.
Como programador de C, tuve un problema para entender qué son realmente los símbolos de Ruby. Me iluminé después de ver cómo se implementan los símbolos en el código fuente.
Dentro del código Ruby, hay una tabla hash global, cadenas mapeadas a enteros. Todos los símbolos de rubí se guardan allí. El intérprete de Ruby, durante la etapa de análisis del código fuente, usa esa tabla hash para convertir todos los símbolos en enteros. Entonces internamente todos los símbolos se tratan como enteros. Esto significa que un símbolo ocupa solo 4 bytes de memoria y todas las comparaciones son muy rápidas.
Así que, básicamente, puede tratar los símbolos de Ruby como cadenas que se implementan de una manera muy inteligente. Se ven como cadenas, pero tienen un rendimiento casi como enteros.
Cuando se crea una nueva cadena, en Ruby se asigna una nueva estructura C para mantener ese objeto. Para dos cadenas de Ruby, hay dos punteros a dos ubicaciones de memoria diferentes (que pueden contener la misma cadena). Sin embargo, un símbolo se convierte inmediatamente a tipo C int. Por lo tanto, no hay forma de distinguir dos símbolos como dos objetos Ruby diferentes. Este es un efecto secundario de la implementación. Solo ten esto en cuenta cuando codifiques y eso es todo.
El problema que tengo con conceptos similares en otros idiomas (por ejemplo, C) se puede expresar fácilmente como:
#define RED 1
#define BLUE 2
#define BIG 1
#define SMALL 2
o
enum colors { RED, BLUE };
enum sizes { BIG, SMALL };
Lo que causa problemas tales como:
if (RED == BIG)
printf("True");
if (BLUE == 2)
printf("True");
Ninguno de los cuales realmente tiene sentido. Los átomos resuelven un problema similar sin los inconvenientes mencionados anteriormente.
En Lisp, símbolo y átomo son dos conceptos diferentes y no relacionados.
Por lo general, en Lisp, un ATOM no es un tipo de datos específico. Es una mano corta para NOT CONS.
(defun atom (item)
(not (consp item)))
Además, el tipo ATOM es igual que el tipo (NO CONS).
Cualquier cosa que no sea una celda de cons es un átomo en Common Lisp.
Un SÍMBOLO es un tipo de datos específico.
Un símbolo es un objeto con un nombre e identidad. Un símbolo puede ser internado en un paquete . Un símbolo puede tener un valor, una función y una lista de propiedades.
CL-USER 49 > (describe ''FOO)
FOO is a SYMBOL
NAME "FOO"
VALUE #<unbound value>
FUNCTION #<unbound function>
PLIST NIL
PACKAGE #<The COMMON-LISP-USER package, 91/256 internal, 0/4 external>
En el código fuente de Lisp, los identificadores de variables, funciones, clases, etc. se escriben como símbolos. Si el lector lee una expresión s de Lisp, crea nuevos símbolos si no se conocen (disponibles en el paquete actual) o reutiliza un símbolo existente (si está disponible en el paquete actual. Si el lector de Lisp lee un lista como
(snow snow)
luego crea una lista de dos células cons. El CAR de cada punto de cons para el mismo símbolo nieva . Solo hay un símbolo para eso en la memoria Lisp.
También tenga en cuenta que el plist (la lista de propiedades) de un símbolo puede almacenar metainformación adicional para un símbolo. Este podría ser el autor, una ubicación de origen, etc. El usuario también puede usar esta característica en sus programas.
En Ruby, los símbolos se usan a menudo como claves en hash, con tanta frecuencia que Ruby 1.9 incluso introdujo una abreviatura para construir un hash. Lo que previamente escribiste como:
{:color => :blue, :age => 32}
ahora se puede escribir como:
{color: :blue, age: 32}
Esencialmente, son algo entre cadenas y enteros: en el código fuente se asemejan a cadenas, pero con diferencias considerables. Las mismas dos cadenas son, de hecho, instancias diferentes, mientras que los mismos símbolos son siempre la misma instancia:
> ''foo''.object_id
# => 82447904
> ''foo''.object_id
# => 82432826
> :foo.object_id
# => 276648
> :foo.object_id
# => 276648
Esto tiene consecuencias tanto con el rendimiento como con el consumo de memoria. Además, son inmutables. No está destinado a ser alterado una vez cuando se le asigna.
Una regla de oro discutible sería usar símbolos en lugar de cadenas para cada cadena que no sea para la salida.
Aunque tal vez parezca irrelevante, la mayoría de los editores que resaltan el código colorean los símbolos de forma diferente que el resto del código, haciendo la distinción visual.
En Scheme (y otros miembros de la familia Lisp), los símbolos no son solo útiles, son esenciales.
Una propiedad interesante de estos lenguajes es que son homoicónicos . Un programa o expresión de esquema se puede representar como una estructura de datos de esquema válida.
Un ejemplo podría aclarar esto (usando el Esquema Gauche):
> (define x 3)
x
> (define expr ''(+ x 1))
expr
> expr
(+ x 1)
> (eval expr #t)
4
Aquí, expr es solo una lista, que consiste en el símbolo + , el símbolo xy el número 1 . Podemos manipular esta lista como cualquier otra, pasarla, etc. Pero también podemos evaluarla, en cuyo caso se interpretará como código.
Para que esto funcione, Scheme necesita poder distinguir entre símbolos y literales de cadena. En el ejemplo anterior, x es un símbolo. No puede reemplazarse con un literal de cadena sin cambiar el significado. Si tomamos una lista ''(imprimir x) , donde x es un símbolo y lo evaluamos, eso significa algo más que '' (imprimir ''x'') , donde ''x'' es una cadena.
La capacidad de representar expresiones de Scheme usando estructuras de datos Scheme no es solo un truco, por cierto; leer expresiones como estructuras de datos y transformarlas de alguna manera, es la base de las macros.
En algunos idiomas, los literales asociativos de arreglos tienen claves que se comportan como símbolos.
En Python [1], un diccionario.
d = dict(foo=1, bar=2)
En Perl [2], un hash.
my %h = (foo => 1, bar => 2);
En JavaScript [3], un objeto.
var o = {foo: 1, bar: 2};
En estos casos, foo
y bar
son como símbolos, es decir, cadenas inmutables sin comillas.
[1] Prueba:
x = dict(a=1)
y = dict(a=2)
(k1,) = x.keys()
(k2,) = y.keys()
assert id(k1) == id(k2)
[2] Esto no es del todo cierto:
my %x = (a=>1);
my %y = (a=>2);
my ($k1) = keys %x;
my ($k2) = keys %y;
die unless /$k1 == /$k2; # dies
[1] En JSON, esta sintaxis no está permitida porque las claves deben ser citadas. No sé cómo demostrar que son símbolos porque no sé leer la memoria de una variable.
En realidad no estás en lo cierto al decir que Python no tiene analogía con átomos o símbolos. No es difícil crear objetos que se comporten como átomos en python. Solo haz, bueno, objetos. Llanura de objetos vacíos. Ejemplo:
>>> red = object()
>>> blue = object()
>>> c = blue
>>> c == red
False
>>> c == blue
True
>>>
¡TADA! ¡Átomos en Python! Uso este truco todo el tiempo. En realidad, puedes ir más allá de eso. Puede darle un tipo a estos objetos:
>>> class Colour:
... pass
...
>>> red = Colour()
>>> blue = Colour()
>>> c = blue
>>> c == red
False
>>> c == blue
True
>>>
Ahora, tus colores tienen un tipo, por lo que puedes hacer cosas como esta:
>>> type(red) == Colour
True
>>>
Si me preguntas, eso es en realidad una mejora en los símbolos Lispy.
Los átomos (en Erlang o Prolog, etc.) o los símbolos (en Lisp o Ruby, etc.) - de aquí solo se llaman átomos - son muy útiles cuando se tiene un valor semántico que no tiene una representación "natural" subyacente natural. Toman el espacio de enums de estilo C como este:
enum days { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
La diferencia es que los átomos normalmente no tienen que declararse y no tienen NINGUNA representación subyacente de la que preocuparse. El átomo el monday
en Erlang o Prolog tiene el valor de "el átomo monday
" y nada más o menos.
Si bien es cierto que se puede obtener el mismo uso de los tipos de cuerdas que se obtendría de los átomos, existen algunas ventajas para los segundos. En primer lugar, debido a que se garantiza que los átomos son únicos (detrás de las escenas, sus representaciones de cadenas se convierten en algún tipo de ID fácil de probar) es mucho más rápido compararlos que comparar cadenas equivalentes. En segundo lugar, son indivisibles. El átomo monday
no se puede probar para ver si termina en el day
por ejemplo. Es una unidad semántica pura e indivisible. Tiene menos sobrecarga conceptual de la que tendría en una representación de cadena en otras palabras.
También podría obtener el mismo beneficio con las enumeraciones estilo C. La velocidad de comparación en particular es, en todo caso, más rápida. Pero ... es un número entero. Y puedes hacer cosas raras como tener SATURDAY
y SUNDAY
con el mismo valor:
enum days { SATURDAY, SUNDAY = 0, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY }
Esto significa que no puede confiar en que los diferentes "símbolos" (enumeraciones) sean cosas diferentes y, por lo tanto, hace que el razonamiento sobre el código sea mucho más difícil. Demasiado, el envío de tipos enumerados a través de un protocolo cableado es problemático porque no hay forma de distinguir entre ellos y los enteros regulares. Los átomos no tienen este problema. Un átomo no es un número entero y nunca se verá como uno detrás de la escena.
Los átomos proporcionan pruebas rápidas de igualdad, ya que usan identidad. En comparación con los tipos o enteros enumerados, tienen mejor semántica (¿por qué representarías un valor simbólico abstracto por un número de todos modos?) Y no están restringidos a un conjunto fijo de valores como enumeraciones.
El compromiso es que son más caros de crear que las cadenas literales, ya que el sistema necesita conocer todas las instancias existentes para mantener la singularidad; esto cuesta tiempo principalmente para el compilador, pero cuesta memoria en O (número de átomos únicos).
Los átomos son como una enumeración abierta, con infinitos valores posibles, y no es necesario declarar nada por adelantado. Así es como se usan típicamente en la práctica.
Por ejemplo, en Erlang, un proceso espera recibir uno de un puñado de tipos de mensajes, y es más conveniente etiquetar el mensaje con un átomo. La mayoría de los otros idiomas usarían una enumeración para el tipo de mensaje, lo que significa que siempre que quiera enviar un nuevo tipo de mensaje, debo agregarlo a la declaración.
Además, a diferencia de las enumeraciones, se pueden combinar conjuntos de valores de átomos. Supongamos que quiero monitorear el estado de mi proceso Erlang, y tengo alguna herramienta de monitoreo de estado estándar. Puedo extender mi proceso para responder al protocolo de mensajes de estado , así como a mis otros tipos de mensajes . Con enumeraciones, ¿cómo resolvería este problema?
enum my_messages {
MSG_1,
MSG_2,
MSG_3
};
enum status_messages {
STATUS_HEARTBEAT,
STATUS_LOAD
};
El problema es que MSG_1 es 0, y STATUS_HEARTBEAT también es 0. Cuando recibo un mensaje de tipo 0, ¿qué es? Con átomos, no tengo este problema.
Los átomos / símbolos no son solo cadenas con comparación de tiempo constante :).
Se garantiza que los átomos son únicos e integrales, en contraste con, por ejemplo, valores constantes de punto flotante, que pueden diferir debido a la inexactitud mientras se está codificando, enviándolos a través del cable, decodificando por el otro lado y convirtiéndolo de nuevo a punto flotante . No importa qué versión de intérprete estés utilizando, garantiza que el átomo siempre tenga el mismo "valor" y sea único.
La VM de Erlang almacena todos los átomos definidos en todos los módulos en una tabla de átomos global.
No hay ningún tipo de datos booleanos en Erlang . En cambio, los átomos true
y false
se usan para denotar valores booleanos. Esto evita que uno haga ese tipo de cosas desagradables:
#define TRUE FALSE //Happy debugging suckers
En Erlang, puede guardar átomos en archivos, leerlos, pasarlos por el cable entre máquinas virtuales Erlang remotas, etc.
Solo como ejemplo, guardaré un par de términos en un archivo y luego los leeré. Este es el archivo fuente de Erlang lib_misc.erl
(o su parte más interesante para nosotros ahora):
-module(lib_misc).
-export([unconsult/2, consult/1]).
unconsult(File, L) ->
{ok, S} = file:open(File, write),
lists:foreach(fun(X) -> io:format(S, "~p.~n",[X]) end, L),
file:close(S).
consult(File) ->
case file:open(File, read) of
{ok, S} ->
Val = consult1(S),
file:close(S),
{ok, Val};
{error, Why} ->
{error, Why}
end.
consult1(S) ->
case io:read(S, '''') of
{ok, Term} -> [Term|consult1(S)];
eof -> [];
Error -> Error
end.
Ahora voy a compilar este módulo y guardar algunos términos en un archivo:
1> c(lib_misc).
{ok,lib_misc}
2> lib_misc:unconsult("./erlang.terms", [42, "moo", erlang_atom]).
ok
3>
En el archivo erlang.terms
obtendremos este contenido:
42.
"moo".
erlang_atom.
Ahora leámoslo de nuevo:
3> {ok, [_, _, SomeAtom]} = lib_misc:consult("./erlang.terms").
{ok,[42,"moo",erlang_atom]}
4> is_atom(SomeAtom).
true
5>
Verá que los datos se leen con éxito del archivo y la variable SomeAtom
realmente contiene un átomo erlang_atom
.
lib_misc.erl
contenidos de lib_misc.erl
se lib_misc.erl
de "Programación de Erlang: Software para un mundo concurrente" de Joe Armstrong, publicado por The Pragmatic Bookshelf. El código fuente del resto está aquí .
Un breve ejemplo que muestra cómo la capacidad de manipular símbolos conduce a un código más limpio: (El código está en Scheme, un dialecto de Lisp).
(define men ''(socrates plato aristotle))
(define (man? x)
(contains? men x))
(define (mortal? x)
(man? x))
;; test
> (mortal? ''socrates)
=> #t
Puede escribir este programa usando cadenas de caracteres o constantes enteras. Pero la versión simbólica tiene ciertas ventajas. Se garantiza que un símbolo es único en el sistema. Esto hace que comparar dos símbolos sea tan rápido como comparar dos punteros. Esto es obviamente más rápido que comparar dos cadenas. El uso de constantes enteras permite a las personas escribir códigos sin sentido como:
(define SOCRATES 1)
;; ...
(mortal? SOCRATES)
(mortal? -1) ;; ??
Probablemente se encuentre una respuesta detallada a esta pregunta en el libro Common Lisp: A Gentle Introduction to Symbolic Computation .