Atom Lisp et Erlang, symboles Ruby et Scheme. Dans quelle mesure sont-ils utiles?

Quelle est l’utilité d’avoir un type de données atomique dans un langage de programmation?

Quelques langages de programmation ont le concept d’atome ou de symbole pour représenter une sorte de constante. Il y a quelques différences entre les langues que j’ai rencontrées (Lisp, Ruby et Erlang), mais il me semble que le concept général est le même. Je m’intéresse à la conception des langages de programmation et je me demandais quelle est la valeur du fait d’avoir un type d’atome dans la vie réelle. D’autres langages tels que Python, Java, C # semblent se débrouiller très bien sans cela.

Je n’ai aucune expérience réelle de Lisp ou de Ruby (je connais les syntaxes, mais je ne les ai pas utilisées non plus dans un projet réel). J’ai utilisé suffisamment Erlang pour être habitué au concept là-bas.

Un petit exemple qui montre comment la possibilité de manipuler des symboles mène à un code plus propre: (Le code est dans Scheme, un dialecte de Lisp).

(define men '(socrates plato aristotle)) (define (man? x) (contains? men x)) (define (mortal? x) (man? x)) ;; test > (mortal? 'socrates) => #t 

Vous pouvez écrire ce programme en utilisant des chaînes de caractères ou des constantes entières. Mais la version symbolique présente certains avantages. Un symbole est garanti être unique dans le système. Cela permet de comparer deux symboles aussi rapidement que de comparer deux pointeurs. C’est évidemment plus rapide que de comparer deux chaînes. L’utilisation de constantes entières permet aux utilisateurs d’écrire du code sans signification comme:

 (define SOCRATES 1) ;; ... (mortal? SOCRATES) (mortal? -1) ;; ?? 

Probablement une réponse détaillée à cette question pourrait être trouvée dans le livre Common Lisp: Une introduction douce au calcul symbolique .

Les atomes sont des littéraux, des constantes avec leur propre nom pour la valeur. Ce que vous voyez est ce que vous obtenez et ne vous attendez pas plus. Le chat de l’atome signifie “chat” et c’est tout. Vous ne pouvez pas jouer avec, vous ne pouvez pas le changer, vous ne pouvez pas le briser en morceaux; c’est un chat Faites avec.

J’ai comparé des atomes à des constantes portant leur nom. Vous avez peut-être déjà travaillé avec du code utilisant des constantes: par exemple, disons que j’ai des valeurs pour les couleurs des yeux: BLUE -> 1, BROWN -> 2, GREEN -> 3, OTHER -> 4 . Vous devez faire correspondre le nom de la constante avec une valeur sous-jacente. Les atomes vous permettent d’oublier les valeurs sous-jacentes: la couleur de mes yeux peut simplement être «bleue», «brune», «verte» et «autre». Ces couleurs peuvent être utilisées n’importe où dans n’importe quel code: les valeurs sous-jacentes ne seront jamais en conflit et il est impossible qu’une telle constante soit indéfinie!

tiré de http://learnyousomeerlang.com/starting-out-for-real#atoms

Cela étant dit, les atomes finissent par être un meilleur ajustement sémantique pour décrire les données de votre code dans des endroits où d’autres langages seraient forcés d’utiliser des chaînes, des énumérations ou des définitions. Ils sont plus sûrs et plus conviviaux à utiliser pour obtenir des résultats similaires.

Les atomes (dans Erlang ou Prolog, etc.) ou les symboles (dans Lisp ou Ruby, etc.) – appelés ici uniquement atomes – sont très utiles lorsque vous avez une valeur sémantique sans représentation «native» sous-jacente naturelle. Ils prennent l’espace des énumérations de style C comme ceci:

 enum days { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } 

La différence est que les atomes ne doivent généralement pas être déclarés et qu’ils n’ont AUCUNE représentation sous-jacente. L’atome monday à Erlang ou Prolog a la valeur de “l’atome monday ” et rien de plus ou moins.

Bien qu’il soit vrai que vous pourriez obtenir la même utilisation des types de chaînes que vous le feriez avec des atomes, il y a des avantages à ces derniers. Premièrement, parce que les atomes sont garantis comme étant uniques (dans les coulisses, leurs représentations de chaînes sont converties en une forme d’identifiant facile à tester), il est beaucoup plus rapide de les comparer que de comparer des chaînes équivalentes. Deuxièmement, ils sont indivisibles. L’atome monday ne peut pas être testé pour voir s’il se termine par day par exemple. C’est une unité sémantique pure et indivisible. En d’autres termes, vous avez une surcharge conceptuelle moindre que dans une représentation sous forme de chaîne.

Vous pouvez également obtenir beaucoup des mêmes avantages avec les énumérations de style C. La vitesse de comparaison en particulier est, le cas échéant, plus rapide. Mais … c’est un entier. Et vous pouvez faire des choses bizarres comme avoir SATURDAY et SUNDAY traduire par la même valeur:

 enum days { SATURDAY, SUNDAY = 0, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY } 

Cela signifie que vous ne pouvez pas faire confiance à différents “symboles” (énumérations) et que cela rend le raisonnement sur le code beaucoup plus difficile. En outre, l’envoi de types énumérés via un protocole filaire est problématique car il n’existe aucun moyen de les distinguer des entiers réguliers. Les atomes n’ont pas ce problème. Un atome n’est pas un entier et ne ressemblera jamais à un dans les coulisses.

En tant que programmeur C, j’ai du mal à comprendre ce que sont réellement les symboles Ruby. J’ai été éclairé après avoir vu comment les symboles sont implémentés dans le code source.

Dans le code Ruby, il existe une table de hachage globale, des chaînes mappées sur des entiers. Tous les symboles rbuy y sont conservés. L’interpréteur Ruby, lors de l’parsing syntaxique du code source, utilise cette table de hachage pour convertir tous les symboles en entiers. En interne, tous les symboles sont traités comme des entiers. Cela signifie qu’un symbole occupe seulement 4 octets de mémoire et que toutes les comparaisons sont très rapides.

Donc, en gros, vous pouvez traiter les symboles Ruby comme des chaînes qui sont implémentées de manière très intelligente. Ils ressemblent à des chaînes mais fonctionnent presque comme des nombres entiers.

Lorsqu’une nouvelle chaîne est créée, dans Ruby, une nouvelle structure C est allouée pour conserver cet object. Pour deux chaînes Ruby, il existe deux pointeurs vers deux emplacements de mémoire différents (qui peuvent contenir la même chaîne). Cependant, un symbole est immédiatement converti en type C int. Il n’y a donc aucun moyen de distinguer deux symboles comme deux objects Ruby différents. Ceci est un effet secondaire de la mise en œuvre. Gardez cela à l’esprit lorsque vous codez et c’est tout.

En Lisp, le symbole et l’ atome sont deux concepts différents et indépendants.

Habituellement, dans Lisp, un ATOM n’est pas un type de données spécifique. C’est une main courte pour NOT CONS.

 (defun atom (item) (not (consp item))) 

De plus, le type ATOM est identique au type (NOT CONS).

Tout ce qui n’est pas une cellule contre est un atome de Common Lisp.

Un SYMBOL est un type de données spécifique.

Un symbole est un object avec un nom et une identité. Un symbole peut être interné dans un package . Un symbole peut avoir une valeur, une fonction et une liste de propriétés.

 CL-USER 49 > (describe 'FOO) FOO is a SYMBOL NAME "FOO" VALUE # FUNCTION # PLIST NIL PACKAGE # 

Dans le code source Lisp, les identificateurs des variables, fonctions, classes, etc. sont écrits sous forme de symboles. Si une expression Lisp s est lue par le lecteur, elle crée de nouveaux symboles si ceux-ci ne sont pas connus (disponibles dans le package actuel) ou réutilise un symbole existant (s’il est disponible dans le package actuel). liste comme

 (snow snow) 

alors il crée une liste de deux contre cellules. La CAR de chaque contre cellule pointe vers le même symbole de neige . Il n’y a qu’un seul symbole dans la mémoire Lisp.

Notez également que le plist (la liste de propriétés) d’un symbole peut stocker des informations méta supplémentaires pour un symbole. Cela pourrait être l’auteur, un emplacement source, etc. L’utilisateur peut également utiliser cette fonctionnalité dans ses programmes.

Dans Scheme (et les autres membres de la famille Lisp), les symboles ne sont pas seulement utiles, ils sont essentiels.

Une propriété intéressante de ces langues est qu’elles sont homoiconiques . Un programme ou une expression Scheme peut lui-même être représenté comme une structure de données Scheme valide.

Un exemple pourrait rendre cela plus clair (en utilisant Gauche Scheme):

 > (define x 3) x > (define expr '(+ x 1)) expr > expr (+ x 1) > (eval expr #t) 4 

Ici, expr est juste une liste composée du symbole + , du symbole x et du nombre 1 . On peut manipuler cette liste comme une autre, la faire circuler, etc. Mais on peut aussi l’évaluer, auquel cas elle sera interprétée comme du code.

Pour que cela fonctionne, Scheme doit pouvoir faire la distinction entre les symboles et les littéraux de chaîne. Dans l’exemple ci-dessus, x est un symbole. Il ne peut pas être remplacé par un littéral de chaîne sans en changer le sens. Si nous prenons une liste ‘(print x) , où x est un symbole et l’évalue, cela signifie autre chose que ‘ (print “x”) , où “x” est une chaîne.

La possibilité de représenter des expressions Scheme en utilisant des structures de données Scheme n’est pas simplement un gadget, en passant; lire des expressions en tant que structures de données et les transformer d’une certaine manière, est la base des macros.

Dans certaines langues, les littéraux de tableau associatif ont des clés qui se comportent comme des symboles.

Dans Python [1], un dictionnaire.

 d = dict(foo=1, bar=2) 

En Perl [2], un hash.

 my %h = (foo => 1, bar => 2); 

En JavaScript [3], un object.

 var o = {foo: 1, bar: 2}; 

Dans ces cas, foo et bar sont comme des symboles, c’est-à-dire des chaînes immuables non cotées.

[1] Preuve:

 x = dict(a=1) y = dict(a=2) (k1,) = x.keys() (k2,) = y.keys() assert id(k1) == id(k2) 

[2] Ce n’est pas tout à fait vrai:

 my %x = (a=>1); my %y = (a=>2); my ($k1) = keys %x; my ($k2) = keys %y; die unless \$k1 == \$k2; # dies 

[1] En JSON, cette syntaxe n’est pas autorisée car les clés doivent être citées. Je ne sais pas comment prouver qu’ils sont des symboles car je ne sais pas lire la mémoire d’une variable.

En réalité, vous n’êtes pas d’accord pour dire que Python n’a pas d’analogue aux atomes ou aux symboles. Ce n’est pas difficile de faire des objects qui se comportent comme des atomes en python. Fais simplement des objects. Objets vides Exemple:

 >>> red = object() >>> blue = object() >>> c = blue >>> c == red False >>> c == blue True >>> 

TADA! Des atomes en python! J’utilise cette astuce tout le temps. En fait, vous pouvez aller plus loin que cela. Vous pouvez donner un type à ces objects:

 >>> class Colour: ... pass ... >>> red = Colour() >>> blue = Colour() >>> c = blue >>> c == red False >>> c == blue True >>> 

Maintenant, vos couleurs ont un type, vous pouvez donc faire des choses comme ceci:

 >>> type(red) == Colour True >>> 

Si vous me demandez, c’est en fait une amélioration sur les symboles lispy.

Les atomes sont garantis uniques et intégraux, contrairement aux valeurs constantes à virgule flottante, par exemple, qui peuvent différer en raison de l’inexactitude lors de l’encodage, de leur envoi par fil, du décodage de l’autre côté et de la conversion en virgule flottante. . Quelle que soit la version de l’interpréteur que vous utilisez, elle garantit que l’atome a toujours la même “valeur” et est unique.

La VM d’Erlang stocke tous les atomes définis dans tous les modules d’une table d’atomes globale.

Il n’y a pas de type de données booléen dans Erlang . Au lieu de cela, les atomes true et false sont utilisés pour désigner des valeurs booléennes. Cela empêche quelqu’un de faire ce genre de chose méchante:

 #define TRUE FALSE //Happy debugging suckers 

Dans Erlang, vous pouvez enregistrer des atomes dans des fichiers, les lire, les transmettre par câble entre des machines virtuelles Erlang distantes, etc.

Par exemple, je vais enregistrer quelques termes dans un fichier, puis les relire. Ceci est le fichier source Erlang lib_misc.erl (ou sa partie la plus intéressante pour nous maintenant):

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

Maintenant, je vais comstackr ce module et enregistrer quelques termes dans un fichier:

 1> c(lib_misc). {ok,lib_misc} 2> lib_misc:unconsult("./erlang.terms", [42, "moo", erlang_atom]). ok 3> 

Dans le fichier erlang.terms nous aurons ce contenu:

 42. "moo". erlang_atom. 

Maintenant relisons-le:

 3> {ok, [_, _, SomeAtom]} = lib_misc:consult("./erlang.terms"). {ok,[42,"moo",erlang_atom]} 4> is_atom(SomeAtom). true 5> 

Vous voyez que les données sont lues avec succès à partir du fichier et la variable SomeAtom contient vraiment un atome erlang_atom .


lib_misc.erl contenus de lib_misc.erl sont extraits de “Programming Erlang: Software for a Concurrent World” de Joe Armstrong, publié par The Pragmatic Bookshelf. Le code source du rest est ici .

Les atomes fournissent des tests d’égalité rapides, car ils utilisent l’identité. Par rapport aux types ou aux entiers énumérés, ils ont une meilleure sémantique (pourquoi représenteriez-vous une valeur symbolique abstraite par un nombre de toute façon?) Et ils ne sont pas limités à un ensemble fixe de valeurs telles que les énumérations.

Le compromis est qu’ils sont plus coûteux à créer que les chaînes littérales, car le système doit connaître toutes les instances existantes pour conserver son caractère unique. Cela coûte du temps surtout pour le compilateur, mais cela coûte de la mémoire en O (nombre d’atomes uniques).

Dans Ruby, les symboles sont souvent utilisés comme clés dans les hachages, si souvent que Ruby 1.9 a même introduit un raccourci pour construire un hachage. Ce que vous avez écrit précédemment comme:

 {:color => :blue, :age => 32} 

peut maintenant être écrit comme:

 {color: :blue, age: 32} 

Essentiellement, ils sont quelque chose entre des chaînes et des entiers: dans le code source, ils ressemblent à des chaînes, mais avec des différences considérables. Les deux mêmes chaînes sont en fait des instances différentes, alors que les mêmes symboles sont toujours la même instance:

 > 'foo'.object_id # => 82447904 > 'foo'.object_id # => 82432826 > :foo.object_id # => 276648 > :foo.object_id # => 276648 

Cela a des conséquences à la fois sur les performances et la consommation de mémoire. De plus, ils sont immuables. Non destiné à être modifié une fois lorsqu’il est assigné.

Une règle de base discutable consisterait à utiliser des symboles au lieu de chaînes pour chaque chaîne non destinée à la sortie.

Bien que cela puisse sembler sans importance, la plupart des éditeurs de mise en évidence de codes colorent les symboles différemment du rest du code, en faisant la distinction visuelle.

Le problème que j’ai avec des concepts similaires dans d’autres langues (par exemple, C) peut être facilement exprimé comme suit:

 #define RED 1 #define BLUE 2 #define BIG 1 #define SMALL 2 

ou

 enum colors { RED, BLUE }; enum sizes { BIG, SMALL }; 

Ce qui pose des problèmes tels que:

 if (RED == BIG) printf("True"); if (BLUE == 2) printf("True"); 

Ni l’un ni l’autre n’a vraiment de sens. Les atomes résolvent un problème similaire sans les inconvénients notés ci-dessus.

Les atomes sont comme un enum ouvert, avec des valeurs possibles infinies, et pas besoin de déclarer quoi que ce soit devant. C’est ainsi qu’ils sont généralement utilisés dans la pratique.

Par exemple, dans Erlang, un processus s’attend à recevoir l’un des quelques types de message, et il est plus pratique d’étiqueter le message avec un atome. La plupart des autres langues utiliseraient une énumération pour le type de message, ce qui signifie que chaque fois que je veux envoyer un nouveau type de message, je dois l’append à la déclaration.

De plus, contrairement aux énumérations, des ensembles de valeurs d’atomes peuvent être combinés. Supposons que je veuille contrôler le statut de mon processus Erlang et que je dispose d’un outil standard de surveillance de l’état. Je peux prolonger mon processus pour répondre au protocole de message d’état ainsi qu’à mes autres types de message . Avec les énumérations, comment pourrais-je résoudre ce problème?

 enum my_messages { MSG_1, MSG_2, MSG_3 }; enum status_messages { STATUS_HEARTBEAT, STATUS_LOAD }; 

Le problème est que MSG_1 est à 0 et STATUS_HEARTBEAT à 0. Quand je reçois un message de type 0, c’est quoi? Avec les atomes, je n’ai pas ce problème.

Les atomes / symboles ne sont pas simplement des chaînes avec une comparaison à temps constant :).