为什么Elixir中的元组不可枚举?

时间:2016-12-26 16:58:59

标签: arrays performance tuples elixir enumerable

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

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

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

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

    迭代时的
  1. - 1_000为1.02x,1_000_000元素为1.13x
  2. 随机访问 - 1.68x代表1_000,2.48x代表1_000_000
  3. 和复制 - 1.82x表示1_000,6.37x表示1_000_000。
  4. 因此,我的元组代码比地图上的相同代码快5倍。它可能不需要解释为什么元组比map更有效。目标已经实现,但是每个人都告诉他们不要使用元组来获取类似元素的列表,并且没有人可以解释这个规则(例如https://stackoverflow.com/a/31193180/5796559}。

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

    所以,

    1。为什么元组在Elixir中不可枚举?是否存在任何技术或逻辑限制?

    2。 为什么我不应该将它们用作类似元素的列表?有没有缺点?

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

2 个答案:

答案 0 :(得分:6)

1。不为元组实现Enumerable的原因

来自已退休的 Elixir对话邮件列表:

  

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

- Peter Minten

  

我希望元组首先是可枚举的,甚至是   最终在他们身上实施了Enumerable,但没有成功。

- Chris Keele

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

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

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

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

这样Elixir可以保存代码免于愚蠢的错误和复杂的错误,除非你做出错误的架构决策

完全。现在想象我们为元组实现Enumerable。它的作用是让所有元组都可以枚举,而Elixir中99.9%的元组并不是这样的。所有检查都被打破了。悲剧就像世界上所有动物都开始嘎嘎叫一样。如果元组意外地传递给Enum或Stream模块,那么您将看不到有用的错误消息。而不是这样,您的代码将产生意外的结果,不可预测的行为以及可能的数据损坏。

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

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

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

但你不能为元组编写相同的类型规范,因为{type}表示"类型为type"的单个元素的元组。您可以为预定义长度的元组(如{type, type, type})编写typespec,或者为tuple()等任何元素的元组编写,但是没有办法只为设计的类似元素的元组编写typespec。所以选择元组来存储你的elemenets集合意味着你失去了一个很好的能力来使你的代码健壮。

结论

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

如果你真的想因为某种原因想要使用元组作为集合,并且你确定你知道你做了什么,那么将它包装成一些{{3}}是个好主意。您可以为您的结构实现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}}

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

答案 1 :(得分:1)

如果您确定需要在那里使用元组,则可能会以编译时为代价来实现所请求的功能。下面的解决方案将编译很长时间(对于@max_items 1000,考虑≈100s。)一旦编译,执行时间会让你高兴。在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?的实现,因为在您仅请求迭代时编译需要更多时间。