【问题标题】:Comma separated binary arguments? - elixir逗号分隔的二进制参数? - 灵丹妙药
【发布时间】:2019-11-30 17:51:43
【问题描述】:

这个月我一直在学习 elixir,当时我想将二进制对象转换为位列表,以进行模式匹配。

我的研究使我here 找到了一篇文章,展示了这样做的方法。但是,我不完全理解传递给 extract 函数的参数之一。

我可以复制并粘贴代码,但我想了解这里发生了什么。

参数是这样的:<<b :: size(1), bits :: bitstring>>

我的理解

我知道<< x >> 表示二进制对象x。从逻辑上讲,这似乎类似于在 List 上执行:[head | tail] = list,以获取第一个元素,然后将其余元素作为一个名为 tail 的新列表。

我不明白的地方

但是,我对语法并不熟悉,而且我从未在 elixir 中见过::,也从未见过用逗号分隔的二进制对象:,。我也没有见过在 Elixir 中使用size(x),也从未遇到过bitstring

底线


如果有人可以准确地解释这个参数的语法是如何分解的,或者指出我会非常感激的资源。

为方便起见,该文章中的代码:

defmodule Bits do
  # this is the public api which allows you to pass any binary representation
  def extract(str) when is_binary(str) do
    extract(str, [])
  end

  # this function does the heavy lifting by matching the input binary to
  # a single bit and sends the rest of the bits recursively back to itself
  defp extract(<<b :: size(1), bits :: bitstring>>, acc) when is_bitstring(bits) do
    extract(bits, [b | acc])
  end

  # this is the terminal condition when we don't have anything more to extract
  defp extract(<<>>, acc), do: acc |> Enum.reverse
end

IO.inspect Bits.extract("!!") # => [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1]
IO.inspect Bits.extract(<< 99 >>) #=> [0, 1, 1, 0, 0, 0, 1, 1]

【问题讨论】:

    标签: binary elixir


    【解决方案1】:

    您可以阅读所有这些here,简短回答:

    • :: 类似于警卫,如 a when is_integer(a),在你的情况下 size(1) 期望 1 位二进制
    • , 是匹配二进制文件之间的分隔符,例如 [x | []] 中的 |[a, b] 中的逗号
    • bitstring 是二进制文件的超集,您可以阅读它herehere,任何二进制文件都可以表示为位串
    iex> ?h
    104
    iex> ?e
    101
    iex> ?l
    108
    iex> ?o
    111
    iex> <<104, 101, 108, 108, 111>>
    "hello"
    iex> [104, 101, 108, 108, 111]
    'hello'
    

    但反之则不然

    iex> <<1, 2, 3>>
    <<1, 2, 3>>
    

    【讨论】:

    • 很好的答案,也很好地链接到媒体。 Elixir 模式匹配似乎非常容易用于结构化二进制数据。
    • “但反之亦然”非常具有误导性。从 erlang/elixir 的角度来看,&lt;&lt;104, 101, 108&gt;&gt;&lt;&lt;1, 2, 3&gt;&gt; 之间的差异为零;这只是一个可视化问题。更重要的是,可以将charlists: :as_lists 选项传递给IO.inspect/2,以防止整数“字母化”。
    【解决方案2】:

    经过一番研究,我意识到我忽略了一些位于:elixir-lang 的重要信息。

    根据文档,&lt;&lt;x :: size(y)&gt;&gt; 表示一个位串,其十进制值为x,由长度为y 的位串表示。

    此外,&lt;&lt;binary&gt;&gt; 将始终尝试将左优先方向的值合并为 bytes8-bits,但是,如果位数不能被 8 整除,则将有 x 字节,然后通过位串。

    例如:

    iex> <<3::size(5), 5::size(6)>> # <<00011, 000101>>
    <<24, 5::size(3)>>  # automatically shifted to: <<00011000(24) , 101>>
    

    现在,elixir 还允许我们对二进制文件和位串进行模式匹配,如下所示:

    iex> <<3 :: size(2), b :: bitstring>> = <<61 :: size(6)>> # [11] [1101]
    iex> b
    <<13 :: size(4)>> # [1101]
    

    所以,我完全误解了二进制文件和双字符串,以及两者之间的模式匹配。

    【讨论】:

    • &lt;&lt;24, 5::size(3)&gt;&gt; > 将始终尝试以左优先方向合并值-- 显示二进制(= 二进制类型和位串类型)在 iex 中,一次一个字节,末尾的任何部分字节以位串表示法表示。另一个例子:&lt;&lt;3::size(5), 5::size(6), 1::size(5)&gt;&gt; => &lt;&lt;24, 161&gt;&gt;。 24 和 161 是从哪里来的?我插入了 3、5 和 1! => 00011, 000101, 00001。现在删除逗号:0001100010100001。现在一次显示 8 位 = 1 个字节:00011000 10100001, 和 iex(1)&gt; 0b00011000 => 24, iex(2)&gt; 0b10100001 => 161
    • 我想将二进制对象转换为位列表以进行模式匹配。是的,没必要那样做。
    • @7stud 我想在 8 位二进制文​​件中使用这些位来切换条件。所以[b1, b2, ...] = extract(&lt;&lt;binary&gt;&gt;) 然后我想说,if b1, do: x.... if b2, do: y... 有没有更好的方法来做我想做的事情,而不是模式匹配?
    • 我发布了另一个答案来解决您的问题。
    【解决方案3】:

    Elixir 模式匹配似乎非常易于使用 结构化二进制数据。

    是的。你可以感谢 erlang 的发明者。

    根据文档,&lt;&lt;x :: size(y)&gt;&gt; 表示一个位串, 谁的十进制值为 x 并由一串位表示 y 长度。

    让我们稍微简化一下:&lt;&lt;x :: size(y)&gt;&gt; 是插入到 y 位的整数 x。例子:

    <<1 :: size(1)>>  => 1
    <<1 :: size(2)>>  => 01
    <<1 :: size(3)>>  => 001
    <<2 :: size(3)>>  => 010
    <<2 :: size(4)>>  => 0010
    

    binary 类型中的位数可以被 8 整除,因此二进制类型具有整数字节数(1 字节 = 8 位)。 bitstring 中的位数不能被 8 整除。这就是 binary 类型和 bitstring 类型之间的区别。

    我理解 > 表示二进制对象 x。逻辑上对我来说, 看起来这类似于执行:[head |尾] = 列表 在 List 上,获取第一个元素,然后将其余元素作为 名为 tail 的新列表。

    是的:

    defmodule A do
    
      def show_list([]), do: :ok
      def show_list([head|tail]) do
        IO.puts head
        show_list(tail)
      end
    
      def show_binary(<<>>), do: :ok
      def show_binary(<<char::binary-size(1), rest::binary>>) do
        IO.puts char
        show_binary(rest)
      end
    
    end
    

    在 iex 中:

    iex(6)> A.show_list(["a", "b", "c"])    
    a
    b
    c
    :ok
    
    iex(7)> "abc" = <<"abc">> = <<"a", "b", "c">> = <<97, 98, 99>>
    "abc"
    
    iex(9)> A.show_binary(<<97, 98, 99>>)   
    a
    b
    c
    :ok
    

    或者您可以将二进制中的整数解释为普通的旧整数:

      def show(<<>>), do: :ok
    
      def show(<<ascii_code::integer-size(8), rest::binary>>) do
        IO.puts ascii_code
        show(rest)
      end
    

    在 iex 中:

    iex(6)> A.show(<<97, 98, 99>>)            
    97
    98
    99
    :ok
    

    utf8 类型非常有用,因为它会抓取尽可能多的字节来获取整个 utf8 字符:

      def show(<<>>), do: :ok
    
      def show(<<char::utf8, rest::binary>>) do
        IO.puts char
        show(rest)
      end
    

    在 iex 中:

    iex(8)> A.show("ۑ")
    8364
    235
    :ok
    

    如您所见,uft8 类型返回字符的 unicode 代码点。将字符作为字符串/二进制获取:

      def show(<<>>), do: :ok
      def show(<<codepoint::utf8, rest::binary>>) do
        IO.puts <<codepoint::utf8>>
        show(rest)
      end
    

    您获取代码点(一个整数)并使用它来创建二进制/字符串&lt;&lt;codepoint::utf8&gt;&gt;

    在 iex 中:

    iex(1)> A.show("ۑ")
    €                                                          
    ë
    :ok
    

    但是,您不能为 utf8 类型指定大小,因此如果要读取多个 utf8 字符,则必须指定多个段。

    当然,段 rest::binary,即没有指定大小的 binary 类型,非常有用。它只能出现在模式的末尾,rest::binary 就像贪婪的正则表达式:(.*)rest::bitstring 也是如此。

    尽管 elixir 文档在任何地方都没有提到它,但片段中的 total number of bits 是其中之一:

         |              |          |    
         v              v          v
    << 1::size(8), 1::size(16), 1::size(1) >>
    

    实际上是unit * size,其中每种类型都有一个默认的unit。段的默认类型是integer,因此上面每个段的类型默认为integer。整数的默认unit 为1 位,因此第一段的总位数为:8 * 1 bit = 8 bitsbinary 类型的默认 unit 为 8 位,因此段如下:

    << char::binary-size(6)>>
    

    总大小为6 * 8 bits = 48 bits。等效地,size(6) 只是字节数。您可以指定unit,就像您可以指定size,例如&lt;&lt;1::integer-size(2)-unit(3)&gt;&gt;。该段的总位大小为:2 * 3 bits = 6 bits

    但是,我不熟悉语法

    看看这个:

      def bitstr2bits(bitstr) do
        for <<bit::integer-size(1) <- bitstr>>, do: bit
      end
    

    在 iex 中:

    iex(17)> A.bitstr2bits <<1::integer-size(2), 2::integer-size(2)>>   
    [0, 1, 1, 0]
    

    等价:

    iex(3)> A.bitstr2bits(<<0b01::integer-size(2), 0b10::integer-size(2)>>)
    [0, 1, 1, 0]
    

    Elixir 倾向于使用库函数抽象出递归,因此通常您不必像在链接中那样提出自己的递归定义。但是,该链接显示了标准的基本递归技巧之一:将 accumulator 添加到函数调用以收集您希望函数返回的结果。该函数也可以这样写:

      def bitstr2bits(<<>>), do: [] 
      def bitstr2bits(<<bit::integer-size(1), rest::bitstring>>) do
        [bit | bitstr2bits(rest)]
      end
    

    链接处的累加器函数是尾递归,这意味着它占用了恒定(少量)的内存——无论需要多少次递归函数调用来逐步遍历位串。一千万位的位串?需要 1000 万次递归函数调用?那只需要少量的内存。在过去,我发布的替代定义可能会使您的程序崩溃,因为它会为每个递归函数调用占用越来越多的内存,并且如果位串足够长,则所需的内存量会太大,您会获取 stackoverflow 并且您的程序会崩溃。但是,erlang 已经优化掉了非尾递归的递归函数的缺点。

    【讨论】:

    • 天哪。很好的解释。
    【解决方案4】:

    并不是问题的真正答案,但为了格式化,我将其放在这里。在 中,我们通常使用Kernel.SpecialForms.for/1 理解来生成位串。

    for << b :: size(1) <- "!!" >>, do: b
    #⇒ [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1]
    
    for << b :: size(1) <- <<99>> >>, do: b
    #⇒ [0, 1, 1, 0, 0, 0, 1, 1]
    

    【讨论】:

    • @AlekseiMatiushkin 哇!这非常有见地!
    【解决方案5】:

    我想使用 8 位二进制文​​件中的位来切换条件。所以

    [b1, b2, ...] = extract(<<binary>>) 
    

    然后我想说:

    if b1, do: x.... 
    if b2, do: y... 
    

    有没有更好的方法来做我想做的事情,而不是模式 匹配?

    首先,在 elixir 中评估为 false 的唯一术语是 falsenil(就像在 ruby​​ 中一样):

    iex(18)> x = 1
    1
    
    iex(19)> y = 0
    0
    
    iex(20)> if x, do: IO.puts "I'm true."
    I'm true.
    :ok
    
    iex(21)> if y, do: IO.puts "I'm true."
    I'm true.
    :ok
    

    虽然,修复很简单:

    if b1 == 1, do: ...
    

    不需要将位提取到列表中,因为您可以迭代位串:

      def check_bits(<<>>), do: :ok
      def check_bits(<<bit::integer-size(1), rest::bitstring>>) do
        if bit == 1, do: IO.puts "bit is on"
        check_bits(rest)
      end
    

    换句话说,您可以将位串视为列表。

    或者,代替在函数体中执行逻辑来确定位是否为1,您可以在函数头部使用模式匹配:

      def check_bits(<<>>), do: :ok
    
      def check_bits(<< 1::integer-size(1), rest::bitstring >>) do
        IO.puts "The bit is 1."
        check_bits(rest)
      end
    
      def check_bits(<< 0::integer-size(1), rest::bitstring >>) do
        IO.puts "The bit is 0."
        check_bits(rest)
      end
    

    不要使用变量 bit 来进行匹配,如下所示:

      bit::integer-size(1)
    

    ...您使用文字值,1:

      1::integer-size(1)
    

    唯一可以匹配文字值的是文字值本身。结果,模式:

    << 1::integer-size(1), rest::bitstring >>
    

    只会匹配第一个位 integer-size(1)1 的位串。此处使用的 literal 匹配类似于使用列表执行以下操作:

      def list_contains_4([4|_tail]) do
        IO.puts "found a 4"
        true  #end the recursion and return true
      end
    
      def list_contains_4([head|tail]) do
        IO.puts "#{head} is not a 4"
        list_contains_4(tail)
      end
    
      def list_contains_4([]), do: false
    

    第一个函数子句尝试匹配列表头部的文字4。如果列表头不是4,则不匹配;所以 elixir 移动到下一个函数子句,在下一个函数子句中,变量 head 将匹配列表中的任何内容。

    在函数头部使用模式匹配而不是在函数体中执行逻辑在 erlang 中被认为更时尚、更高效。

    【讨论】:

      猜你喜欢
      • 2018-06-12
      • 2019-02-15
      • 2018-10-11
      • 1970-01-01
      • 2010-10-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多