DEV Community

niku
niku

Posted on • Edited on

How to write bitstring as read such as 001 1101 on Elixir

UPDATE: Added binary handling with using 0b and :binary.encode_unsigned/1. Thanks for Elixir Forum.

My motivation

I want to write bits like 010 1100 000 0010 (from protocol buffer's encoding sample) on Elixir particular for testing.

Please note, most of cases we have to handle on erlang or elixir is binary which means bitstring to be divisible by 8. If you wanted to write binary as you read, you may use integer literal starting 0b and :binary.encode_unsigned/1 such as:

:binary.encode_unsigned(0b1010_1100_1111_1111)
# => <<172, 255>>
Enter fullscreen mode Exit fullscreen mode

However, I want to handle bitstring which is not divisible by 8, for example 3bits, 7bits, 17bits and so on. binary.encode_unsigned/1 aligns to binary.

bit_size(:binary.encode_unsigned(0b101))
# => 8
Enter fullscreen mode Exit fullscreen mode

In above case, I wanted to get 3 as bit size but 8. So I can't use 0b and :binary.encode_unsigned/1 for "writing what I read".

I know writing bitstring as I read has already be able to do following like:

<<0::1, 1::1, 0::1, 1::1, 1::1, 0::1, 0::1, 0::1, 0::1, 0::1, 0::1, 0::1, 1::1, 0::1>>
# => <<88, 2::size(6)>>
Enter fullscreen mode Exit fullscreen mode

It is correct. Because, in Integer, a representation of 8bits 010 1100 0 is <<88>> (you can check Integer.digits(88, 2) # => [1, 0, 1, 1, 0, 0, 0], please note heading 0s are dropped) and the rest 00 0010 is <<2::size(6)>>.

However, I feel the former <<0::1, 1::1, ...>> is quite verbose and the latter <<88, 2::size(6)>> is not intuitive. Don't you feel so?

I want to write bits as read such as:

010 1100 000 0010 
# => <<88, 2::size(6)>>
Enter fullscreen mode Exit fullscreen mode

My solution

sigil is

one of the mechanisms provided by the language for working with textual representations.

That's I want.

First time, I implement as function following like:

# You can check the code on iex
defmodule BitsSigils do
  require Logger

  def sigil_b(term, _modifiers) do
    for <<c <- term>>,
      into: <<>> do
        case c do
          ^c when c in [?0, ?1] ->
            # It's a valid character as an element.
            <<c::1>>
          ^c when c in [?_, ?\s] ->
            # It's a valid character as a just placeholder.
            <<>>
          _ ->
            Logger.warn("Given unexpected sigil_b character, just ignored. The character was \"#{:binary.encode_unsigned(c)}\", its integer value was #{c}.")
            <<>>
        end
    end
  end
end

import BitsSigils
~b(_010 1100 _000 0010)
# => <<88, 2::size(6)>>
Enter fullscreen mode Exit fullscreen mode

Yeah, It seems to work fine. But it doesn't work as a left side on a pattern matching.

case <<88, 2::size(6)>> do
  ~b(_010 1100 _000 0010) ->
    true
  _ ->
    false
end

# ** (CompileError) iex:16: cannot invoke remote function BitsSigils.sigil_b/2 inside a match
#     (stdlib) lists.erl:1354: :lists.mapfoldl/3
#     (stdlib) lists.erl:1354: :lists.mapfoldl/3
Enter fullscreen mode Exit fullscreen mode

So finally, I implement sigil_b as a macro following like:

# You can check the code on iex
defmodule BitsSigils do
  require Logger

  defmacro sigil_b({:<<>>, meta, [arg]}, modifiers) do
    result = do_sigil_b(arg, modifiers)
    {:<<>>, meta, [Macro.escape(result)]}
  end

  def do_sigil_b(term, _modifiers) do
    for <<c <- term>>,
      into: <<>> do
        case c do
          ^c when c in [?0, ?1] ->
            # It's a valid character as an element.
            <<c::1>>
          ^c when c in [?_, ?\s] ->
            # It's a valid character as a just placeholder.
            <<>>
          _ ->
            Logger.warn("Given unexpected sigil_b character, just ignored. The character was \"#{:binary.encode_unsigned(c)}\", its integer value was #{c}.")
            <<>>
        end
    end
  end
end

import BitsSigils
case <<88, 2::size(6)>> do
  ~b(_010 1100 _000 0010) ->
    true
  _ ->
    false
end
# => true
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • Most of cases you have to handle is binary which is special case of bitstring. In such cases, You may use 0b and :binary.encode_unsigned/1
  • If you want to get something textual representation, you could use sigil.
  • If you want to use sigil at left hand side on an evaluation, you had to use macro instead of function.

Top comments (0)