LoginSignup
12
4

More than 5 years have passed since last update.

Haskellで、バイナリ表現の値を手軽に処理する

Posted at

1. はじめに

Haskell(GHC)を用いて、バイナリ表現の値を手軽に処理する方法を紹介します。

例えば業務などで、ちょっとしたテストデータを生成したり、バイナリ値を読み込みたい場合などに、Haskellを用いて高品質かつ手早く行うことが出来ます。1
Haskellでのバイナリ処理は、対話環境(REPL)であるGHCiとの相性もよく、関数を試行錯誤的に試せます。

なお、各節に対応したソースファイルを、GitHubのリポジトリに置いておきました。

2. バイナリ値の出力と入力の基本

2.1 バイナリ入出力用のライブラリ

Haskellを用いて、バイナリ表現された整数や浮動小数点などの値を入出力するには、様々な方法があります。
本記事では、バイナリ操作用のライブラリの一つであるbinaryライブラリを用いる例を紹介します。2 3

C言語の場合であれば、例えば次の図のように、stdioライブラリのfwrite関数やfread関数を用いて、バイナリ表現の値を入出力します。
lib_binary_c.png

一方、本記事で紹介するHaskellのbinaryライブラリの場合には、次の図のように、ByteString型のデータ構造を介してバイナリの操作と入出力を行います。
lib_binary_haskell.png

この構成のおかげで、対話環境GHCiを用いて、ファイル入出力を伴わずに、関数を容易にデバッグすることも出来ます。

2.2 バイナリ出力のシンプルなコード例

この節では、バイナリ表現の値を出力させるシンプルなコード例を、一つだけ示します。
以下に示すように、コードの本体はmainの1行のみです。

2.2.put.hs
import qualified Data.ByteString.Lazy as B
import           Data.Binary.Put

main :: IO ()
main = B.putStr $ runPut $ putInt32le 0x01234567

上記ではputInt32le関数が、32bitの整数値(Int32)を、リトルエンディアン(le)形式4で、ByteString型のデータに出力することを指示しています。

続いて、上記コードを、runghcコマンドを用いてスクリプト的に実行してみます。5 6

$ runghc 2.2.put.hs | xxd
00000000: 6745 2301                                gE#.

以上のように、実質的に1行のコードにより、整数値をバイナリ表現で出力することが出来ました。さらに色々な出力方法や詳細は、3章で紹介します。

2.3 バイナリ入力のシンプルなコード例

この節では、バイナリ表現の値を入力するシンプルなコード例を、一つだけ示します。
以下に示すように、コードの本体はmainの数行のみです。

2.3.get.hs
import qualified Data.ByteString.Lazy as B
import           Data.Binary.Get

main :: IO ()
main = do
    x <- B.getContents
    print $ runGet getWord8 x

上記ではgetWord8関数が、ByteString型のデータを入力し、符号なし8bitの整数値(Word8)のデータとして解釈します。

続いて、上記コードを、runghcコマンドを用いてスクリプト的に実行してみます。7

$ echo -ne '\xFF' | runghc 2.3.get.hs
255

以上のように、バイナリデータを入力し、8bitの符号なし整数として解釈することが出来ました。さらに色々な入力方法や詳細は、4章で紹介します。

本章での基本的なバイナリの入出力の説明は以上です。ここまでの内容を応用することで、基本的なバイナリ操作は可能です。
続く3章と4章では、詳細をさらに紹介します。長いので、目的に応じた箇所を読んでください。

3. 出力の詳細・発展編

この章では、バイナリ値を出力する多様な例を、いくつか簡単に紹介します。

3.1 準備

まずは、これからの説明を簡潔にするために、コードを書き直しておきます。以下は、2.2節で示したコード例です。

2.2.put.hs
main :: IO ()
main = B.putStr $ runPut $ putInt32le 0x01234567

これを、以下のように書き直します。runPut関数以降の部分について、bin関数を定義して分離するとともに、do記法に変更しました。8 (実行結果は、いずれのコードでも同じです。)

3.1.put_do.hs
main :: IO ()
main = B.putStr bin

bin :: B.ByteString
bin = runPut $ do
    putInt32le 0x01234567

以降の節では、このbin関数の部分のみを明示して説明していきます。

3.2 複数のバイナリ値の出力

以下は、複数のバイナリ値を出力する例です。単純に、putXXX関数を複数記述するだけで、複数のバイナリ値を出力できます。

3.2.put_multi.hs
bin :: B.ByteString
bin = runPut $ do
    putInt32le 0x01234567
    putInt16le 0x55AA

実行してみます。

$ runghc 3.2.put_multi.hs | xxd
00000000: 6745 2301 aa55                           gE#..U

32bitの整数値(Int32)と16bitの整数値(Int16)を、リトルエンディアン(le)形式で出力できています。

3.3 いろいろな型におけるバイナリ値の出力

以下は、いろいろな型におけるバイナリ値を出力する例です。

3.3.put_various.hs
bin :: B.ByteString
bin = runPut $ do
    putInt32le    0x01234567
    putInt16le    0x55aa
    putWord16le   0xcafe
    putFloatle    1.0
    putDoublele   1.0
    putByteString "ABC"
    putWord8      0xff

実行してみます。

$ runghc 3.3.put_various.hs | xxd
00000000: 6745 2301 aa55 feca 0000 803f 0000 0000  gE#..U.....?....
00000010: 0000 f03f 4142 43ff                      ...?ABC.

整数や浮動小数点や文字列などを、バイナリ値として出力できています。
各々のputXXX関数の説明については、こちらのドキュメントが参考になります。

3.4 リストからのバイナリ値の出力

以下に、リストからバイナリ値を出力する例をいくつか示します。

まずは、単純にリストをバイナリ値として出力する例です。次のように、mapM_関数を使って手軽に書けます。

3.4.put_list.hs
bin :: B.ByteString
bin = runPut $ do
    mapM_ putInt16le [1, 2, 3, 4]

実行結果は、このようになります。

$ runghc 3.4.put_list.hs | xxd
00000000: 0100 0200 0300 0400                      ........

次は、リストを範囲指定で手軽に書く例です。

3.4.put_list_range.hs
bin :: B.ByteString
bin = runPut $ do
    mapM_ putWord8 [0..63]

実行結果は、このようになります。

$ runghc 3.4.put_list_range.hs | xxd
00000000: 0001 0203 0405 0607 0809 0a0b 0c0d 0e0f  ................
00000010: 1011 1213 1415 1617 1819 1a1b 1c1d 1e1f  ................
00000020: 2021 2223 2425 2627 2829 2a2b 2c2d 2e2f   !"#$%&'()*+,-./
00000030: 3031 3233 3435 3637 3839 3a3b 3c3d 3e3f  0123456789:;<=>?

次は、少し複雑なデータ系列(sin波)を手軽に生成する例です。

3.4.put_list_sin.hs
bin :: B.ByteString
bin = runPut $ do
    mapM_ putFloatle (map sin [0, (pi/16) .. (pi/2)])

実行結果は、このようになります。

$ runghc 3.4.put_list_sin.hs | xxd
00000000: 0000 0000 c2c5 473e 16ef c33e da39 0e3f  ......G>...>.9.?
00000010: f304 353f 32db 543f 5e83 6c3f bf14 7b3f  ..5?2.T?^.l?..{?
00000020: 0000 803f                                ...?

3.5 Int型におけるバイナリ値の出力

整数出力用のputInt64le, putInt32le, putInt16le関数は、各々、Int64, Int32, Int16型の値を引数とします。従って、Int型の値をバイナリ値として出力するためには、次のように、fromIntegral関数を用いて型を変換する必要があります。

3.5.put_int.hs
val :: Int
val = -3

bin :: B.ByteString
bin = runPut $ do
    putInt64le (fromIntegral val)

実行結果は、このようになります。

$ runghc 3.5.put_int.hs | xxd
00000000: fdff ffff ffff ffff                      ........

なお、Word型の値をバイナリ出力する場合も同様に、fromIntegralを用いて型を変換する必要があります。

本章でのバイナリ出力の詳細説明は以上です。

4. 入力の詳細・発展編

この章では、バイナリ値を入力する多様な例を、いくつか簡単に紹介します。

4.1 準備

まずは、これからの説明を簡潔にするために、コードを書き直しておきます。以下は、2.3節で示したコード例です。

2.3.get.hs
main :: IO ()
main = do
    x <- B.getContents
    print $ runGet getWord8 x

これを、以下のように書き直します。runGet関数以降の部分について、getData関数を定義して分離するとともに、do記法に変更しました。9 (実行結果は、いずれのコードでも同じです。)

4.1.get_do.hs
main :: IO ()
main = do
    x <- B.getContents
    print $ getData x

getData :: B.ByteString -> Word8
getData = runGet $ do
    x <- getWord8
    return x

以降の節では、このgetData関数の部分のみを明示して説明していきます。

4.2 複数のバイナリ値の入力

以下は、複数のバイナリ値を入力する例です。単純に、getXXX関数を複数記述するだけで、複数のバイナリ値を入力できます。

4.2.get_multi.hs
getData :: B.ByteString -> (Word8, Word8)
getData = runGet $ do
    x1 <- getWord8
    x2 <- getWord8
    return (x1, x2)

実行してみます。

$ echo -ne '\xFF\x08' | runghc 4.2.get_multi.hs
(255,8)

バイナリ表現の値を入力し、2つの符号なし8bitの整数値(Word8)として解釈できています。

4.3 いろいろな型におけるバイナリ値の入力

以下は、いろいろな型におけるバイナリ値を入力する例です。(3.3節における出力のコード例と対応しています。)

4.3.get_various.hs
getData :: B.ByteString -> (Int32, Int16, Word16, Float, Double, ByteString, Word8)
getData = runGet $ do
    x1 <- getInt32le
    x2 <- getInt16le
    x3 <- getWord16le
    x4 <- getFloatle
    x5 <- getDoublele
    x6 <- getByteString 3
    x7 <- getWord8
    return (x1, x2, x3, x4, x5, x6, x7)

実行してみます。3.3節のコード例を使ってテストデータを生成しています。

$ runghc 3.3.put_various.hs > test.dat
$ runghc 4.3.get_various.hs < test.dat
(19088743,21930,51966,1.0,1.0,"ABC",255)

整数や浮動小数点や文字列などを、バイナリ値として入力できています。
各々のgetXXX関数の説明については、こちらのドキュメントが参考になります。

4.4 リストへの入力

以下は、複数のバイナリ値を入力してリストに格納する例です。4.2節のコード例と比べると、入力した各々の値を、タプルではなくリストに格納しています。

4.4.get_list.hs
getData :: B.ByteString -> [Word8]
getData = runGet $ do
    x1 <- getWord8
    x2 <- getWord8
    x3 <- getWord8
    return [x1, x2, x3]

実行してみます。

$ echo -ne '\xFF\x08\x7F' | runghc 4.4.get_list.hs
[255,8,127]

複数の符号なし8bit整数値(Word8)をリストへ入力できました。

なお、上記のコードは、replicateM関数を使用して、以下のようにコンパクトに記述することも出来ます。

4.4.get_list_replicateM.hs
getData :: B.ByteString -> [Word8]
getData = runGet $ do
    xs <- replicateM 3 getWord8
    return xs

4.5 Int型への入力

整数入力用のgetInt64le, getInt32le, getInt16関数は、各々、Int64, Int32, Int16型の値を入力します。従って、Int型の変数にバイナリ値を入力するためには、次のように、fromIntegral関数を用いて型を変換する必要があります。

4.5.get_int.hs
getData :: B.ByteString -> Int
getData = runGet $ do
    x <- getInt64le
    return $ fromIntegral x

実行結果は、このようになります。

$ echo -ne '\xFD\xFF\xFF\xFF\xFF\xFF\xFF\xFF' | runghc 4.5.get_int.hs
-3

なお、上記のコードは、<$>関数を使用して、以下のように記述することも出来ます。

4.5.get_int_fmap.hs
getData :: B.ByteString -> Int
getData = runGet $ do
    x <- fromIntegral <$> getInt64le
    return x

4.6 入力値に依存した入力制御、その1

以下は、最初の入力値によって、その後に入力する個数を決める例です。

4.6.get_cond_count.hs
getData :: B.ByteString -> [Word8]
getData = runGet $ do
    n <- fromIntegral <$> getWord8
    xs <- replicateM n getWord8
    return xs

最初の入力値nによって、replicateM関数の繰り返し数を決定しています。

実行してみます。

$ echo -ne '\x03\xFF\x08\x7F' | runghc 4.6.get_cond_count.hs
[255,8,127]

最初の入力値に応じて、3個の複数の符号なし8bit整数値(Word8)をリストへ入力できました。

4.7 入力値に依存した入力制御、その2

以下は、最初の入力値によって、その後に入力するデータ型を決める例です。

4.7.get_cond_type.hs
getData :: B.ByteString -> Val
getData = runGet $ do
    n <- fromIntegral <$> getWord8
    x <- case n of
        1 -> F <$> getFloatle
        _ -> D <$> getDoublele
    return x

data Val = F Float
         | D Double
         deriving Show

最初の入力値を用いて、case式において単精度浮動小数点で入力するか倍精度浮動小数点で入力するかを切り替えています。また、入力した値は、いずれかの浮動小数点型を格納できる代数的データ型Valに格納しています。

実行してみます。

$ echo -ne '\x01\x00\x00\x00\x40' | runghc 4.7.get_cond_type.hs
F 2.0

最初の入力値に応じて、次のデータを単精度浮動小数点値(Float)として入力できました。

4.8 不定個数データの入力

以下は、不定個数のデータを入力する例です。データ列が空であるか否かを、isEmpty関数を使って判定しています。

4.8.get_eof.hs
getData :: B.ByteString -> [Word8]
getData = runGet getList

getList :: Get [Word8]
getList = do
    empty <- isEmpty
    if empty
        then return []
        else do
            v  <- getWord8
            vs <- getList
            return (v:vs)

実行してみます。

$ echo -ne '\xFF\x08\x7F' | runghc 4.8.get_eof.hs
[255,8,127]

入力した3個の複数の符号なし8bit整数値(Word8)を全て入力できました。

なお、上記のコードのelse節は、以下のようにコンパクトに記述することも出来ます。

4.8.get_eof_applicative.hs
getList :: Get [Word8]
getList = do
    empty <- isEmpty
    if empty
        then return []
        else (:) <$> getWord8 <*> getList

可読性やメンテナンス性を考慮して、色々な記述スタイルが可能です。

本章でのバイナリ入力の詳細説明は以上です。

5. 補足

以下は、いくつかの補足情報です。

5.1 対話的なテスト

binaryライブラリの関数は、ByteString型のデータを経由して入出力を行います。これにより例えば、入力用の関数をテストする際には、入力用のテストデータをByteString型で用意することにより、ファイル入出力を伴わずにテストを行えます。
次のように、putXXX関数を用いて、入力用のテストデータ(testData)を容易に作れます。

5.1.ghci_test.hs
getData :: B.ByteString -> Float
getData = runGet $ do
    x <- getFloatle
    return x

testData :: B.ByteString
testData = runPut $ do
    putFloatle 1.0

その上で、対話環境GHCiを用いて、対話的に入力用の関数(getData)の挙動をテストすることが出来ます。

$ ghci 5.1.ghci_test.hs 
ghci> getData testData
1.0

5.2 代数的データ型のバイナリ値の入出力

自分で定義した代数的データ型についても、バイナリ値の入出力を容易に行えます。具体的には、定義した代数的データ型を、Binaryクラスのinstanceにすることで実現できます。
詳細については、こちらのドキュメントのExampleが参考になります。

5.3 その他、いろいろな例

最後に、色々な実用コード例をさらに少し紹介します。

以下は、16x16画角のPGMフォーマット10のグレイスケール画像を生成する例です。画像内容は上下方向の単純なグラデーションです。

5.3.image_pgm.hs
main :: IO ()
main = B.putStr image

image :: B.ByteString
image = runPut $ do
    putByteString "P5\n#\n16 16\n255\n"
    mapM_ putWord8 [0,1..255]

以下は、ビッグエンディアン形式のバイナリデータを3個入力して、リトルエンディアン形式に変換して出力する例です。入出力とも、単精度浮動小数点(Float)の値です。

5.3.convert_endian.hs
main :: IO ()
main = do
    x <- B.getContents
    B.putStr $ putLe $ getBe x

getBe :: B.ByteString -> [Float]
getBe = runGet $ replicateM 3 getFloatbe

putLe :: [Float] -> B.ByteString
putLe xs = runPut $ mapM_ putFloatle xs

以下は、単精度浮動小数点(Float)のバイナリデータを16個入力して、最大値が1となるように各々を正規化して、テキスト形式で出力する例です。

5.3.normalize.hs
main :: IO ()
main = do
    x <- B.getContents
    print $ normal $ getData x

getData :: B.ByteString -> [Float]
getData = runGet $ replicateM 16 getFloatle

normal :: [Float] -> [Float]
normal xs = let maxval = maximum xs
            in  map (/ maxval) xs

以下は、テーブルデータを作成する例です。角度とcos値とsin値からなる行を、15度ごとに出力します。角度を32bit整数値(Int32)として、cos値とsin値を単精度浮動小数点値(Float)として出力しています。

5.3.sin_table.hs
main :: IO ()
main = B.putStr table

table :: B.ByteString
table = runPut $ do
    mapM_ putRow [0, 15 .. 360]

putRow :: Int32 -> Put
putRow degree = do
    let radian = (fromIntegral degree / 360) * (2*pi)
    putInt32le degree
    putFloatle $ cos radian
    putFloatle $ sin radian

以下は、倍精度浮動小数点(Double)の乱数値をバイナリ出力する例です。-1から1の範囲の乱数を16個生成しています。11

5.3.random_num.hs
main :: IO ()
main = do
    xs <- numbers
    B.putStr $ bin xs

numbers :: IO [Double]
numbers = replicateM 16 $ randomRIO (-1, 1)

bin :: [Double] -> B.ByteString
bin xs = runPut $ mapM_ putDoublele xs

これらの例のように、シンプルに手軽に記述できます。
本章での補足は以上です。

本記事では、Haskell(GHC)を用いて、バイナリ表現の値を手軽に処理する方法を紹介しました。

では、Happy Haskelling!


  1. 処理速度やメモリ効率の制約が厳しい場合には、本記事の内容よりも工夫が必要です。 

  2. binaryライブラリは、GHCコンパイラに付属して提供されているため、改めてのインストールが不要です。 

  3. 本記事ではバイナリ操作用のライブラリとして、binaryライブラリを紹介していますが、他にもcerealライブラリwineryライブラリなどのserialization用のライブラリがあります。 

  4. x86やARMでは、CPUの自然なデータ表現はリトルエンディアン形式となっています。なおbinaryライブラリには、ビッグエンディアン形式に対応した関数も用意されています。 

  5. stack環境を使用している場合には、runghcの替わりにstack runghcとして実行してください。 

  6. ここでは、runghcの結果をxxdコマンドに接続することで、バイナリ値を16進数のテキスト形式で表示させています。 

  7. ここでは、Linuxのechoコマンドで生成したデータをパイプで標準入力に接続することで、バイナリデータを入力させています。 

  8. 本来、runPut関数に続くputXXX関数が1つだけの場合には、doの記述は不要です。この記事では、他のコード例と比べやすいように、doを明記しています。 

  9. 本来、runGet関数に続くgetXXX関数が1つだけの場合には、doの記述およびreturnの記述は不要です。この記事では、他のコード例と比べやすいように、doreturnを明記しています。 

  10. PGMフォーマットについては、こちらのドキュメントが参考になります。 

  11. ここでは簡単のために、randomライブラリのrandomRIO関数を使用して乱数値を生成しています。randomRIOについては、この記事が参考になります。 

12
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
4