エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

ビットとバイトとストリングと in Elixir

Elixir でビット操作を行うコードを書いたんですが、使うべき関数やガードで混乱しそうになったので、明日の自分のためにまとめてみました。

ガード

# 入力 is_binary/1 is_bitstring/1
1 "あいう" true true
2 <<12, 34>> true true
3 <<12::5, 34::5>> false true

is_binary/1 はバイト列(ビット長が 8 の倍数になっているビット列)の場合に true になります。 上記の 3 番目は 10 ビットのビット列なので false を返します。

全体で 8 の倍数であれば true になりますので、次のように書いても true になります。

iex> is_binary(<<12::3, 34::5>>)
true

パタンマッチング

パタンマッチングでも binary で受ける場合には、そのビット長が 8 の倍数になっている必要があります。 端数があるとエラーになります。

iex> <<a, rest::binary>> = ""
""

iex> a
227

iex> rest
<<129, 130>>
iex> <<a, rest::binary>> = <<123, 234>>
<<123, 234>>

iex> a
123

iex> rest
<<234>>
iex> <<a, rest::binary>> = <<123::5, 234::5>>
** (MatchError) no match of right hand side value: <<218, 2::size(2)>>

任意の長さのビット列を受けるには bitstring を指定します。

iex> <<a, rest::bitstring>> = ""
""

iex> a
227

iex> rest
<<129, 130>>
iex> <<a, rest::bitstring>> = <<123, 234>>
<<123, 234>>

iex(79)> a
123

iex(80)> rest
<<234>>
iex(81)> <<a, rest::bitstring>> = <<123::5, 234::5>>
<<218, 2::size(2)>>

iex(82)> a
218

iex(83)> rest
<<2::size(2)>>

長さとサイズ

# 入力\関数 String.length/1 byte_size/1 bit_size/1
1 "あいう" 3 9 72
2 <<123, 234>> 2 2 16
3 <<12::5, 34::5>> エラー*1 2 10

*1 ** (FunctionClauseError) no function clause matching in String.Unicode.length/1

String.lenth/1UTF-8 としての文字数を返します。 ビット列の長さが 8 の倍数でない場合、エラーが発生します。

byte_size/1 はバイト数を返しますが、興味深いのはビット列の長さが 8 の倍数でないばあいでもエラーにはならず、8 で割って切り上げた数字を返す点です。

ちなみに lengthsize という名前について。 実行が線形時間のものには length を、定数時間のものには size をつけるようにしてるとのこと。

また、byte_size/1bit_size/1 はガードに記述できるためか、ドキュメントでは Functions でなく Guards に記載されています。

# 内容
1 binary() バイト列(長さが 8 の倍数のビット列)
2 bitstring() ビット列
3 String.t() 文字列(binary()エイリアス
4 string() Erlang の文字列(文字リスト charlist() に同じ)

"string" という単語が 3 回も出てくるので、うろ覚えでいると足をすくわれそうです。

String.t()binary() と同じものをさしていますが、値が文字列(UTF-8 エンコーディングのバイナリ)であることをドキュメントで明確にしたいばあいのために用意されているようです。

また string()Erlang の文字列型であるため、Elixir 内では charlist() を使うように勧められています。

やりたかったこと

「任意のビット列を任意のサイズで分割し、左詰めでバイト列にしたリストを返す」という機能を実現すべく四苦八苦していました。

defmodule Bin do
  @spec chunk_every(bitstring(), pos_integer()) :: [binary()]
  def chunk_every(bin, n) when is_bitstring(bin) and is_integer(n) and n > 0 do
    padding_size = rem(8 - rem(n, 8), 8)

    bin
    |> Stream.unfold(fn
      <<>> ->
        nil

      <<chunk::size(n), rest::bitstring>> ->
        {<<chunk::size(n), 0::size(padding_size)>>, rest}


      chunk ->
        chunk_size = bit_size(chunk)
        padding_size = n - chunk_size + padding_size
        {<<chunk::bitstring, 0::size(padding_size)>>, <<>>}
    end)
    |> Enum.to_list()
  end
end

たとえば。 60 ビットのビット列を 12 ビットごとに分割し、分割したそれぞれのビットを左詰めしたバイト列のリストとして取得したいばあい。

このばあい、12 ビットのビット列は 16 ビット = 2 バイトのバイト列に収まるので、2 バイトのバイト列のリストが返ります。

iex> Bin.chunk_every(<<0x123456789abcdef::60>>, 12)                    
[<<18, 48>>, "E`", <<120, 144>>, <<171, 192>>, <<222, 240>>]

結果がわかりにくいので、パタンマッチングで確認。

iex> Bin.chunk_every(<<0x123456789abcdef::60>>, 12) == [<<0x12, 0x30>>, <<0x45, 0x60>>, <<0x78, 0x90>>, <<0xab, 0xc0>>, <<0xde, 0xf0>>]
true

いつか読むはずっと読まない: ≥ 1.6

第 2 版の邦訳が出版されていることを、つい最近になって知りました。 不覚。 買わねば。

プログラミング Elixir(第2版)

プログラミング Elixir(第2版)

  • 作者:Thomas,Dave
  • 発売日: 2020/12/01
  • メディア: 単行本