lists:map 在 Erlang 中有副作用
lists:map with side-effects in Erlang
我有一个 ID 批次(子列表)列表,我想遍历此列表并为 ID 批次中的每个 ID 生成一个工作进程。这些工作人员中的每一个都将查询一些服务,获取结果并将其发送回调用者。简而言之,我想将 id
的列表映射到我凭借这些 id
获得的数据列表。我设法做到了这一点,但我认为这是一种不合常理的方式:
lists:map(fun(Ids) ->
Pids = [spawn_link(fun() ->
Result = [...] % Here goes a side-effect operation (http request)
Self ! {received_data, process(Result)}
end) || Id <- Ids],
[receive {received_data, Data} -> Data end || _Pid <- Pids],
end, JobChunks)))
在这种情况下,如您所见,我滥用了 map
函数,因为它被设计为没有副作用。但我没有看到另一种选择。有 foreach()
但它仅用于 运行 副作用并且仅用于 returns ok
而在我的情况下我也想保留列表的形状。在 Haskell 中有一个方便的类型-class Traversable
with traverse
function 正是这样做的: 运行s fmap
同时允许您对每个项目执行操作(效果)。 Erlang 中有类似的东西吗? (也许像 smap
?)。
Erlang 与 Haskell 不同,它不是 纯 函数式编程语言。作为必然结果,它不会根据函数是否可以产生副作用来限制函数。在 Haskell 中,甚至 I/O 子系统也不能破坏其纯度,这就是为什么 Traversable
和 Functor
之间存在类型级别的区别(traverse
和fmap
) 其中前者可以 运行 影响容器的每个元素,而后者不能。在 Erlang 中没有这样明显的区别,因此,您可能有一个函数 execute(Container) ->
并且您仅通过注视它的签名就不知道它是否会 运行 效果。这就是为什么在 Erlang 中使用 map
和 smap
(或 traverse
,或随便你怎么称呼它)没有任何意义,也不会带来任何价值。但是对于这种操作使用 lists:map
确实违反了 map
的契约,它应该是一个纯函数。在这种情况下,我可能会建议您使用列表理解,在我看来 是一种更惯用的方式:
[begin
Pids = [spawn_link(fun() ->
% Side-effect operation which worker performs
end) || Id <- Ids],
[receive {received_data, Data} -> Data end || _Pid <- Pids]
end || Ids <- JobChunks].
同样 在我自己的观点中 副作用是列表理解和 lists:map()
之间的主要区别。当以上述方式使用它们时,我通常将它们视为 Haskell 的单子理解。
我喜欢@Oleksandr 的回答,但是在列表理解中使用 begin..end 块感觉有点 脏 。我会为此使用函数。
同样重要的是要注意,他的回答的第二部分并不能保证尊重原始列表的顺序(即它只是具有相同的元素数量,但它们将根据其中的顺序进行排序他们到了)。这对您来说可能没问题,但如果您希望能够匹配输入 (Id) 和输出 (Results),则必须使用选择性接收,我将在下面向您展示。
所以,这就是我在没有 OTP 的情况下实现它的方式(因为你也不使用 OTP):
your_function() ->
[process_chunk(Ids) || Ids <- JobChunks].
process_chunk(Ids) ->
Pids = [spawn_side_effect_fun(Id) || Id <- Ids],
[get_result_for(Pid) || _Pid <- Pids].
spawn_side_effect_fun(Id) ->
Self = self(),
spawn_link(fun() ->
Self ! {received_data, self(), your_side_effect_operation()}
end).
get_result_for(Pid) ->
receive
%% Here we're pattern-matching on Pid
%% so that we get the result for this particular Pid
%% therefore the order is preserved in the final list.
{received_data, Pid, Data} -> Data
end.
请注意,您并未在此处处理任何错误,这一点也很重要。由于您没有捕获退出,因此衍生进程中的错误只会杀死主要进程。
我有一个 ID 批次(子列表)列表,我想遍历此列表并为 ID 批次中的每个 ID 生成一个工作进程。这些工作人员中的每一个都将查询一些服务,获取结果并将其发送回调用者。简而言之,我想将 id
的列表映射到我凭借这些 id
获得的数据列表。我设法做到了这一点,但我认为这是一种不合常理的方式:
lists:map(fun(Ids) ->
Pids = [spawn_link(fun() ->
Result = [...] % Here goes a side-effect operation (http request)
Self ! {received_data, process(Result)}
end) || Id <- Ids],
[receive {received_data, Data} -> Data end || _Pid <- Pids],
end, JobChunks)))
在这种情况下,如您所见,我滥用了 map
函数,因为它被设计为没有副作用。但我没有看到另一种选择。有 foreach()
但它仅用于 运行 副作用并且仅用于 returns ok
而在我的情况下我也想保留列表的形状。在 Haskell 中有一个方便的类型-class Traversable
with traverse
function 正是这样做的: 运行s fmap
同时允许您对每个项目执行操作(效果)。 Erlang 中有类似的东西吗? (也许像 smap
?)。
Erlang 与 Haskell 不同,它不是 纯 函数式编程语言。作为必然结果,它不会根据函数是否可以产生副作用来限制函数。在 Haskell 中,甚至 I/O 子系统也不能破坏其纯度,这就是为什么 Traversable
和 Functor
之间存在类型级别的区别(traverse
和fmap
) 其中前者可以 运行 影响容器的每个元素,而后者不能。在 Erlang 中没有这样明显的区别,因此,您可能有一个函数 execute(Container) ->
并且您仅通过注视它的签名就不知道它是否会 运行 效果。这就是为什么在 Erlang 中使用 map
和 smap
(或 traverse
,或随便你怎么称呼它)没有任何意义,也不会带来任何价值。但是对于这种操作使用 lists:map
确实违反了 map
的契约,它应该是一个纯函数。在这种情况下,我可能会建议您使用列表理解,在我看来 是一种更惯用的方式:
[begin
Pids = [spawn_link(fun() ->
% Side-effect operation which worker performs
end) || Id <- Ids],
[receive {received_data, Data} -> Data end || _Pid <- Pids]
end || Ids <- JobChunks].
同样 在我自己的观点中 副作用是列表理解和 lists:map()
之间的主要区别。当以上述方式使用它们时,我通常将它们视为 Haskell 的单子理解。
我喜欢@Oleksandr 的回答,但是在列表理解中使用 begin..end 块感觉有点 脏 。我会为此使用函数。
同样重要的是要注意,他的回答的第二部分并不能保证尊重原始列表的顺序(即它只是具有相同的元素数量,但它们将根据其中的顺序进行排序他们到了)。这对您来说可能没问题,但如果您希望能够匹配输入 (Id) 和输出 (Results),则必须使用选择性接收,我将在下面向您展示。
所以,这就是我在没有 OTP 的情况下实现它的方式(因为你也不使用 OTP):
your_function() ->
[process_chunk(Ids) || Ids <- JobChunks].
process_chunk(Ids) ->
Pids = [spawn_side_effect_fun(Id) || Id <- Ids],
[get_result_for(Pid) || _Pid <- Pids].
spawn_side_effect_fun(Id) ->
Self = self(),
spawn_link(fun() ->
Self ! {received_data, self(), your_side_effect_operation()}
end).
get_result_for(Pid) ->
receive
%% Here we're pattern-matching on Pid
%% so that we get the result for this particular Pid
%% therefore the order is preserved in the final list.
{received_data, Pid, Data} -> Data
end.
请注意,您并未在此处处理任何错误,这一点也很重要。由于您没有捕获退出,因此衍生进程中的错误只会杀死主要进程。