在 Elixir 中使用 pin 运算符匹配位串的模式

Pattern matching a bitstring using pin operator in Elixir

# Erlang/OTP 24, Elixir 1.12.3

bmp_signature = <<66, 77>>
#=> "BM"

<<^bmp_signature, _::binary>> = <<66, 77, 30, 0>>
#=> ** (MatchError) no match of right hand side value: <<66, 77, 30, 0>>

为什么会这样?

简而言之,我想在一个循环中对位串进行模式匹配,而不是手动编写方法定义。所以不是这个:

@bmp_signature <<66, 77>>
…

def type(<<@bmp_signature, _::binary>>), do: :bmp
…

……像这样:

@signatures %{
  "bmp" => <<66, 77>>,
  …
}

def detect_type(file) do
  find_value_func = fn {extension, signature} ->
    case file do
      <<^signature, _::binary>> -> extension
      _ -> false
    end
  end

  Enum.find_value(@signatures, find_value_func)
end

这是否可以在没有元编程的情况下解决?

你的语法有点不对劲。请记住,固定运算符 ^ 仅固定一个值。在您的示例中,您试图将其固定到 2 个值。

因此,如果您要匹配的对象是您知道的具有 2 个值的二进制文件,那么您需要将它们都固定下来,例如

iex> <<bmp_sig1, bmp_sig2>> = <<66, 77>>
iex> <<^bmp_sig1, ^bmp_sig2, rest::binary>> = <<66, 77, 88, 23, 44, 89>>
<<66, 77, 88, 23, 44, 89>>
iex> rest
<<88, 23, 44, 89>>

二进制语法 <<>> 不是执行此操作的唯一方法 - 您可以使用常规字符串完成相同的操作(假设值实际上是字符串):

iex> x = "apple"
"apple"
iex> "ap" <> rest = x
"apple"
iex> rest
"ple"

这里的问题是您不能 pin 前缀,因为您需要 literal 值才能进行匹配。这是因为事先不知道二进制文件的长度。

如果您知道您的“签名”总是有 2、3 或 4 个字符,则可以对变量进行编码以适当固定。但是,如果您必须处理未知长度,那么您可能需要依赖更像正则表达式或 String.starts_with?/2 或类似的东西。

Erlang 的 :binary.match/2 是完成这项工作的正确工具:

defmodule Type do
  @signatures [
    {"bmp", <<66, 77>>},
    {"jpg", <<255, 216, 255>>},
  ]

  def detect(file_binary) do
    find_value_func = fn {extension, signature} ->
      case :binary.match(file_binary, signature) do
        {0, _} -> extension
        _ -> nil
      end
    end

    Enum.find_value(@signatures, find_value_func)
  end
end

Type.detect(<<66, 77, 0, 0>>) #=> "bmp"
Type.detect(<<55, 66, 0, 0>>) #=> nil
Type.detect(<<255, 216, 255, 0>>) #=> "jpg"

非常感谢@Everett 提出的寻找其他地方的建议。

在没有元编程的情况下询问这是否可行就像在没有 Enum 模块和递归的情况下要求解决列表反向问题。

中,元编程来解决这类任务。它使代码干净、简洁且易于管理。

defmodule Type do
  @signatures %{
    bmp: <<66, 77>>
  }

  Enum.each(@signatures, fn {name, bom} ->
    def detect(<<unquote(bom), _::binary>>), do: unquote(name)
  end)
  def detect(_), do: nil
end

Type.detect(<<66, 77, 30, 0>>)
#⇒ :bmp
Type.detect(<<66, 30, 0>>)
#⇒ nil

尽管如此,如果没有元编程,它仍然可以以一种丑陋而不是惯用的方式完成。

defmodule Type do
  @signatures [
    {<<66, 77>>, :bmp},
    {<<77, 66>>, :pmb}
  ]
  
  def detect(signature, candidates \ @signatures) do
    for <<c <- signature>>, reduce: @signatures do
      acc -> 
        Enum.reduce(acc, [], fn
          {"", name}, acc -> [{"", name} | acc]
          {<<^c, rest::binary>>, name}, acc -> [{rest, name} | acc]
          _, acc -> acc
        end)
    end
  end
end

case Type.detect(<<66, 77, 30, 0>>) do
  [{"", type}] -> {:ok, type}
  [] -> {:error, :none}
  [few] -> {:error, few: few}
end
#⇒ {:ok, :bmp}