在 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
模块和递归的情况下要求解决列表反向问题。
在elixir中,是元编程来解决这类任务。它使代码干净、简洁且易于管理。
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}
# 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
模块和递归的情况下要求解决列表反向问题。
在elixir中,是元编程来解决这类任务。它使代码干净、简洁且易于管理。
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}