あどけない話

Internet technologies

Haskell ポインタープログラミング

早いもので、今年も12月25日となりました。メリークリスマス! うちのちびっ子怪獣たちも、サンタさんに書いた手紙通り、レゴをもらってご満悦のようです。

そして今日は、Haskell Advent Calendar 2013 の最終日でもあります。

Haskellらしい?

「純粋なコードで構成するのが Haskell らしいプログラムであり、IOはHaskellらしくない」という発言をよく耳にします。

確かに、命令プログラミングの世界から関数プログラミングの世界にやってきたとしたら、

  • 不変データを使った永続データプログラミング
  • 部品プログラミング
  • 純粋なコードに対する性質テスト

などには、衝撃を受けることでしょう。

でも、純粋なコードは、Haskell の世界の半分でしかありません。そこは、コンパイラーという保護者に守られた未成年の世界です。Simon Peyton Jones さんの言葉を借りれば、象牙の塔の中とも言えるでしょう。

象牙の塔の外を見るには、IO の世界に踏み出す必要があります。そして、IO もまた Haskell です。IO の世界では、命令プログラミングのようにデータを破壊できるようになります。以下に、例を示します。

  • IORef が指す先(データ)を変えられる
  • 可変な IOArray を使える。よって、変更可能なハッシュテーブルも使える
  • FFI を利用して C ライブラリやシステムコールを呼び出せる

さらに、IO の世界では並行(cocurrent)プログラミングも可能です。

  • forkIO による軽量スレッドの生成
  • MVar による軽量スレッド間の同期と通信
  • STM によるデッドロックの回避

自己責任である大人の世界に、ようこそ。この記事では、Haskellを型安全なCとして使い、メモリーを操作する方法について説明します。

ポインター

僕が Haskell がある程度分かるようになったころのことです。System.Posix.IOを眺めていて、fdReadBuf や fdWriteBuf という関数を見つけました。これらの関数は、以下のような型を持っています。

fdReadBuf :: Fd -> Ptr Word8 -> ByteCount -> IO ByteCount
fdWriteBuf :: Fd -> Ptr Word8 -> ByteCount -> IO ByteCount

名前が示すように、これはいわゆるファイルからバッファー(一定のメモリー領域)にデータを読み込んだり、バッファーからファイルへデータを書き込んだりする関数です。知識が足らなかった当時の僕は、Ptr Word8 がバッファーを意味していることは推測できましたが、どうやって使ったらいいのか分かりませんでした。

まず Word8 ですが、これは8ビットの符号なし整数を意味しています。つまり、バイトのことです。ちなみに、Haskell では Int が符号ありの整数、Word が符号なしの整数です。その後の数字は、ビット数を表します。Word8 などは、Data.Word で定義されています。

Ptr は文字通り、ポインター。Foreign.Ptr で定義されています。

よって、Ptr Word8 はバイトへのポインターのことです。C で書くなら unsigned char * に相当します。

冷静に考えるとバッファーを確保する関数さえ分かれば、この二つの関数を使ってファイルをコピーするプログラムが書けそうです。しかし、初心者にはその関数を見つけるのが難しいでしょう。なぜなら、Foreign.Ptr を探しても、そういう関数は存在しないからです。

必要な関数が複数のモジュールに散らばっている。これが、Haskell でのポインタープログラミングを分かりにくしている最大の原因です。

ファイルのコピー

バッファーを確保する関数は Foreign.Marshal.Alloc で定義されています。「そんなの知るか!」って感じですよね。たとえば、以下の関数が使えます。

allocaBytes :: Int -> (Ptr a -> IO b) -> IO b

これは OO の世界で言うローンパターンですね。もうファイルをコピーするプログラムが書けるでしょう。たとえば、以下のようになります。

module Main where

import Control.Monad (when)
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Ptr (Ptr)
import System.Environment (getArgs)
import System.Posix.IO (OpenMode(..))
import qualified System.Posix.IO as IO
import System.Posix.Types (Fd)
import Data.Word (Word8)

type Buffer = Ptr Word8

bufSize :: Int
bufSize = 4096

main :: IO ()
main = do
    [src,dst] <- getArgs
    from <- IO.openFd src ReadOnly Nothing IO.defaultFileFlags
    to <- IO.openFd dst WriteOnly (Just 0o644) IO.defaultFileFlags
    copy from to
    IO.closeFd from
    IO.closeFd to

copy :: Fd -> Fd -> IO ()
copy from to = allocaBytes bufSize loop
  where
    siz = fromIntegral bufSize
    loop :: Buffer -> IO ()
    loop ptr = do
        n <- IO.fdReadBuf from ptr siz
        when (n /= 0) $ do
            IO.fdWriteBuf to ptr n
            loop ptr

モリーを書き換える

次に、自分で直接メモリーを書き換えてみましょう。たとえば、コピーするついでにシーザー暗号よろしく数値を3増やしてみましょうか。

Ptr が指すメモリーに値を書き込むには、これまた「知るか!」とう感じですが、Foreign.Storable の poke を使います。読み込むときは peek です。(BASIC みたいですね!)

以下に peek と poke の型を示します。

poke :: Storable a => Ptr a -> a -> IO ()
peek :: Storable a => Ptr a -> IO a

Storable は、データをバイト列にシリアライズするための型クラスです。デフォルトで Word8 は Storable のインスタンスになっています。Word8 をシリアライズすると、単に1バイトですから、Word8 の値を poke すれば一バイト書き込めることになります。

バッファー全体を走査するには、ポインターの値を増やさないといけません。これには Foreign.Ptr の plusPtr が使えます。

plusPtr :: Ptr a -> Int -> Ptr b

Haskell では基本的に変数に再代入できませんから、C のように気軽に p++ などとはできません。関数に引数に、plusPtr した値を渡すなどの工夫が必要です。では、コードを書いてみましょう。

module Main where

import Control.Monad (when)
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Ptr (Ptr, plusPtr)
import System.Environment (getArgs)
import System.Posix.IO (OpenMode(..))
import qualified System.Posix.IO as IO
import System.Posix.Types (Fd)
import Data.Word (Word8)
import Foreign.Storable (peek, poke)

type Buffer = Ptr Word8

bufSize :: Int
bufSize = 4096

main :: IO ()
main = do
    [src,dst] <- getArgs
    from <- IO.openFd src ReadOnly Nothing IO.defaultFileFlags
    to <- IO.openFd dst WriteOnly (Just 0o644) IO.defaultFileFlags
    copy from to
    IO.closeFd from
    IO.closeFd to

copy :: Fd -> Fd -> IO ()
copy from to = allocaBytes bufSize loop
  where
    siz = fromIntegral bufSize
    loop :: Buffer -> IO ()
    loop ptr = do
        n <- IO.fdReadBuf from ptr siz
        encrypt ptr (fromIntegral n)
        when (n /= 0) $ do
            IO.fdWriteBuf to ptr n
            loop ptr

encrypt :: Buffer -> Int -> IO ()
encrypt _   0 = return ()
encrypt ptr n = do
    c <- peek ptr
    poke ptr (c + 3)
    encrypt (ptr `plusPtr` 1) (n - 1)

バッファーの境界を守るのは、自己責任です。くれぐれも気を付けて。

締め

ポインタープログラミングのとっかかりとしては、十分なことが分かったと思います。ぜひ、Haskell での低レベルなプログラミングにも挑戦してみて下さい。

それでは、よいお年を!