“So”, I thought to myself, “I feel like learning how to use the Haskell Foreign Function Interface. I think I’ll write a binding to a C library”
As one does.
Seeing as both my Haskell and my C are less than stellar this proved to be an interesting challenge, but I rose to it and put together a basic set of bindings for QDBM‘s Depot library. It’s only lightly tested and very incomplete, but seems to work and if you want something along these lines it will probably do the job. You can get it from my misc HG repository. The resulting code is extremely imperative to use, but otherwise not too bad.
This post is a literate Haskell file with a demo of how to use it.
We’re going to write a bulk file importer. It will get its data from stdin and will read a bunch of records formated as “key = value”, ignoring blank lines and lines starting with #. It will import them into a qdbm Depot database file, printing out a message for every value it overwrites.
> module Main where > import qualified Data.ByteString as ByteString > import Data.ByteString (ByteString)
The library is based around strict ByteStrings, both because of the way it interoperates with C and for performance reasons.
> import qualified Data.ByteString.Char8 as Char8 > import Data.ByteString.Char8 (pack, unpack)
In order to convert between ByteStrings and normal Haskell Strings
> import System.Environment > import System.IO > import Text.Printf
Just some random useful imports
> import QDBM.Depot
And of course we need to import the QDBM module itself.
> main = do args <- getArgs > case args of > [dbname, importfile] -> importInto (pack dbname) importfile > x -> putStrLn "Expected usage: bulkimport dbname importfile"
Boring, and woefully inadequate, argument processing.
> where importInto dbname importfile = > do db <- openDepot dbname Write
Open a handle on the database in write mode.
> eachLine importfile $ processLine db
I’m pretty sure the Haskell Cabal will want my blood for writing a foreach in Haskell. But anyway, this should be fairly self explanatory.
> closeDepot db
Finally, close the database file
> parseRecord line = let (start, end) = Char8.break (=='=') line in (start, ByteString.drop 1 end)
Split the line around the first = in order to get the key and value. We should probably do better error handling here. Oh well.
> processLine db line = if (not (ByteString.null line) && Char8.head line /= '#') > then do currentValue <- get db key > case currentValue of > Nothing -> put db key value > Just currentValue | currentValue == value -> return () > Just currentValue -> printf "Replacing value for key %s. Value %s -> %s\n" (show key) (show currentValue) (show value) > else return () > where (key, value) = parseRecord line
For each line, we first check if it’s empty or starts with #. If it is, we ignore it. Else we look up the relevant key in the database. If it’s there already and with a different value with print a warning message. Else we put the new value into the database.
> eachLine :: FilePath -> (ByteString -> IO ()) -> IO () > eachLine file f = do handle <- openFile file ReadMode > catch (eachLineInHandle handle) (const $ hClose handle) > where eachLineInHandle handle = ByteString.hGetLine handle >>= f >> eachLineInHandle handle >
Simple utility function for operating over the lines of a file.
And that’s about it. It’s fairly grotty code, but hopefully conveys the idea of how to use the basics (not that that would really have been hard to figure out). There are a few other functions in the module, but they should be fairly self explanatory.