Lisp和Erlang Atoms,Ruby和Scheme符​​号。它们有用吗?

时间:2011-02-02 13:00:04

标签: ruby erlang scheme lisp

在编程语言中使用原子数据类型的功能有多大用处?

一些编程语言具有原子或符号的概念来表示各种常量。我遇到的语言(Lisp,Ruby和Erlang)之间存在一些差异,但在我看来,一般概念是相同的。我对编程语言设计很感兴趣,我想知道原子类型在现实生活中提供了什么价值。其他语言如Python,Java,C#似乎在没有它的情况下做得很好。

我没有Lisp或Ruby的实际经验(我知道语法,但在实际项目中没有使用过)。我已经使用Erlang足以用于那里的概念。

13 个答案:

答案 0 :(得分:53)

  

Atoms是文字,常量有自己的名称。你所看到的就是你得到的,不要期望更多。原子猫的意思是“猫”,就是这样。你不能玩它,你不能改变它,你不能把它粉碎成碎片;这是猫。处理它。

     

我将原子与以其名称作为其值的常量进行比较。您可能以前使用过使用常量的代码:例如,假设我有眼睛颜色的值:BLUE -> 1, BROWN -> 2, GREEN -> 3, OTHER -> 4。您需要将常量的名称与某个基础值匹配。原子让你忘记了潜在的价值观:我的眼睛颜色可以简单地是“蓝色”,“棕色”,“绿色”和“其他”。这些颜色可以在任何代码段中的任何地方使用:底层值永远不会发生冲突,这样的常量不可能是未定义的!

取自http://learnyousomeerlang.com/starting-out-for-real#atoms

说到这里,原子最终会成为一种更好的语义,适合在代码中描述数据,其他语言将被强制使用字符串,枚举或定义。它们更安全,更友好,可用于类似的预期结果。

答案 1 :(得分:37)

一个简短的例子,展示了操纵符号的能力如何导致更清晰的代码:(代码在Scheme中,是Lisp的一种方言)。

(define men '(socrates plato aristotle))

(define (man? x) 
    (contains? men x))

(define (mortal? x) 
    (man? x))

;; test

> (mortal? 'socrates)
=> #t

您可以使用字符串或整数常量编写此程序。但符号版本具有一定的优势。保证符号在系统中是唯一的。这使得比较两个符号与比较两个指针一样快。这显然比比较两个字符串更快。使用整数常量允许人们编写无意义的代码,如:

(define SOCRATES 1)
;; ...

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

可能会在Common Lisp: A Gentle Introduction to Symbolic Computation一书中找到这个问题的详细答案。

答案 2 :(得分:14)

原子(在Erlang或Prolog等中)或符号(在Lisp或Ruby等中) - 这里仅称为原子 - 在您具有没有自然的“本机”表示的语义值时非常有用。他们采用C风格的枚举空间:

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

不同之处在于通常不必声明原子并且它们没有潜在的表示来担心。 Erlang或Prolog中的原子monday具有“原子monday”的值,而不是更多或更少。

虽然你可以从原子中获得与字符串类型相同的用法,但后者有一些优点。首先,因为保证原子是唯一的(在幕后,它们的字符串表示被转换成某种形式的易于测试的ID),所以比较它们比比较等效字符串要快得多。其次,它们是不可分割的。无法测试原子monday以查看它是否以day结尾。它是一个纯粹的,不可分割的语义单元。与换句话说,字符串表示相比,概念重载更少。

使用C风格的枚举也可以获得很多相同的好处。特别是比较速度,如果有的话,更快。但是......它是一个整数。你可以做一些奇怪的事情,例如将SATURDAYSUNDAY翻译成相同的值:

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

这意味着你不能相信不同的“符号”(枚举)是不同的东西,因此使代码的推理变得更加困难。太,通过有线协议发送枚举类型是有问题的,因为没有办法区分它们和常规整数。原子没有这个问题。原子不是整数,在幕后看起来永远不会像一个。

答案 3 :(得分:13)

作为一名C程序员,我在理解Ruby符号究竟是什么方面遇到了问题。在看到源代码中的符号实现后,我开悟了。

在Ruby代码中,有一个全局哈希表,字符串映射到整数。所有ruby符号都保存在那里。在源代码解析阶段,Ruby解释器使用该哈希表将所有符号转换为整数。然后在内部所有符号都被视为整数。这意味着一个符号只占用4个字节的内存,所有比较都非常快。

所以基本上你可以将Ruby符号视为以非常聪明的方式实现的字符串。 它们看起来像字符串,但几乎像整数一样。

创建新字符串时,在Ruby中会分配一个新的C结构来保留该对象。对于两个Ruby字符串,有两个指向两个不同内存位置的指针(可能包含相同的字符串)。但是,符号会立即转换为C int类型。因此,无法将两个符号区分为两个不同的Ruby对象。这是实施的副作用。在编码时请记住这一点,这就是全部。

答案 4 :(得分:12)

在Lisp中符号 atom 是两个不同且无关的概念。

通常在Lisp中,ATOM不是特定的数据类型。这是NOT CONS的简称。

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

类型ATOM也与类型(NOT CONS)相同。

任何不是cons单元格的东西都是Common Lisp中的原子。

SYMBOL是一种特定的数据类型。

符号是具有名称和标识的对象。符号可以在中实现。符号可以包含值,函数和属性列表。

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>

在Lisp源代码中,变量,函数,类等的标识符被写为符号。如果读取器读取了Lisp s表达式,它会创建新符号(如果它们未知(在当前包中可用)或重用现有符号(如果它在当前包中可用)。如果Lisp读取器读取列表如

(snow snow)

然后它创建了两个cons单元的列表。每个缺点单元格的CAR指向相同的符号 snow 。在Lisp内存中只有一个符号。

另请注意,符号的plist(属性列表)可以存储符号的其他元信息。这可以是作者,源位置等。用户也可以在他/她的程序中使用此功能。

答案 5 :(得分:6)

在Scheme(以及Lisp系列的其他成员)中,符号不仅有用,而且非常重要。

这些语言的有趣属性是homoiconic。 Scheme程序或表达式本身可以表示为有效的Scheme数据结构。

一个例子可能会更清楚(使用Gauche Scheme):

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

此处, expr 只是一个列表,由符号 + ,符号 x 和数字 1 <组成/ em>的。我们可以像其他任何一样操纵这个列表,传递它等等。但是我们也可以对它进行评估,在这种情况下它将被解释为代码。

为了使其工作,Scheme需要能够区分符号和字符串文字。在上面的示例中, x 是一个符号。在不改变含义的情况下,不能用字符串文字替换它。如果我们取一个列表'(print x),其中 x 是一个符号,并对其进行评估,这意味着除了'之外的其他内容(print“x”) ,其中“x”是一个字符串。

顺便说一句,使用Scheme数据结构表示Scheme表达式的能力不仅仅是一种噱头;将表达式作为数据结构读取并以某种方式对其进行转换是宏的基础。

答案 6 :(得分:3)

与e相比,原子被保证是独一无二的。例如,浮点常数值,由于编码时的不准确性,通过线路发送它们,在另一侧解码并转换回浮点,可能会有所不同。无论您使用何种版本的解释器,它都能确保原子始终具有相同的“值”并且是唯一的。

Erlang VM将所有模块中定义的所有原子存储在全局atom table

no Boolean data type in Erlang。相反,原子truefalse用于表示布尔值。这可以防止人们做这种讨厌的事情:

#define TRUE FALSE //Happy debugging suckers

在Erlang中,您可以将原子保存到文件中,读取它们,通过远程Erlang VM之间的线路传递它们。

就像我将一些术语保存到一个文件中,然后再读回来一样。这是Erlang源文件lib_misc.erl(或者它现在最有趣的部分):

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

现在我将编译此模块并将一些术语保存到文件中:

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

在文件erlang.terms中,我们会收到以下内容:

42.
"moo".
erlang_atom. 

现在让我们回读一下:

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

您看到数据已从文件中成功读取,变量SomeAtom确实包含原子erlang_atom


lib_misc.erl内容摘自Joe Armstrong撰写的“编程Erlang:并发世界的软件”,由The Pragmatic Bookshelf出版。其余的源代码是here

答案 7 :(得分:3)

在某些语言中,关联数组文字的键的行为与符号类似。

在Python [1]中,一本字典。

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

在Perl [2]中,哈希。

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

在JavaScript [3]中,一个对象。

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

在这些情况下,foobar就像符号,即不带引号的不可变字符串。

[1]证明:

x = dict(a=1)
y = dict(a=2)

(k1,) = x.keys()
(k2,) = y.keys()

assert id(k1) == id(k2)

[2]这不是真的:

my %x = (a=>1);
my %y = (a=>2);

my ($k1) = keys %x;
my ($k2) = keys %y;

die unless \$k1 == \$k2; # dies

[1]在JSON中,不允许使用此语法,因为必须引用键。我不知道如何证明它们是符号,因为我不知道如何读取变量的内存。

答案 8 :(得分:3)

你实际上并不正确地说python与原子或符号没有相似之处。在python中制作像原子一样的对象并不困难。只要做好对象。普通的空物。例如:

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

TADA! python中的原子!我一直都在使用这个技巧。实际上,你可以走得更远。您可以为这些对象指定一个类型:

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

现在,你的颜色有一个类型,所以你可以做这样的事情:

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

所以,这或多或少等同于lispy符号的功能,以及它们的属性列表。

答案 9 :(得分:2)

在Ruby中,符号通常用作哈希中的键,因此Ruby 1.9甚至引入了构造哈希的简写。你之前写的是:

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

现在可以写成:

{color: :blue, age: 32}

基本上,它们是字符串和整数之间的东西:在源代码中它们类似于字符串,但有很大差异。相同的两个字符串实际上是不同的实例,而相同的符号始终是相同的实例:

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

这会对性能和内存消耗产生影响。而且,它们是不可改变的。分配后不一定要改变。

有争议的经验法则是为每个不用于输出的字符串使用符号而不是字符串。

虽然看起来似乎无关紧要,但大多数代码突出显示的编辑器对颜色符号的颜色与代码的其余部分不同,从而产生了视觉上的区别。

答案 10 :(得分:2)

我在其他语言(例如C)中使用类似概念的问题可以很容易地表达为:

#define RED 1
#define BLUE 2

#define BIG 1
#define SMALL 2

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

导致问题,例如:

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

这两者都没有意义。原子解决了类似的问题,没有上述缺点。

答案 11 :(得分:1)

Atoms提供快速的相等性测试,因为它们使用身份。与枚举类型或整数相比,它们具有更好的语义(为什么你还要用数字表示抽象符号值?)并且它们不限于像枚举这样的固定值集。

折衷方案是创建它们比文字字符串更昂贵,因为系统需要知道所有存在的实例以保持唯一性;这花费了大部分时间用于编译器,但它以O(独特原子数)的形式花费内存。

答案 12 :(得分:1)

原子就像一个开放的枚举,具有无限可能的值,无需预先声明任何内容。这就是他们通常在实践中使用的方式。

例如,在Erlang中,进程期望接收少数几种消息类型之一,并且使用atom标记消息最方便。大多数其他语言都会使用枚举作为消息类型,这意味着无论何时我想发送新类型的消息,我都必须将其添加到声明中。

此外,与枚举不同,可以组合原子值集。假设我想监控我的Erlang进程的状态,我有一些标准的状态监控工具。我可以扩展我的流程以响应状态消息协议以及我的其他消息类型。有了枚举,我该如何解决这个问题?

enum my_messages {
  MSG_1,
  MSG_2,
  MSG_3
};

enum status_messages {
  STATUS_HEARTBEAT,
  STATUS_LOAD
};

问题是MSG_1是0,STATUS_HEARTBEAT也是0.当我收到类型0的消息时,它是什么?对于原子,我没有这个问题。

原子/符号不仅仅是具有恒定时间比较的字符串:)。