組込みに欠かせない Elixir でのビットの扱い方


これは fukuoka.ex Elixir/Phoenix Advent Calendar 2020 の20日目です。
昨日は @akkiii さんの RubyプロダクトをElixirプロダクトへリプレイスする作業を自動化しよう① fukuoka.ex Elixir/Phoenix Advent Calendar 2020【5分で読める】 でした。

はじめに

一月ぐらい前に「さあアドベントカレンダーの季節だ。しばらく放っておいた LiveView の再訓練をして記事を書くぞ〜」とか思ってたのですが、まあ、ありがたいことに非Elixir業務が千客万来で身動き取れず。アドベントカレンダーのタイトルに Phoenix が入っているところで、また分類が「Webテクノロジー」であるところで大変申し訳ございませんが、Phoenix とは全く独立のネタでございます。ご査収のほど何卒よろしくおねがい申し上げます。

で、何かを書くかというとビット操作の話です。いえね、Elixir / Erlang を使う理由は各自で色々とあるとは思いますが、ビット操作(とパターンマッチの組み合わせ)だけでも使う価値があるのではないかと思うほど便利なんですよ。一番使うシーンは Nerves など使って IoT する場合でしょう。加えて、通信プロトコルの実装を作るときなども大変便利だと思います。

ところがところが、ビット操作だけでキレイにまとまってるドキュメントがあまり無いようなんです。一つには Binary 型が文字列と大変密接に関係するので、文字列の方での扱いがメインになってしまうという嫌いがあるやに思います。そこで自分への備忘録を兼ねてここに書き記しておく次第です。

n進数表記

ビット操作をしていると 2, 8, 16 進数を使いたくなります。それも複数バイトとかビット列とかで表現したくなります。まず、どんな表記が可能かを見ていきます。

n進数のリテラル

まずは 2, 8, 16 進数での表記から。

10進数はそのまま、2進数は 0b を前につけて表現します。

iex(1)> 104
104
iex(2)> 0b1101000
104

これ先頭に 0 を付けても値は変わりません。また任意の場所に _ を挿入することが出来ます。

iex(3)> 0b01101000
104
iex(4)> 0b0110_1000
104
iex(5)> 0b01_101_000
104

8進数は 0o (最初がゼロで次がオーであることに注意)を使います。先頭の 0_ の扱いも一緒です。

iex(6)> 0o150
104
iex(7)> 0o0150
104
iex(8)> 0o000_150
104

16進数は 0x を使います。この x ってのは hexadecimalx と信じてるのですが、どなんでしょね。ここでも先頭の 0_ の扱いは一緒です。

iex(9)> 0x68
104
iex(10)> 0x068
104
iex(11)> 0x0000_0068
104

さてここで注意してほしいのは、上で書いたリテラルは全部 Elixir での普通の整数です。組込みをやってる文脈ですと 0x68 と書くと1バイトで 0x0000_0068 と書くと2バイトのような気がしたりします。しかし実際にはそんなことはなく、いつもどおりの任意桁数の整数になります。次のような例でも、1バイトの大きさに丸められたりはしません。

iex(15)> x = 0x68
104
iex(16)> x + 0x98
256

バイナリ (Binary)

Elixir ではバイト列をバイナリ (binary) と呼びます。組込みでもたいていはバイト単位で入出力を扱うのでバイナリをよく使います。が、これちょっと紛らわしい言葉遣いです。

  • 8bit区切りの列のみを指す(2進数の意味の binary ではない)
  • 文字列と互換性がある

このため、後で出てくるビット列と混乱しやすい上に、何かと文字列扱いにされがちなので注意してください。バイト列で扱いたくても積極的にUTF-8の文字列扱いにされてしまいます。

バイナリ(バイト列)は以下のように <<>> とで挟みます。先程の値を1バイトだけからなるバイナリにしてみます。

iex(14)> <<104>>                        
"h"
iex(15)> <<0b1101000>>
"h"
iex(16)> <<0o150>>    
"h"
iex(17)> <<0x68>> 
"h"

と該当する ASCII のコードで印字されてしまいます。なので以下のように無機質なバイト列を与えたにも関わらず、人間が余計な意味を感じてしまうようなことも起こりえます。

iex(25)> <<104, 0b1100101, 0o154, 0x16c, 111>>
"hello"
iex(26)> <<77, 0b1100101, 0o162, 0x72, 121>>
"Merry"

目的の n進数 で出力する方法はこの後で紹介します。

そしてややこしいことに、このカンマで区切られた一つ々々は1バイトになります。演算の結果が 0〜255 の範囲を超えた場合は丸められます。
以下はわかりにくいですが h の ASCII コードが 104 で、それに 256 を足しても引いても同じ値に丸められることを示しています。下の2つは、英小文字から 32 だけ小さい方にずらすと ASCII コードで大文字が該当するので、丸めた結果が 72 になって大文字の H になってる様子です。

iex(32)> <<104+256>>                                
"h"
iex(33)> <<104-256>>
"h"
iex(34)> <<104+224>>
"H"
iex(35)> <<104-288>>
"H"

なお、この <<>> は特殊な記法ですが、れっきとした <<>>/1 関数扱いであることに驚いてください。そのへんの不思議な文法に基づく記法は他のも含めて Kernel.SpecialForms に記載があります。

長さを測る

以上のようなバイト列の長さを測るには byte_size/1 を用います。

iex(36)> byte_size(<<104, 0b1100101, 0o154, 0x16c, 111>>)    
5

String.length/1 というのもあります。しかしこれ、バイト長を見るのには使わないでください。この関数は文字列リストに対しても使えて、その場合はバイト数ではなく UTF-8 で見た場合の文字数を返します。バイナリは各要素が1バイトなのでバイナリに限っては同じ値を返すと思われますが、何かと混乱のもとになるので避けたほうが賢明です。

ビット列 (Bitstring)

上ではバイト列を扱いましたが、必ずしも8bitでない長さを直接扱いたい場合が出てきます。そういう場合に bit 長を明示して記述する方法があります。

iex(24)> <<0b1101000::size(7)>>
<<104::size(7)>>

はい、今度は h とか言われません。7bit 長のパターンが出来てます。以下のようにも記述できます。

iex(29)> <<104::size(7)>>       
<<104::size(7)>>
iex(30)> <<0o150::size(7)>> 
<<104::size(7)>>
iex(31)> <<0x68::size(7)>> 
<<104::size(7)>>
iex(32)> <<0x0000_0068::size(7)>>
<<104::size(7)>>

几帳面な方向けに size を使いましたが、以下のように省略も出来ます。

iex(38)> <<104::7>>                
<<104::size(7)>>
iex(39)> <<0o150::7>>              
<<104::size(7)>>
iex(40)> <<0x68::7>> 
<<104::size(7)>>

さらにこうも書けます。どれも 7bit 長の 1101000 を表現しています。そしてすごいことに、同じ値をこういう風にも記述できます。

iex(36)> <<0xd::4, 0b000::3>> 
<<104::size(7)>>

これは4bitの 1101 に3bitの 000 をつないだビット列です。さらにこうも書けます。

iex(37)> <<0b110::3, 0o2::2, 0::2>>
<<104::size(7)>>

これは3つのパートから出来てますが 110 10 00 をつなげて 1101000 を構成しています。盛り上がってきましたねぇ。そうこなくっちゃです。

8n bit長でないビット列はバイナリではない

上の7bitの値は先程の1バイトのバイナリと同様のビットパターンのように見えますが、ビット長が違います。試しに先程のバイナリと比較してみましょう。

iex(25)> <<0b1101000>> == <<0b1101000::size(7)>>
false

と、異なる値であることがわかります。これを明示的に 8bit として指定すると同じバイナリになります。

iex(26)> <<0b1101000>> == <<0b1101000::size(8)>>
true

なお byte_size/1 関数で見ると、収容しているバイト長が返ってきます。7bitのビット列は1バイトの箱に収容されているので 1 byte として扱われます。

iex(45)> byte_size(<<0b1101000::size(7)>>)                              
1
iex(46)> byte_size(<<0b1101000::size(8)>>)
1
iex(47)> byte_size(<<0b1101000::size(7)>>) == byte_size(<<0b1101000::size(8)>>)
true

n進数出力

さて、これまで値の出力はバイナリの場合で印字可能な場合は文字列で、そうでない場合は10進数で出力されてきました。当然 2, 8, 16 進数でも出力したいですね。inspect/2 関数や IO.inspect/2 関数で出力可能です。以下では inspect/2 を使ってみます。

iex(74)> inspect(104)
"104"
iex(75)> inspect(104, base: :binary)
"0b1101000"
iex(76)> inspect(104, base: :octal) 
"0o150"
iex(77)> inspect(104, base: :hex)  
"0x68"
iex(78)> inspect(0b1101000, base: :hex)
"0x68"
iex(79)> inspect(0x68, base: :binary)  
"0b1101000"

このように変換可能です。ビット列の場合も同様です。

iex(80)> inspect( <<0b110::3, 0o2::2, 0::2>>, base: :binary)    
"<<0b1101000::size(7)>>"
iex(81)> inspect( <<0b110::3, 0o2::2, 0::2>>, base: :hex)   
"<<0x68::size(7)>>"

第1引数に値が来れば良いので、こんな風にも使えます。

iex(82)> 0b1101000 |> inspect(base: :hex)                   
"0x68"
iex(83)> 0x68 |> inspect(base: :binary)  
"0b1101000"
iex(84)> <<0b110::3, 0o2::2, 0::2>> |> inspect(base: :binary)
"<0b1101000::size(7)>>"

inspect の結果は第1引数の値そのものが出てきます。関数を連ねるパイプの間に挟む場合にも使える inspect 関数らしい便利な使い方が可能です。

なお、ここで取りうるオプションはまだまだあります。詳しくは Inspect.Opts を御覧ください。

パターンマッチ

Elixir / Erlang のバイナリやビット列が強力なのは、当然ながらパターンマッチにも使えることです。まず、先程の1バイトのバイナリでパターンマッチしてみましょう。

iex(113)> x = <<0b1101000>>
"h"
iex(114)> <<a::size(4), b::size(2), c::size(2)>> = x
"h"
iex(115)> IO.inspect(a, base: :binary)              
0b110
6
iex(116)> IO.inspect(b, base: :binary)              
0b10
2
iex(117)> IO.inspect(c, base: :binary)              
0b0
0

と、このように 4bit, 2bit, 2bit でマッチが出来ました。このマッチはより単純に

iex(119)> <<a::4, b::2, c::2>> = x                  
"h"

と書けますので大変簡便にビット単位でのマッチが可能であることがわかります。複数バイトでも同様にマッチさせることが出来ます。

iex(92)>  x = <<104, 0b1100101, 0o154, 0x16c, 111>>
"hello"
iex(93)> <<a::16, b::16, c::8>> = x                
"hello"
iex(94)> IO.inspect(a, base: :hex)                 
0x6865
26725
iex(95)> IO.inspect(b, base: :hex)                 
0x6C6C
27756
iex(96)> IO.inspect(c, base: :hex)                 
0x6F
111

全部をマッチさせるのではなく「先頭1byteだけマッチさせて、残りをマッチ」というのもできます。

iex(126)> <<p::8, rest::binary>> = x     
"hello"
iex(127)> p
104
iex(128)> rest
"ello"

このように、bit長の代わりに binary と記述しておくと可変長マッチをしてくれます。

型の指定

上ではなにげにさらりと binary と書けば可変長でマッチすると書きましたが、厳密には 「rest 変数に binary 型の値が収まるようにマッチするようにせよ」という指示になります。この binary の他にいくつかの型を記述することが可能です。

ここに来ることの出来るオプションの型指定はバリエーションが多く、組込みでもかなり使えそうです。公式ドキュメントの オプションの型指定の例 によると以下のような指示が出来て、これらは全部同じマッチになります。

iex(159)> <<102::integer-native, rest::binary>> = "foo"
"foo"
iex(160)> x = <<0b1100110, 0b1101111, 0b1101111>>      
"foo"
iex(161)> <<102::integer-native, rest::binary>> = x    
"foo"
iex(162)> <<102::native-integer, rest::binary>> = x
"foo"
iex(163)> <<102::unsigned-big-integer, rest::binary>> = x
"foo"
iex(164)> <<102::unsigned-big-integer-size(8), rest::binary>> = x
"foo"
iex(165)> <<102::unsigned-big-integer-8, rest::binary>> = x
"foo"
iex(166)> <<102::8-integer-big-unsigned, rest::binary>> = x
"foo"
iex(167)> <<102, rest::binary>> = x
"foo"

先頭が 102 (= "f") で、残りをいろんな型表現で rest 変数にマッチさせてます。このマッチを一番厳密に表現している型表記が unsigned-big-integer-size(8) です。

  • 符号なし
  • ビッグエンディアン
  • 整数
  • 8bit

でマッチするようにという指示になっています。これらの型は順序に関係なく - で結べば良いことになってます。そして、単に binary とやるとデフォルトがこの設定になります。

詳しくは Kernel.SpecialForms の ::/2 関数の型を参照してください。

浮動小数点表記を解析する

この型指定でデータ型がどう表現されているかも覗けます。例えば「1.0 って浮動小数点形式ではどのように表現されるのだろう」などと思ったとすると、次のように瞬時に解決できます。

iex(132)> <<1.0::float>> |> inspect(base: :binary)
"<<0b111111, 0b11110000, 0b0, 0b0, 0b0, 0b0, 0b0, 0b0>>" 

これをより詳しく、符号、指数部、仮数部、に分けて見てみたいなと思っても一発です。IEEE754の倍精度 では、符号ビットが先頭1bit、指数部が続く11bit、仮数部が残りの52bit、と決まっていますので、以下のように書くとうまくマッチできます。

iex(146)> <<sign::1, exp::11, frac::52>> = <<1.0::float>>    
<<63, 240, 0, 0, 0, 0, 0, 0>>
iex(147)> inspect(sign, base: :binary)                       
"0b0"
iex(148)> inspect(exp, base: :binary) 
"0b1111111111"
iex(149)> inspect(frac, base: :binary)
"0b0"

これ、符号は 0 が正です。指数部が 0b1111111111 (=1023) なのは規格で 1023 だけオフセットするとなってるからで、要は 1023 - 1023 = 0 で指数部は 0 です。仮数部が 000...000 になってるのは、仮数部を正規化したことで先頭に来る 1 を省略する(いわゆる「ケチ表現」にする)ことになってるからで、もとの仮数部のビット列は 1000...000 です。つまり $+1.000...000 \times 2^0$ を表現しているとわかります。

負の数の表現は?とか、2進数だと無限小数になるパターンはどうだろうとか思ってもすぐに分かります。

iex(150)> <<sign::1, exp::11, frac::52>> = <<-1.0::float>>
<<191, 240, 0, 0, 0, 0, 0, 0>>
iex(151)> inspect(sign, base: :binary)                    
"0b1"
iex(152)> inspect(exp, base: :binary)                     
"0b1111111111"
iex(153)> inspect(frac, base: :binary)                    
"0b0"
iex(154)> <<sign::1, exp::11, frac::52>> = <<1/10::float>>
<<63, 185, 153, 153, 153, 153, 153, 154>>
iex(155)> inspect(sign, base: :binary)                    
"0b0"
iex(156)> inspect(exp, base: :binary)                     
"0b1111111011"
iex(157)> inspect(frac, base: :binary)                    
"0b1001100110011001100110011001100110011001100110011010" 

浮動小数点の表現方法を知っていても、実際にビットパターンがどうなるのかを詳細に考えるのは大変です。簡単に具体的にこのように見えると大変おもしろいですね。

応用例

このビット列のパターンマッチは、組込みやってると超絶便利です。以下は私が以前に書いた SPI 通信のプログラム の一部です。

    {:ok, val} = SpiInOut.send_receive(spi_name, inst)
    <<_::size(12), val12bit::size(12)>> = val

SPI 通信はちょっと私には不思議な全二重の通信方式です。どう不思議かというと、送信コマンドが終わり切る前に、結果の値が返り始めるのです。ここで使ってた SPI デバイスはアナログ・デジタルのコンバータで、コマンドの送信途中で変換結果が返り始めます。全体で24bitのレスポンスが得られ、先頭12bitを無視して後ろの12bitを用いると意味を持つ値になります。

このプログラムでは、SPI デバイスから返ってきた値を一旦全部 val 変数に格納し、次の行で先頭12bitを _ にマッチさせて捨ててしまい、残りの12bitを val12bit に格納します。

これを普通のプログラミング言語で書くと、まあ面倒な書き方になりますし、後で見たときの可読性がよろしくありません。が、Elixir ではパターンマッチ一発で記述ができて、可読性も良いです。

これ当然 Elixir ってぐらいで、関数の引数でマッチさせて if を使わずに条件分岐させることも出来ます。
公式ドキュメント に、ファイルの先頭を見て :png か :jpg かを判定するプログラム例がありますので合わせて御覧ください。

まとめ

Elixir / Erlang で超絶便利なビット列の扱いについてまとめてみました。様々な記法が可能な上に、Elixir / Erlang ならではの強力なパターンマッチにも使うことが出来て、特に組込みや通信プロトコルのプログラミングでは便利に使えます。そうですね、Erlang が元々電話交換機用の言語として設計されたという生い立ちを考えると、ビット操作が便利なのも宜なるかな、です。

なお、ビット列の操作をするのには Bitwise というライブラリがあります。こちらも合わせて使うと無敵です。Bitwise についてはまた今度機会があったら書いてみます。

さて、明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2020 の21日目の記事は @GKBR さんの Hello! After World!! 2D - (第0章) です。お楽しみに!

おおっと忘れてた。
<<77, 0b1100101, 0o162, 0x72, 121, 32, 0x58, 0o155, 0b01100001, 0x73, 33>>
そして1
[12450, 12523, 12465, 12511, 12473, 12488, 12398, 12415, 12394, 12373, 12435,
12289, 33391, 12356, 12362, 24180, 12434, 12362, 36814, 12360, 12367, 12384,
12373, 12356] |> IO.puts

参考文献

for との組み合わせが楽しいとの話を頂きました。ちょうど @koga1020 さんが記事をお書きになってましたので先頭にあげておきます。どうぞご参照くださいませ。


  1. 作り方は @torifukukaiou さんの2019年のアドベントカレンダー12月3日なので、一二三、123ダーなElixirのこと を参考にしてください。