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>>
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
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)>>
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 0
s 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)>>
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)>>
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
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
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)