バイナリデータの扱いは、どの言語でも重要な概念です。Elixirは、バイナリデータを記述し、マッチさせ、構築するためのエレガントな構文を提供しています。
Elixirでは、バイナリデータはビット文字列型と呼ばれます。バイナリデータ_型_(一般的なバイナリデータと混同しないでください)はビット文字列の特定の形式であり、これについては後の演習で説明します。
ビット文字列リテラルは、ビット文字列の特殊形式<<>>を使って定義します。ビット文字列リテラルを定義するときは、セグメント単位で定義します。各セグメントは値と型を持ち、それらは::演算子で区切られます。型は、その値をエンコードするために何ビット使うかを指定します。型は完全に省略でき、その場合は8ビットの整数値が既定になります。
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>>
型を::1と指定するのは、::size(1)と書くための省略記法です。ビットサイズが変数から来る場合は、長いほうの構文を使う必要があります。
バイナリの整数リテラルを書くときは、リテラルの先頭に0bを付けることで、2進数表記で直接書くことができます。なお、テスト結果の出力やiexでの表示では、いずれにせよ10進数として表示されることに注意してください。
<<0b1011::4>> == <<11::4>>
# => true
セグメントの値がそのセグメントの型の容量を超えると、左から切り捨てられます。
<<0b1011::3>> == <<0b0011::3>>
# => true
特殊形式を使えば、既存のビット文字列の先頭にも末尾にも追加できます。既存のビット文字列のサイズが不明な場合は、::bitstring型を使う必要があります。
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
特殊形式を使えば、変数に格納されたビット文字列を連結できます。サイズが不明な2つのビット文字列を連結するときは、::bitstring型を使う必要があります。
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
特殊形式から値を取り出すために、パターンマッチングを行うこともできます。取り出したい各フラグメントのビット数を知っておく必要がありますが、1つだけ例外があります。::bitstring型はサイズが不明なビット文字列に対してパターンマッチングに使えますが、これは最後のフラグメントにしか使えません。
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
ビット文字列は、(テストランナーやiexによって)作成時に使った形式とは異なる形式で表示されることがあります。このことが、ビット文字列を学ぶときにしばしば混乱を招きます。
既定では、ビット文字列は8ビット(1バイト)のフラグメントで表示されます。たとえ異なるサイズのフラグメントで作成したとしてもです。
<<2011::11>>
# => <<251, 3::size(3)>>
表示可能なUTF-8エンコードされた文字列を表すビット文字列を作成すると、それは文字列として表示されます。
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
ペイントバイナンバー(別名「カラーバイナンバー」)は、色を塗るための白黒の絵です。 絵の各部分にはそれぞれ違う数字が書かれていて、その数字は、あらかじめ決められたカラーパレットの中の特定の色に対応しています。 目標は、各部分を正しい色で塗り、最後に美しいカラフルな絵を浮かび上がらせることです。 子どもにも大人にも、リラックスできる活動です。
Elixirでペイントバイナンバーのアプリを書くことになりました。 このアプリでは、独自のデータ形式で絵をインポートしたりエクスポートしたりできるようにしたいと思います。 絵のデータを保存するには、バイナリファイルを使うことにしました。
この演習では、2進数に慣れていて、2進数を10進数に変える仕組みと、10進数を2進数に変える仕組みを理解していることを前提としています。
下のようなスマイリーの絵を想像してみましょう。 絵の背景は白です。スマイリーには黒い輪郭があり、塗りつぶしの色は黄色です。
この絵では3色使います。 これらの色にインデックスを割り当ててみましょう。
0(2進数: 0b00)は白、1(2進数: 0b01)は黒、2(2進数: 0b10)は黄色。これで、これらの色のインデックスを使って、各ピクセルの色を表せます。
| スマイリー | 色のインデックスを付けたスマイリー |
|---|---|
![]() |
![]() |
PaintByNumber.palette_bit_size/1関数を実装します。この関数は、パレットの色数を受け取り、その数の色のインデックスを2進数で表すのに必要なビット数を返します。色のインデックスは常に0から始まり、連続して増える整数です。
たとえば、13種類の色を表すには4ビットが必要です。4ビットなら最大16個の色のインデックスを格納できます(2^4)。3ビットでは足りません。3ビットで格納できる色のインデックスは最大8個だからです(2^3)。
PaintByNumber.palette_bit_size(13)
# => 4
注意: Elixirの標準ライブラリにはlog2関数がありません。この関数が見つかる、ElixirからErlangライブラリを使う方法については、あとで学びます。ここでは、再帰とべき乗演算子**を使ってこのタスクを解きましょう。
PaintByNumber.empty_picture/0関数を実装します。空のビット列を返します。
アプリの基本的な機能を手動でテストするために、あらかじめ用意されたテスト用の絵を使います。 テスト用の絵は、4つの異なる色を持つ4つのピクセルでできています。
PaintByNumber.test_picture/0関数を実装します。4つのセグメントからなるビット列を返します。
各セグメントのビットサイズは2です。セグメントの値は0、1、2、3です。
PaintByNumber.prepend_pixel/3関数を実装します。この関数は3つの引数を取ります。追加先の絵のビット列、パレットの色数、新しいピクセルの色のインデックスです。新しいピクセルを先頭に追加した絵のビット列を返します。
picture = <<2::4, 0::4>>
color_count = 13
pixel_color_index = 11
PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index)
# => <<178, 0::size(4)>>
# (which is equal to <<11::4, 2::4, 0::4>>)
PaintByNumber.get_first_pixel/2関数を実装します。この関数は2つの引数を取ります。読み取り元の絵のビット列と、パレットの色数です。与えられた絵の最初のピクセルの色のインデックスを返します。空の絵が渡された場合はnilを返します。
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
PaintByNumber.drop_first_pixel/2関数を実装します。この関数は2つの引数を取ります。ピクセルを削除する絵のビット列と、パレットの色数です。最初のピクセルを除いた絵を返します。空の絵が渡された場合は、空の絵を返します。
picture = <<2::3, 5::3, 5::3, 0::3>>
color_count = 6
PaintByNumber.drop_first_pixel(picture, color_count)
# => <<180, 0::size(1)>>
# (which is equal to <<5::3, 5::3, 0::3>>)
PaintByNumber.concat_pictures/2関数を実装します。2つのビット列を引数に取ります。第1引数を第2引数の先頭に追加した結果のビット列を返します。
picture1 = <<52::6, 51::6>>
picture2 = <<0::6, 34::6, 12::6>>
PaintByNumber.concat_pictures(picture1, picture2)
# => <<211, 48, 34, 12::size(6)>>
# (which is equal to <<52::6, 51::6, 0::6, 34::6, 12::6>>)