packstream
Haskell implementation of the PackStream binary serialization format, used as the wire format by Neo4j's BOLT protocol.
PackStream is similar to MessagePack but adds a Structure type for encoding typed graph objects (nodes, relationships, temporal values, spatial points, etc.).
Note: Most users should depend on bolty instead, which provides a full Neo4j driver. This package is useful if you need to work with the PackStream wire format directly.
Types
PackStream defines 9 value types, represented by the Ps algebraic data type:
| PackStream type |
Haskell constructor |
Description |
| Null |
PsNull |
Missing or empty value |
| Boolean |
PsBoolean !Bool |
True or false |
| Integer |
PsInteger !PSInteger |
Signed integer (up to 64-bit) |
| Float |
PsFloat !Double |
64-bit IEEE 754 float |
| Bytes |
PsBytes !ByteString |
Raw byte array |
| String |
PsString !Text |
UTF-8 text |
| List |
PsList !(Vector Ps) |
Ordered collection |
| Dictionary |
PsDictionary !(HashMap Text Ps) |
Key-value map (text keys) |
| Structure |
PsStructure !Tag !(Vector Ps) |
Tagged composite (tag byte + positional fields) |
Integers use a variable-width encoding: values in [-16, 127] are encoded in a single byte (no tag), with INT_8, INT_16, INT_32, and INT_64 for larger values.
The PackStream type class
Convert between Haskell types and Ps values:
class PackStream a where
toPs :: a -> Ps -- encode to Ps AST
toBinary :: a -> Put -- encode directly to wire format (optional, defaults to putPs . toPs)
fromPs :: Ps -> Result a -- decode from Ps AST
Built-in instances exist for Bool, Int, Int64, Word8, Word16, Word32, Double, Text, ByteString, Vector, HashMap Text, Maybe, tuples (up to 9), and more.
Quick start
Encoding and decoding Ps values
import Data.PackStream (pack, unpack)
import Data.PackStream.Ps (Ps(..))
import qualified Data.HashMap.Lazy as H
-- Encode a dictionary to binary
let ps = PsDictionary $ H.fromList
[ ("name", PsString "Alice")
, ("age", PsInteger 30)
]
let bytes = pack ps -- :: Lazy ByteString
-- Decode binary back to a Ps value
case unpack bytes of
Right val -> print (val :: Ps)
Left err -> putStrLn $ "Decode error: " <> show err
Custom types
import Data.PackStream.Ps (PackStream(..), Ps(..), (.:), withDictionary)
import Data.PackStream.Result (Result(..))
import qualified Data.HashMap.Lazy as H
data Person = Person { name :: Text, age :: Int64 }
instance PackStream Person where
toPs Person{name, age} = PsDictionary $ H.fromList
[ ("name", toPs name)
, ("age", toPs age)
]
fromPs = withDictionary "Person" $ \m -> do
name <- m .: "name" -- uses (.:) operator for key lookup + decode
age <- m .: "age"
pure $ Person name age
Structures
Structures are the key differentiator from MessagePack. They carry a tag byte identifying the type and positional fields:
import Data.PackStream.Ps (Ps(..), PackStream(..))
import qualified Data.Vector as V
-- A Date structure (tag 0x44) with one field: days since Unix epoch
let date = PsStructure 0x44 (V.singleton (PsInteger 19737))
-- Define a custom structure type
data MyDate = MyDate { days :: Int64 }
instance PackStream MyDate where
toPs (MyDate d) = PsStructure 0x44 (V.singleton (toPs d))
fromPs (PsStructure 0x44 fs) | V.length fs == 1 = MyDate <$> fromPs (fs V.! 0)
fromPs other = typeMismatch "MyDate" other
Neo4j uses structures for graph types (Node 0x4E, Relationship 0x52, Path 0x50), temporal types (Date 0x44, Time 0x54, DateTime 0x49, Duration 0x45), and spatial types (Point2D 0x58, Point3D 0x59).
Module structure
Public API:
Data.PackStream — top-level pack/unpack + re-exports
Data.PackStream.Ps — core Ps type, PackStream class, operators
Data.PackStream.Result — Result type (Success/Error)
Data.PackStream.Tags — wire format tag constants
Data.PackStream.Timestamp — helpers for epoch-based temporal conversions
Internal modules (exposed but not part of the stable API):
Data.PackStream.Get / Data.PackStream.Get.Internal — binary decoding primitives
Data.PackStream.Put — binary encoding primitives
Data.PackStream.Integer — variable-width integer encoding
Data.PackStream.Generic — GHC.Generics-based deriving (experimental)
Data.PackStream.Assoc — ordered association lists
Compat.Binary / Compat.Prelude — compatibility wrappers
Supported GHC versions
9.6.7, 9.8.4, 9.10.3, 9.12.3
License
Apache-2.0