为啥元组在 Elixir 中不可枚举?

Posted

技术标签:

【中文标题】为啥元组在 Elixir 中不可枚举?【英文标题】:Why tuples are not enumerable in Elixir?为什么元组在 Elixir 中不可枚举? 【发布时间】:2017-05-11 02:06:04 【问题描述】:

我需要一个有效的结构来存储数千个相同类型的元素,并且能够进行随机访问。

虽然列表在迭代和前置方面效率最高,但随机访问速度太慢,因此不符合我的需求。

地图效果更好。但是,它会导致一些开销,因为它适用于键可能是任何东西的键值对,而我需要一个索引从 0 到 N 的数组。因此,我的应用程序在地图上的运行速度太慢。对于像处理随机访问的有序列表这样简单的任务,我认为这是不可接受的开销。

我发现元组是 Elixir 中对我的任务最有效的结构。与我机器上的地图相比,它更快

    迭代时 - 1_000 为 1.02x,1_000_000 元素为 1.13x 关于随机访问 - 1_000 为 1.68x,1_000_000 为 2.48x 在复制时 - 1_000 为 2.82x,1_000_000 为 6.37x。

因此,我在元组上的代码比在地图上的相同代码快 5 倍。它可能不需要解释为什么 tuple 比 map 更有效。目的达到了,但是大家都说“不要对相似元素的列表使用元组”,没有人能解释这个规则(这种情况的例子https://***.com/a/31193180/5796559)。

顺便说一句,Python 中有元组。它们也是不可变的,但仍然是可迭代的。

所以,

1.为什么元组在 Elixir 中不可枚举?有任何技术或逻辑限制吗?

2.为什么我不应该将它们用作相似元素的列表?有什么缺点吗?

请注意:问题是“为什么”,而不是“如何”。上面的解释只是一个例子,元组比列表和映射更好。

【问题讨论】:

查看这篇文章-***.com/a/31193180/5796559 我不同意“不要将元组用于相似元素的列表”。你有链接到有人说这个吗?也许上下文不同?我过去使用过元组,我希望通过索引快速随机访问列表(与您的用例相同),这对我的用例来说是一个巨大的加速,因为索引变为 O(1) 而不是 O(n)。 @KeithA 那篇文章没有回答我的问题。 @Dogbert KeithA 上面发布的链接包含这样的例子,人们说“元组不应该被迭代”。否则为什么它们不可枚举?顺便说一句,您还可以通过用地图替换列表来实现巨大的加速。 @Ninigi 我对大约 100_000 个元素进行随机访问。这种长度的列表比随机访问的元组慢 3700 倍。所以我什至不尝试使用列表。我已经尝试过比我的任务慢 5 倍的地图。我还没有尝试过 :array ,但它在 Elixir 中也是不可枚举的,所以用 :array 替换元组对代码可读性没有意义。 【参考方案1】:

1。不为 Tuple 实现 Enumerable 的原因

来自退休 Elixir 谈话邮件列表:

如果有 元组的协议实现将与所有记录冲突。 鉴于协议的自定义实例几乎总是 为添加元组的记录定义将使整个 Enumerable 协议比较没用。

-- Peter Minten

一开始我希望元组是可枚举的,甚至 最终在它们上实现了 Enumerable,但没有成功。

-- Chris Keele

这如何破坏协议?我会试着把事情放在一起,从技术角度解释问题。

元组。元组的有趣之处在于,它们主要用于一种duck typing,使用pattern matching。每次您想要一些新的简单类型时,都不需要为新的struct 创建新模块。取而代之的是,您创建了一个元组——一种虚拟类型的对象。原子通常用作类型名称的第一个元素,例如:ok, result:error, description。这就是在 Elixir 中几乎任何地方都使用元组的方式,因为这是它们的设计目的。它们也被用作来自 Erlang 的“records”的基础。 Elixir 有用于此目的的结构,但它也提供模块 Record 以与 Erlang 兼容。因此,在大多数情况下,元组表示不打算枚举的异构数据的单一结构。元组应该被视为各种虚拟类型的实例。甚至还有@type 指令允许基于元组定义自定义类型。但请记住它们是虚拟的,is_tuple/1 仍然为所有这些元组返回 true。

协议。 另一方面,Elixir 中的协议是一种type classes,它提供ad hoc polymorphism。对于那些来自 OOP 的人来说,这类似于 superclasses and multiple inheritance。协议为您做的一件重要事情是自动类型检查。当您将一些数据传递给协议函数时,它会检查数据是否属于此类,即该协议是针对该数据类型实现的。如果不是,那么您将收到如下错误:

** (Protocol.UndefinedError) protocol Enumerable not implemented for 

除非您做出错误的架构决策,否则 Elixir 可以让您的代码免于愚蠢的错误和复杂的错误

总共。 现在想象我们为 Tuple 实现 Enumerable。它所做的是使所有元组都可枚举,而 Elixir 中 99.9% 的元组并非如此。所有的支票都坏了。悲剧就像世界上所有的动物都开始嘎嘎叫一样。如果元组被意外传递给 Enum 或 Stream 模块,那么您将看不到有用的错误消息。取而代之的是,您的代码会产生意想不到的结果、不可预知的行为和可能的数据损坏。

2。不使用元组作为集合的原因

良好健壮的 Elixir 代码应包含 typespecs 以帮助开发人员理解代码,并让 Dialyzer 能够为您检查代码。想象一下,你想要一个相似元素的集合。列表和地图的类型规范可能看起来像 like this:

@type list_of_type :: [type]
@type map_of_type :: %optional(key_type) => value_type

但是您不能为元组编写相同的类型规范,因为type 的意思是“type 类型的单个元素的元组”。您可以为预定义长度的元组(如 type, type, type)或任何元素的元组(如 tuple())编写 typespec,但无法仅通过设计为类似元素的元组编写 typespec。所以选择元组来存储你的元素集合意味着你失去了让你的代码健壮的能力。

结论

不使用元组作为相似元素列表的规则是一个经验法则,它解释了在大多数情况下如何在 Elixir 中选择正确的类型。违反此规则可被视为设计选择不当的可能信号。当人们说“元组不是为了设计而设计的集合”时,这不仅意味着“你做了一些不寻常的事情”,而且“你可以通过在应用程序中进行错误的设计来破坏 Elixir 的功能”。

如果您出于某种原因真的想将元组用作集合,并且您确定自己知道自己在做什么,那么最好将其包装成 struct。您可以为您的结构实现 Enumerable 协议,而不会破坏元组周围的所有事物。值得注意的是,Erlang 使用元组作为 arraygb_treesgb_sets 等内部表示的集合。

iex(1)> :array.from_list ['a', 'b', 'c']
:array, 3, 10, :undefined,
 'a', 'b', 'c', :undefined, :undefined, :undefined, :undefined, :undefined,
  :undefined, :undefined

不确定是否有任何其他技术原因不使用元组作为集合。如果有人可以为 Record 和 Enumerable 协议之间的冲突提供另一个很好的解释,欢迎他改进这个答案。

【讨论】:

【参考方案2】:

由于您确定需要在那里使用元组,因此您可能会以编译时间为代价来实现所请求的功能。下面的解决方案将编译很长时间(考虑 ≈100s for @max_items 1000。)一旦编译,执行时间会让你高兴。 Elixir 核心中使用了相同的方法来构建最新的 UTF-8 字符串匹配器。

defmodule Tuple.Enumerable do
  defimpl Enumerable, for: Tuple do
    @max_items 1000

    def count(tuple), do: tuple_size(tuple)

    def member?(_, _), do: false # for the sake of compiling time

    def reduce(tuple, acc, fun), do: do_reduce(tuple, acc, fun)

    defp do_reduce(_,       :halt, acc, _fun),   do: :halted, acc
    defp do_reduce(tuple,   :suspend, acc, fun)  do
      :suspended, acc, &do_reduce(tuple, &1, fun)
    end
    defp do_reduce(,      :cont, acc, _fun),   do: :done, acc
    defp do_reduce(value, :cont, acc, fun)     do
      do_reduce(, fun.(value, acc), fun)
    end

    Enum.each(1..@max_items-1, fn tot ->
      tail = Enum.join(Enum.map(1..tot, & "e_★_#&1"), ",")
      match = Enum.join(["value"] ++ [tail], ",")
      Code.eval_string(
        "defp do_reduce(#match, :cont, acc, fun) do
           do_reduce(#tail, fun.(value, acc), fun)
         end", [], __ENV__
      )
    end)

    defp do_reduce(huge,    :cont, _, _) do
      raise Protocol.UndefinedError, 
            description: "too huge #tuple_size(huge) > #@max_items",
            protocol: Enumerable,
            value: Tuple
    end
  end
end

Enum.each(:a, :b, :c, fn e ->  IO.puts "Iterating: #e" end)
#⇒ Iterating: a
#  Iterating: b
#  Iterating: c

上面的代码明确地避免了member? 的实现,因为当您只请求迭代时编译会花费更多时间。

【讨论】:

@OnorioCatenacci 坦率地说,这个问题让我实现了上述目标。对于 @max_items 设置为 30,这很有意义,我可能会在我的项目中使用此代码。要使这段代码健壮,唯一需要做的就是优雅地回退到Tuple.to_list 来处理巨大的元组。它可能对迭代休息响应特别有用。所以我不会结束这个问题,它有一个价值恕我直言。 我会关闭它,因为我认为他实际上是在问“为什么元组不能枚举?”他并不是在寻求解决方法——已经提供了几种可行的解决方法。他在问为什么该语言的设计方式是这样的——这是一个有效的问题,但它不在 Stack Overflow 上适合提出的问题范围之内。我仍然认为他的问题应该结束。 我担心这对于@max_items=100_100 等来说编译时间太长了。这可能是不良做法的一个例子(请参阅我对您的另一个答案的评论)。

以上是关于为啥元组在 Elixir 中不可枚举?的主要内容,如果未能解决你的问题,请参考以下文章

我可以在这个 Elixir 匿名函数中避免元组参数吗?

协议可枚举未实现 - Elixir

Elixir:如何使结构可枚举

为啥在 elixir 中定义命名函数有两种方式?

Elixir Enum vs Erlang列表

在 Elixir 中,为啥在导入模块时“别名”优于“导入”?