kinkou
kinkou

Reputation: 303

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>>

Why is this happening?

In short, I'd like to pattern match bitstrings in a cycle, rather than writing method definitions by hand. So instead of this:

@bmp_signature <<66, 77>>
…

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

…something like this:

@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

Is this solvable without metaprogramming?

Upvotes: 2

Views: 696

Answers (3)

kinkou
kinkou

Reputation: 303

Erlang's :binary.match/2 is the right tool for the job:

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"

A big thank you goes to @Everett for his advice to look elsewhere.

Upvotes: -1

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121020

Asking whether this is doable without metaprogramming is like asking to solve a list reverse without both Enum module and recursion.

In , there is metaprogramming to solve exactly this kind of task. It makes the code clean, succinct, and manageable.

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

It’s still can be done in an ugly not idiomatic way without metaprogramming, though.

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}

Upvotes: 3

Everett
Everett

Reputation: 9638

Your syntax is slightly off. Remember that the pin operator ^ pins only a single value. In your example, you were trying to pin it to 2 values.

So if the thing you are trying to match on is a binary with 2 values that you are aware of, then you would need to pin both of them, e.g.

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>>

The binary syntax <<>> isn't the only way to do this -- you can accomplish the same with regular strings (assuming the values are in fact strings):

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

The rub here is that you can't pin a prefix because you need a literal value in order to do the match. This is because the length of the binary isn't known beforehand.

If you know your "signatures" always have 2, 3, or 4 characters, you can code your variables to be pinned appropriately. However, if you must deal with a an unknown length, then you'd probably need to rely on something more like a regular expression or String.starts_with?/2 or similar.

Upvotes: 2

Related Questions