elixirpattern-matchingbitstring

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?


Solution

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