Concurrency 为什么我们必须导出spawn使用的函数?

Concurrency 为什么我们必须导出spawn使用的函数?,concurrency,erlang,Concurrency,Erlang,在Erlang中,在处理进程时,必须导出spawn函数中使用的函数 -module(echo). -export([start/0, loop/0]). start() -> spawn(echo, loop, []). “编程Erlang,第二版,第188页”一书中的原因是 “请注意,我们还必须从模块中导出spawn参数。这是一个很好的做法,因为我们可以在不更改客户端代码的情况下更改服务器的内部详细信息。” 在《Erlang编程》一书第121页: -module(frequenc

在Erlang中,在处理进程时,必须导出spawn函数中使用的函数

-module(echo).
-export([start/0, loop/0]).

start() ->
  spawn(echo, loop, []).
“编程Erlang,第二版,第188页”一书中的原因是

“请注意,我们还必须从模块中导出spawn参数。这是一个很好的做法,因为我们可以在不更改客户端代码的情况下更改服务器的内部详细信息。”

在《Erlang编程》一书第121页:

-module(frequency).
-export([start/0, stop/0, allocate/0, deallocate/1]). 
-export([init/0]).  

%% These are the start functions used to create and 
%% initialize the server.

start() ->
   register(frequency, spawn(frequency, init, [])).

init() ->
   Frequencies = {get_frequencies(), []}, 
   loop(Frequencies).
请记住,在生成进程时,必须导出init/0函数,因为它由spawn/3 BIF使用。我们将此函数放在一个单独的export子句中,以区别于客户端函数,后者应该从其他模块调用。


请你给我解释一下这个原因背后的逻辑好吗

如果你想知道为什么你必须导出任何东西,而不是在默认情况下让所有东西都可见,那是因为如果你隐藏了所有不应该调用的函数,用户会更清楚应该调用哪些函数。这样,如果您改变了对实现的想法,那么使用您的代码的人不会注意到。否则,可能有人正在使用您想要更改或删除的函数

例如,假设您有一个模块:

-module(somemod).

useful() ->
    helper().
helper() ->
    i_am_helping.
您想将其更改为:

-module(somemod).

useful() ->
    betterhelper().
betterhelper() ->
    i_am_helping_more.

如果人们只需要调用
有用的
,您应该能够进行此更改。然而,如果所有的东西都被导出,人们可能会依赖于
helper
,而他们不应该依赖。此更改将在不应该的情况下破坏他们的代码。

简短的回答是:spawn不是“语言构造”,而是库函数

这意味着“spawn”位于另一个模块中,该模块无权访问模块中的任何函数,但已导出


您必须以某种方式传递到“spawn”函数才能启动代码。它可以是函数值(即
spawn(fun()->(您想要的任何代码,包括任何本地函数调用)end)
)或模块/导出的函数名/参数,从其他模块可见。

当您执行
生成时,您将创建一个全新的新的进程,该进程具有自己的环境和执行线程。这意味着您不再在调用
spawn
的模块的“内部”执行,因此必须对模块进行“外部”调用。模块中唯一可以从“外部”调用的函数是导出函数,因此必须导出派生函数

看到您在同一个模块中生成一个函数似乎有点奇怪,但这就是原因

我认为重要的是要记住,模块只是代码,不包含任何更深层次的含义,例如像OO语言中的类。因此,即使同一模块的函数在不同的进程中执行,非常常见,它们之间也没有隐含的联系。即使是同一模块中的from/to函数,也必须在进程之间发送消息

编辑:

关于问题的最后一部分,请引用关于将export
init/1
放在单独的出口声明中的内容。不需要这样做,也没有语义意义,您可以使用任意多或任意少的导出声明。因此,您可以将所有函数放在一个导出声明中,或者为每个函数单独设置一个导出声明;这没什么区别

拆分它们的原因纯粹是为了直观和文档目的。您通常将函数分组到单独的导出声明中,以便更容易地看到它们是一个组。您还通常将“内部”导出函数(用户不直接调用的函数)放入单独的导出声明中。在这种情况下,必须为spawn导出
init/1
,但不能直接在spawn外部调用


通过让用户调用
start/0
函数来启动服务器,而不是让他们显式生成
init/1
函数,您可以在以后根据需要更改内部构件。用户只看到
start/0
功能。这正是第一句话想要表达的意思。

逻辑非常简单。然而,混淆很容易出现,因为:

  • 导出不完全匹配面向对象的封装,尤其是公共方法
  • 有几种常见的模式需要导出常规客户端不需要调用的函数
出口的真正作用是什么 导出有一个非常严格的含义:导出的函数是唯一可以通过其完全限定名(即模块、函数名和算术)引用的函数

例如:

-module(m).
-export([f/0]).
f() -> foo.
f(_Arg) -> bar.
g() -> foobar.
您可以使用诸如
m:f()
之类的表达式调用第一个函数,但这不适用于其他两个函数
m:f(确定)
m:g()
将失败并出现错误

由于这个原因,编译器将在上面的示例中警告f/1和g/0没有被调用,也不能被调用(它们是未使用的)

始终可以从模块外部调用函数:函数是值,您可以引用本地函数(在模块内部),并将该值传递到外部。例如,您可以使用非导出函数生成新进程,方法是使用。您可以将示例改写如下:

start() ->
    spawn(fun loop/0).
这不需要导出循环。Joe Armstrong在其他版本的Erlang编程中明确建议如上所述转换代码,以避免导出
循环/0

需要导出的常见模式 由于导出是从模块外部按名称引用函数的唯一方式,因此有两种常见模式需要导出函数,即使这些函数不是公共API的一部分

你提到的例子是
-module(m).
-export([start/0]).
start() -> spawn(fun() -> loop(state) end).
loop(State) ->
    NewState = receive ...
    ...
    end,
    loop(NewState). % not updatable !
-module(m).
-export([start/0]).
-export([loop/1]).
start() -> spawn(fun() -> loop(state) end).
loop(State) ->
    NewState = receive ...
    ...
    end,
    ?MODULE:loop(NewState).