M.Hiroi's Home Page

Functional Programming

お気楽 Erlang プログラミング入門

[ PrevPage | Erlang | NextPage ]

バイナリ

今回は「バイナリ (binary)」というデータ型について説明します。Erlang のバイナリは、バイナリデータ (binary data) を扱うためのデータ構造です。基本的には 1 byte (要素の値は 0 から 255 まで) の 1 次元配列ですが、ビット単位でも操作できるようになっています。

●バイナリの生成

バイナリは次の式で生成することができます。

<< 値:サイズ/タイプ, ... >>

値はデフォルトで整数、サイズはビット幅を指定します。整数以外の値を使用する場合はタイプを指定します。サイズの指定を省略した場合は 8 bit (1 byte) になります。

簡単な例を示します。

> <<1, 2, 3, 4>>.
<<1,2,3,4>>

> <<16#0102:16, 5, 6>>.
<<1,2,5,6>>

> <<1:4, 2:4, 3, 4>>.
<<18,3,4>>

最初の例は要素が 1, 2, 3, 4 のバイナリになります。次の例は、最初の要素が 16 bit の値 16#0102 なので、上位 8 bit と下位 8 bit に分けて格納され、バイナリの値は <<1,2,5,6>> になります。最後の例は、2 つの要素が 4 bit なので、1 つのバイトデータ 18 にまとめられて、大きさが 3 のバイナリデータになります。

バイナリの要素は文字列または文字でも指定することができます。簡単な例を示します。

> <<"abc">>.
<<"abc">>

> <<$a, $b, $c>>.
<<"abc">>

バイナリの大きさは組み込み関数 (BIF) の size/1 で求めることができます。

> size(<<1, 2, 3, 4>>).
4
> size(<<1, 2, 3, 4, 5, 6, 7, 8>>).
8

●タイプの指定方法

タイプで指定できる属性の種類を下記に示します。複数の属性を指定するときはハイフン (-) で区切ります。

簡単な例を示しましょう。

> <<1.2345/float>>.
<<63,243,192,131,18,110,151,141>>

> <<123, 5.6789/float, <<1, 2, 3, 4>>/binary>>.
<<123,64,22,183,49,143,197,4,129,1,2,3,4>>

> <<16#01020304:32>>.
<<1,2,3,4>>

> <<16#01020304:4/unit:8>>.
<<1,2,3,4>>

> <<16#01020304:4/native-unit:8>>.
<<4,3,2,1>>

> <<16#01020304:4/little-unit:8>>.
<<4,3,2,1>>

> <<16#01020304:4/big-unit:8>>.
<<1,2,3,4>>

UTF-8 が扱える端末であれば日本語を表示することもできますが、バイナリで日本語を使用するときはタイプに utf8 を指定する必要があります。werl +pc uncode での実行例を示します。

> "あいうえお".
"あいうえお"

> io:format('~p~n', ["あいうえお"]).
[12354,12356,12358,12360,12362]
ok

> <<"あいうえお">>.
<<"BDFHJ">>

> <<"あいうえお"/utf8>>.
<<"あいうえお"/utf8>>

format で日本語 (utf8) を表示するときは ~s, ~p の代わりに ~ts, ~tp を使います。

> io:format('~ts~n', ["あいうえお"]).
あいうえお
ok
> io:format('~tp~n', ["あいうえお"]).
"あいうえお"

> A = <<"あいうえお"/utf8>>.
<<"あいうえお"/utf8>>
> io:format('~p~n', [A]).       
<<227,129,130,227,129,132,227,129,134,227,129,136,227,129,138>>
ok
> io:format('~tp~n', [A]).
<<"あいうえお"/utf8>>
ok

●ビットストリング

バイナリは 1 byte (8 bit) が標準の単位で、ビットの総数は 8 の倍数になりますが、それ以外のデータ (ビットの総数が 8 の倍数にはならないデータ) でも取り扱うことができます。Erlang ではこれを「ビットストリング (bitstring)」といいます。基本的な操作はバイナリと同じですが、パターンマッチングで動作が異なる場合があります。

簡単な例を示します。

> <<1:2>>.
<<1:2>>

> <<1,2,3,4:4>>.
<<1,2,3,4:4>>

> <<1:4,2,3,4>>.
<<16,32,48,4:4>>

最初の例は 2 bit のビットストリングになります。次の例は、最後のデータが 4 bit なので、28 bit のビットストリングになります。最後の例は、最初のデータが 4 bit なので、2 番目以降のデータが 4 bit ずつ左へシフトされて <<16,32,48,4:4>> となります。

なお、関数 size/1 にビットストリングを適用すると、バイト単位での大きさを返します。ビット単位で大きさを求める場合は組み込み関数 bit_size/1 を使ってください。

> size(<<1:4>>).
0
> size(<<1,2,3,4:4>>).
3
> bit_size(<<1:4>>).
4
> bit_size(<<1,2,3,4:4>>).
28
12> bit_size(<<1,2,3,4>>).
32

●バイナリのパターンマッチング

パターンマッチングはバイナリでも行うことができます。次の例を見てください。

> <<A, B, C, D>> = <<1, 2, 3, 4>>.
<<1,2,3,4>>
> A.
1
> B.
2
> C.
3
> D.
4

> <<X:4,Y:4>> = <<129>>.
<<129>>
> X.
8
> Y.
1

> A1 = <<255, 1.2345/float, <<1, 2, 3, 4>>/binary>>.
<<255,63,243,192,131,18,110,151,141,1,2,3,4>>

> <<X1, Y1/float, Z1/binary>> = A1.
<<255,63,243,192,131,18,110,151,141,1,2,3,4>>
> X1.
255
> Y1.
1.2345
> Z1.
<<1,2,3,4>>

バイナリで整数以外のデータとパターンマッチングするときは、/ の後ろにタイプを指定してください。これでバイナリの要素が float や binary でもパターンマッチングで取り出すことができます。また、ビット幅を指定して値を取り出すこともできます。

リストのパターンマッチングのように、先頭の要素と残りのデータとに分けてマッチングさせることも可能です。次の例を見てください。

> <<H, R/binary>> = <<1, 2, 3, 4, 5>>.
<<1,2,3,4,5>>
> H.
1
> R.
<<2,3,4,5>>

先頭のデータをバイト単位ではなくビット単位で取り出すことも可能です。この場合、残りのデータはビットストリングになるので、タイプには bitstring を指定します。また、ビットストリングからバイトデータを取り出す場合も、残りのデータがビットストリングになるので bitstring を指定します。簡単な実行例を示します。

> <<Hb:4, Rb/bitstring>> = <<16, 2, 3, 4, 5>>.
<<16,2,3,4,5>>

> Hb.
1
> Rb.
<<0,32,48,64,5:4>>

> <<Hb1, Rb1/bitstring>> = <<1, 2, 3, 4, 5:4>>.
<<1,2,3,4,5:4>>

> Hb1.
1
> Rb1.
<<2,3,4,5:4>>

この場合、Rb と Rb1 に binary を指定するとマッチングでエラーになります。

●ビット演算子

ここで Erlang に用意されているビット演算子について説明しておきましょう。

表 : ビット演算子
演算子機能
bandビットごとの論理積を返す
bor ビットごとの論理和を返す
bxorビットごとの排他的論理和を返す
bnotビットごとの論理的な否定を返す
bslm bsl n は m を n ビットだけ左シフトする
bsrm bsr n は m を n ビットだけ右シフトする

band はビットごとの論理積を返します。

> 5 band 3.
1
     0101
 and 0011
---------
     0001

bor はビットごとの論理和を返します。

> 5 bor 3.
7
    0101
 or 0011
--------
    0111

bxor はビットごとの排他的論理和を返します。

> 5 bxor 3.
6
     0101
 xor 0011
---------
     0110

bnot はビットごとの論理的な否定を返します。

> bnot 0.
-1
> bnot 1.
-2

m bsl n は m を n ビット左シフトします。m bsr n は m を n ビット右シフトします。

> 1 bsl 8.
256
> 256 bsr 4.
16

●バイナリの内包表記

Erlang はリストだけではなくバイナリにも「内包表記」を使うことができます。基本的な構文を次に示します。

<< <<式1>> || <<パターン>> <= 式2, 条件式 >>

<< >> の最初にバイナリの要素を生成する式1 (コンストラクタ) を記述します。次に || で区切ったあと、"<<パターン>> <= 式2" を記述します。式2の値はバイナリでなければなりません。式1とパターンは << >> で囲ってください。式2はバイナリをそのまま指定してもかまいません。<= はバイナリの先頭から順番に要素を取り出し、パターンとマッチングを行います。また、カンマで区切ったあと、条件式を指定することができます。この条件式はガードとは違って、真偽値を返す関数であれば何でもかまいません。

簡単な例を示しましょう。

> << <<(X * 2)>> || <<X>> <= <<1, 2, 3, 4>> >>.
<<2,4,6,8>>

> << <<X>> || <<X:4>> <= <<1, 2, 3, 4>> >>.
<<0,1,0,2,0,3,0,4>>

> << <<Y:4, X:4>> || <<X:4, Y:4>> <= <<1, 2, 3, 4>> >>.
<<16,32,48,64>>

最初の例はパターンが <<X>> なので、バイナリから 8 bit ずつデータを取り出して変数 X にセットします。次に X * 2 を計算して、その値がバイナリの要素になります。2 番目の例は、パターンが <<X:4>> なので、バイナリから 4 bit ずつデータを取り出します。その値がそのままバイナリの要素になるので、生成されるバイナリの大きさは 8 byte になります。最後の例は、上位 4 bit と下位 4 bit を反転させたバイナリを生成します。

パターンで指定したビット幅が式で指定したビット幅よりも大きい場合、上位のビットが捨てられて下位のビットが有効になる事に注意してください。次の例を見てください。

> << <<X>> || <<X:16>> <= <<1, 2, 3, 4, 5, 6, 7, 8>> >>.
<<2,4,6,8>>

パターンは <<X:16>> なので、バイナリから 16 bit ずつデータを取り出します。式は <<X>> なので、生成する要素は 8 bit になります。この場合、上位 8 bit は捨てられて、値は下位 8 bit になります。したがって、生成されるバイナリは <<2,4,6,8>> になるわけです。

●バイナリとリストの変換

バイナリの内包表記で、"<<パターン>> <= 式" のかわりにリスト内包表記の "パターン <- 式" を使うことができます。逆に、リスト内包表記で "<<パターン>> <= 式" を使うこともできます。これにより、バイナリからリストへの変換、およびその逆変換を行うことができます。

簡単な例を示します。

> << <<X>> || X <- [1, 2, 3, 4, 5] >>.
<<1,2,3,4,5>>

> [X || <<X>> <= <<1, 2, 3, 4, 5>>].
[1,2,3,4,5]

> << <<X>> || X <- [1, 2, 1000, 4, 5] >>.
<<1,2,232,4,5>>

> << <<X:16>> || X <- [1, 2, 1000, 4, 5] >>.
<<0,1,0,2,3,232,0,4,0,5>>

> << <<X>> || X <- [1, 2, a, 4, 5] >>.
** exception error: bad argument

リストからバイナリに変換する場合、リストの要素の値に注意してください。値がビット幅に収まらない場合、余分な上位ビットは捨てられて下位ビットがバイナリの要素になります。また、数値以外のデータを与えるとエラーになります。

なお、Erlang の組み込み関数 (BIF) にはリストをバイナリに変換する関数 list_to_binary と、バイナリをリストに変換する関数 binary_to_list が用意されています。簡単な実行例を示します。

> list_to_binary([1, 2, 3, 4, 5]).
<<1,2,3,4,5>>
> binary_to_list(<<1, 2, 3, 4, 5>>).
[1,2,3,4,5]

このほかに、モジュール binary にはバイナリを操作するための便利な関数が用意されています。また、モジュール erlang にもデータをバイナリに変換する関数やその逆変換を行う関数があります。詳細は Erlang のリファレンスマニュアル Erlang -- binary, Erlang -- erlang をお読みください。


初出 2011 年 11 月 20 日
改訂 2019 年 1 月 6 日

ファイル入出力

今回は Erlang のファイル入出力について説明します。Erlang は「ポート (port)」というデータ型を介して外部と通信処理を行います。ファイルの入出力処理もポートを介して行われますが、モジュール io, file などに用意されている関数を使うと、ポートを意識しないで入出力処理を行うことができます。

●標準入出力

通常のファイルは、ファイルに対応するポートを生成しないとアクセスすることはできません。ただし、標準入出力は Erlang を起動した時から簡単に利用することができます。一般に、キーボードからの入力を「標準入力」、画面への出力を「標準出力」といいます。

データの入出力処理は標準入出力を使うと簡単です。モジュール io には標準入出力用の関数が用意されています。たとえば、io:write や io:format はデータを標準出力へ出力する関数でした。関数 io:read/1 は Erlang で取り扱うことができる「項」を入力することができます。

io:read(Prompt) => {ok, Data} | eof | {error, Reason}

引数 Prompt は画面に表示するプロンプトで、文字列かアトムで指定します。省略することはできません。read はファイルの終了を検出すると eof を返します。エラーが発生した場合は {error, Reason} を返します。

簡単な実行例を示しましょう。

> io:read(">> ").
>> 1234.
{ok,1234}
> io:read(">> ").
>> 1.2345.
{ok,1.2345}
> io:read(">> ").
>> foo.
{ok,foo}
> io:read(">> ").
>> [1,2,3,4].
{ok,[1,2,3,4]}
> io:read(">> ").
>> {a, b, c, d}.
{ok,{a,b,c,d}}
> io:read(">> ").
>> "hello, world".
{ok,"hello, world"}

データの終わりにはピリオドを入力してください。read は Erlang の構文規則にしたがって標準入力からデータを読み込み、それを項に変換して返します。

●get_line

標準入力から 1 行ずつ読み込む場合、関数 io:get_line/1 を使うと便利です。

io:get_line(Prompt) => Data | eof | {error, Reason}

引数 Prompt は画面に表示するプロンプトで、文字列かアトムで指定します。省略することはできません。

簡単な実行例を示します。

> io:get_line(">> ").
>> hello, world
"hello, world\n"

hello, world と入力してリターンキーを押すと、get_line は入力データを文字列 (リスト) にして返します。このとき、改行文字もいっしょに文字列に格納されます。また、get_line はファイルの終了を検出すると eof を返します。エラーが発生した場合は {error, Reason} を返します。

それでは簡単な例題として、入力をそのままエコーバックする関数 echo/0 を作ってみましょう。プログラムは次のようになります。

リスト : エコーバック

-module(iotest).
-export([echo/0]).

echo() -> 
    case io:get_line("> ") of
        "\n" -> ok;
        Line -> io:fwrite(Line), echo()
    end.

標準入力から get_line で 1 行読み込み、それを io:fwrite で標準出力へ出力します。fwrite は io:format と同じ働きをする関数です。第 1 引数に文字列を与えると、それをそのまま出力します。それから echo を再帰呼び出します。これで echo は無限ループになります。入力された文字列が "\n" の場合は処理を終了します。

簡単な実行例を示します。

> iotest:echo().
> hello, world
hello, world
> foo bar baz
foo bar baz
> 1234567890
1234567890
>
ok

終了する場合はリターンキーだけを押してください。最後の ok は echo/0 の返り値です。

●ファイルのオープンとクローズ

ファイルにアクセスする場合、次の 3 つの操作が基本になります。

  1. アクセスするファイルをオープンする
  2. 入出力関数を使ってファイルを読み書きする。
  3. ファイルをクローズする。

「ファイルをオープンする」とは、アクセスするファイルを指定して、それと 1 対 1に対応するポートを生成することです。入出力関数はオープンしたポートを経由してファイルにアクセスします。Erlang の場合、ファイルのオープンは関数 file:open/2 で行います。

file:open(Filename, Mode) => {ok, IoDevice} | {error, Reason}

open/2 は引数にファイル名 Filename とアクセスモード Mode を指定して、Filename で指定されたファイルに対応する IoDevice を生成して返します。Erlang ではファイルの入出力処理をプロセスで行っていて、open はそのファイルの入出力を行うプロセスの識別子 (Pid) を返します。このほかに「ファイルディスクプリタ」という IoDevice もありますが、本稿では触れません。

アクセスモード Mode はリストで、要素はアクセスモードを表すアトムになります。要素がひとつしかない場合、リストではなくアトムをそのまま指定してもかまいません。下表に主なアクセスモードを示します。

表 : アクセスモード
モード動作
read 読み込み (read) モード
write 書き出し (write) モード
append 追加 (append) モード
binary バイナリの指定

読み込みモードの場合、ファイルが存在しないとエラーになります。書き出しモードの場合、ファイルが存在すれば、そのファイルを大きさ 0 に切り詰めてからオープンします。追加モードの場合、ファイルの最後尾にデータを追加します。binary は取り扱うデータをバイナリデータに設定します。

なお、Windows では「テキストモード」と「バイナリモード」の区別がありますが、Erlang の基本はバイナリモードです。Erlang の場合、行単位で入出力を行う関数において改行の変換が行われます。

オープンしたファイルは必ずクローズしてください。この操作を行う関数が file:close/1 です。

file:close(IoDevice) => ok | {error, Reason}

●ファイルの読み込み

ファイルの読み込みはモジュール file に定義されている関数のほかに、モジュール io の関数でも第 1 引数に IoDevice を指定すると利用することができます。主な入力関数を以下に示します。

io:read(IoDevice, Prompt) => Result
io:get_line(IoDevice, Prompt) => Data | eof | {error, Reason}
file:read_line(IoDevice) => {ok, Data} | eof | {error, Reason}
file:read(IoDevice, Number) => {ok, Data} | eof | {error, Reason}
file:read_file(Filename) => {ok, Binary} | {error, Reason}

read_line/1 は IoDevice から1 行読み込みます。Windows の場合、改行コードの変換が行われます。file:read/2 は Number で指定した個数だけデータを読み込みます。返り値の Data は、open の Mode で binary を指定するとバイナリに、そうでなければリストになります。read_file/1 は Filename で指定したファイルを全部読み込み、データをバイナリに格納して返します。

簡単な例を示します。ファイル test.dat からデータを読み込みます。

foo
bar
baz
1234
5678

図 : test.dat の内容
> file:read_file("test.dat").
{ok,<<"foo\r\nbar\r\nbaz\r\n1234\r\n5678\r\n">>}

> {ok, F} = file:open("test.dat", read).
{ok,<...>}
> file:read_line(F).
{ok,"foo\n"}
> file:read_line(F).
{ok,"bar\n"}
> file:read_line(F).
{ok,"baz\n"}
> file:read_line(F).
{ok,"1234\n"}
> file:read_line(F).
{ok,"5678\n"}
> file:read_line(F).
eof
> file:close(F).
ok

read_file/1 を使って test.dat を読み込むと、データを格納したバイナリが返されます。M.Hiroi の実行環境は Windows なので、改行が \r\n で表されていることがわかります。次に、read_line/1 で 1 行ずつ読み込みます。open のモードで binary を指定していないので、データはリストに格納されて返されます。このとき、改行の変換が行われていることに注意してください。

次は open のモードで binary を指定してみましょう。

> {ok, F1} = file:open("test.dat", [read, binary]).
{ok,<...>}
> file:read_line(F1).
{ok,<<"foo\n">>}
> file:read_line(F1).
{ok,<<"bar\n">>}
> file:read_line(F1).
{ok,<<"baz\n">>}
> file:read_line(F1).
{ok,<<"1234\n">>}
> file:read_line(F1).
{ok,<<"5678\n">>}
> file:read_line(F1).
eof
> file:close(F1).
ok

この場合、読み込んだデータはバイナリに格納されて返されます。行単位の入力なので、改行の変換も行われていることに注意してください。

次は file:read/2 を使って 6 バイトずつデータを読み込んでみましょう。

> {ok, F2} = file:open("test.dat", read).
{ok,<...>}
> file:read(F2, 6).
{ok,"foo\r\nb"}
> file:read(F2, 6).
{ok,"ar\r\nba"}
> file:read(F2, 6).
{ok,"z\r\n123"}
> file:read(F2, 6).
{ok,"4\r\n567"}
> file:read(F2, 6).
{ok,"8\r\n"}
> file:read(F2, 6).
eof
> file:close(F2).
ok

open のモードで binary を指定していないので、読み込んだデータはリストに格納されて返されます。file:read/2 では改行の変換が行われないことに注意してください。また、ファイルの最後では、データが足りなくて指定したサイズに満たない場合もあります。

それでは簡単な例題として、ファイルの内容を画面へ出力する関数 cat/1 を作ってみましょう。プログラムは次のようになります。

リスト : ファイルの表示

cat_sub(In) ->
    case file:read_line(In) of
        eof -> ok;
        {ok, Line} -> io:fwrite(Line), cat_sub(In)
    end.

cat(Filename) ->
    case file:open(Filename, read) of
        {error, Reason} -> io:format('file open error ~w~n', [Reason]);
        {ok, In} -> cat_sub(In), file:close(In)
    end.

cat/1 の引数 Filename はファイル名を表す文字列です。ファイル Filename をオープンして IoDevice を変数 In にセットします。ファイルの表示は関数 cat_sub/1 で行います。read_line/1 で 1 行読み込み、それを fwrite/1 で標準出力へ出力します。それから cat_sub/1 を再帰呼び出しします。ファイルの終了を検出したら処理を終了します。あとは cat/1 に戻って、close でファイルを閉じるだけです。

簡単な実行例を示します。

> iotest:cat("test.dat").
foo
bar
baz
1234
5678
ok

●ファイルの書き込み

データをファイルに書き込むには、ファイルを write モードでオープンします。このとき、注意事項が一つあります。既に同じ名前のファイルが存在している場合は、そのファイルの長さを 0 に切り詰めてからデータを書き込みます。既存のファイルは内容が破壊されることに注意してください。

モジュール io と file に定義されている主な出力関数を以下に示します。

io:nl(IoDevice) => ok
io:write(IoDevice, Term) => ok
io:format(IoDevice, Format, Args) => ok
file:write(IoDevice, Data) => ok | {error, Reason}
file:write_file(Filename, Data) => ok | {error, Reason}

Data はリストまたはバイナリで、要素の値はバイナリデータ (0 - 255) になります。なお、リストの要素はバイナリデータを格納したリストまたはバイナリでもかまいません。

簡単な例を示します。

> {ok, Out1} = file:open("test.out", write).
{ok,<...>}
> file:write(Out1, "foo").
ok
> file:write(Out1, "bar").
ok
> file:write(Out1, "baz").
ok
> file:write(Out1, ["1234", "5678"]).
ok
> file:close(Out1).
ok
> {ok, Data} = file:read_file("test.out").
{ok,<<"foobarbaz12345678">>}
> file:write_file("test1.out", Data).
ok
> file:read_file("test1.out").
{ok,<<"foobarbaz12345678">>}

ファイル test.out を write モードでオープンします。write でデータを書き込みますが、このときデータにリストを渡すと、格納された文字列をファイルに書き込みます。test.out をクローズしたあと、read_file/1 で test.out を読み込むと、データが書き込まれていることがわかります。それから、そのデータを write_file/1 でファイル test1.out に書き込みます。read_file/1 で test1.out を読み込むと、正常に書き込まれていることがわかります。

このほかにも、Erlang にはファイルを操作する便利な関数が多数用意されています。詳しい説明は Erlang のリファレンスマニュアル Erlang -- io, Erlang -- file をお読みください。


初出 2011 年 11 月 20 日
改訂 2019 年 1 月 6 日

Copyright (C) 2011-2019 Makoto Hiroi
All rights reserved.

[ PrevPage | Erlang | NextPage ]