packages feed

binary-generic-combinators 0.4.0.0 → 0.4.2.0

raw patch · 5 files changed

+175/−7 lines, 5 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Data.Binary.Combinators: instance Data.Foldable.Foldable Data.Binary.Combinators.Many
+ Data.Binary.Combinators: instance Data.Foldable.Foldable Data.Binary.Combinators.Some
+ Data.Binary.Combinators: instance Data.Traversable.Traversable Data.Binary.Combinators.Many
+ Data.Binary.Combinators: instance Data.Traversable.Traversable Data.Binary.Combinators.Some
+ Data.Binary.Combinators: instance GHC.Base.Functor Data.Binary.Combinators.Many
+ Data.Binary.Combinators: instance GHC.Base.Functor Data.Binary.Combinators.Some
+ Data.Binary.Combinators: instance forall k (ty :: k). Data.Foldable.Foldable (Data.Binary.Combinators.CountedBy ty)
+ Data.Binary.Combinators: instance forall k (ty :: k). Data.Traversable.Traversable (Data.Binary.Combinators.CountedBy ty)
+ Data.Binary.Combinators: instance forall k (ty :: k). GHC.Base.Functor (Data.Binary.Combinators.CountedBy ty)
+ Data.Binary.Combinators: type MatchByte ctx byte = MatchBytes ctx '[byte]
- Data.Binary.Combinators: data MatchBytes :: Symbol -> [Nat] -> Type
+ Data.Binary.Combinators: data MatchBytes (ctx :: Symbol) (bytes :: [Nat]) :: Type

Files

ChangeLog.md view
@@ -1,5 +1,15 @@ # Changelog for binary-generic-combinators +## v0.4.2.0++- Ensure the modules are compatible with Safe Haskell.+- Add `MatchByte` alias.+- Finally write some documentation.++## v0.4.1.0++- Derive `Functor`, `Foldable` and `Traversable` for `Many`, `Some` and `CountedBy`.+ ## v0.4.0.0  - Initial release as a separate library.
README.md view
@@ -1,1 +1,95 @@ # binary-generic-combinators++## tl;dr++This library provides a set of combinators and utility types to make `Generic`-based deriving of+[binary](https://hackage.haskell.org/package/binary) (de)serialization instances easier and more flexible,+especially when dealing with existing formats.++## Motivation++Isn't it great to just define data types representing your problem domain and let the compiler derive all the instances?+If we are talking about [binary](https://hackage.haskell.org/package/binary) (de)serialization,+the `Binary` instance is already derivable for `Generic` types, but there are a couple of problems with that.++### Compound types++Firstly, the serialization format of compound types is carved in stone (again, we're talking about `Generic`-based deriving).+For example, for some list `[a]` the `binary` library always assumes the list length is mentioned explicitly.+This is totally fine if we just need to be able to serialize our type and deserialize it later in the same program without caring about outside world.+But what if we're writing a parser (or serializer) for some existing data format?+In this case (and it's often _the_ case) the format might just imply the strategy of+"try to parse the elements of some type `a` until failure, and build a list out of that" — pretty much like `Alternative`'s `some` or `many`.+With stock `binary`, we'd have to write a custom instance by hand:++```haskell+data Element = Element { .. } deriving (Generic, Binary)+data MyType = MyType { elems :: [Element] }++instance Binary MyType where+  get = MyType <$> many+  put = mapM_ put . elems+```++Wouldn't it be great if we could just annotate our types in such a way that we don't have to write that instance?+Something like, well... This?+```haskell+data Element = Element { .. } deriving (Generic, Binary)+data MyType = MyType { elems :: Many Element } deriving (Generic, Binary)+```++This library provides an array of wrappers that solve precisely this issue.++### Utilities++Then, what if we need to skip some number of bytes, or any number of bytes as long as their value is `0xff`?+Or, maybe, make sure that the input starts with a certain signature sequence of bytes?+With stock `binary`, we again have to write an instance by hand.+This library provides a few helpers for that too:+```haskell+data MyType = MyType+  { header :: MatchBytes "my format header" '[ 0xd3, 0x4d, 0xf0, 0x0d ]   -- consume 0xd34df00d, or fail the parse+  , slack :: SkipByte 0xff                                                -- skip all subsequent 0xff+  , reserved :: SkipCount Word8 4                                         -- 4 bytes reserved+  ..+  } deriving (Generic, Binary)+```++### Deriving strategies++With stock `binary`, if we serialize an ADT, then `binary` first writes the integer denoting the index of the constructor,+and then the contents of that constructor.+This is not always what's needed.+Consider:+```haskell+data JfifSegment+  = App0Segment (MatchByte "app0 segment" 0xdb, JfifApp0)+  | DqtSegment  (MatchByte "dqt segment"  0xdb, QuantTable)+  | SofSegment  (MatchByte "sof segment"  0xc0, SofInfo)+  | DhtSegment  (MatchByte "dht segment"  0xc4, HuffmanTable)+  | DriSegment  (MatchByte "dri segment"  0xdd, RestartInterval)+  | SosSegment  (MatchByte "sos segment"  0xda, SosImage)+  | UnknownSegment RawSegment+```+Here, the identifiers of the constructors are effectively defined in the standard (by the way, this is JPEG/JFIF).+Deriving `Binary` for this type would yield incorrect results: we don't need to encode or decode the index of the constructor,+it's already baked in the `MatchBytes` part.+In this case, what we need is to try to parse each constructor in order, moving on to the next one if its segment identifier doesn't match+what's in the byte stream.+That's basically what `Alternative`'s `<|>` does!+Here, we can leverage that via this library's handy `Alternatively` type and `DerivingVia`:+```haskell+{-# LANGUAGE DerivingVia #-}++data JfifSegment+  = App0Segment (MatchByte "app0 segment" 0xdb, JfifApp0)+  | DqtSegment  (MatchByte "dqt segment"  0xdb, QuantTable)+  | SofSegment  (MatchByte "sof segment"  0xc0, SofInfo)+  | DhtSegment  (MatchByte "dht segment"  0xc4, HuffmanTable)+  | DriSegment  (MatchByte "dri segment"  0xdd, RestartInterval)+  | SosSegment  (MatchByte "sos segment"  0xda, SosImage)+  | UnknownSegment RawSegment+  deriving Generic+  deriving Binary via Alternatively JfifSegment++```
binary-generic-combinators.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 1a85e157782b016bc20ca8aa8487ecead92a285a1fc236a94ad0e7d6186b37be+-- hash: e141cc9e5969922ea090f392042d502509c08b87e62ad0fec15e2472ad97061c  name:           binary-generic-combinators-version:        0.4.0.0+version:        0.4.2.0 synopsis:       Combinators and utilities to make Generic-based deriving of Binary easier and more expressive description:    Please see the README on GitHub at <https://github.com/0xd34df00d/binary-generic-combinators#readme> category:       Data, Parsing
src/Data/Binary/Combinators.hs view
@@ -1,8 +1,24 @@ {-# LANGUAGE DataKinds, PolyKinds, TypeOperators, GADTs #-} {-# LANGUAGE FlexibleInstances, FlexibleContexts, MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE StandaloneDeriving, DeriveTraversable #-}+{-# LANGUAGE Safe #-} +{-| Description : Combinators and utility types for making 'GHC.Generics.Generic'-based 'Binary' instances more expressive.++This module defines a bunch of types to be used as fields of records with 'GHC.Generics.Generic'-based deriving of 'Binary' instances.+For example:++@+data MyFileFormat = MyFileFormat+  { header :: MatchBytes "my format header" '[ 0xd3, 0x4d, 0xf0, 0x0d ]+  , slack :: SkipByte 0xff+  , reserved :: SkipCount Word8 4+  , subElements :: Some MyElement+  } deriving (Generic, Binary)+@+-}+ module Data.Binary.Combinators ( Many(..) , Some(..)@@ -11,6 +27,7 @@ , SkipByte(..) , MatchBytes , matchBytes+, MatchByte ) where  import Control.Applicative@@ -25,7 +42,10 @@ import Test.QuickCheck  -newtype Many a = Many { getMany :: [a] } deriving (Eq, Ord)+-- | Zero or more elements of @a@, parsing as long as the parser for @a@ succeeds.+--+-- @Many Word8@ will consume all your input!+newtype Many a = Many { getMany :: [a] } deriving (Eq, Ord, Functor, Foldable, Traversable)  instance Show a => Show (Many a) where   show = show . getMany@@ -39,7 +59,10 @@   shrink (Many xs) = Many <$> shrink xs  -newtype Some a = Some { getSome :: [a] } deriving (Eq, Ord)+-- | One or more elements of @a@, parsing as long as the parser for @a@ succeeds.+--+-- @Some Word8@ will consume all your non-empty input!+newtype Some a = Some { getSome :: [a] } deriving (Eq, Ord, Functor, Foldable, Traversable)  instance Show a => Show (Some a) where   show = show . getSome@@ -53,7 +76,8 @@   shrink (Some xs) = Some <$> filter (not . null) (shrink xs)  -newtype CountedBy ty a = CountedBy { getCounted :: [a] } deriving (Eq, Ord)+-- | First, parse the elements count as type @ty@. Then, parse exactly as many elements of type @a@.+newtype CountedBy ty a = CountedBy { getCounted :: [a] } deriving (Eq, Ord, Functor, Foldable, Traversable)  instance Show a => Show (CountedBy ty a) where   show = show . getCounted@@ -68,6 +92,9 @@   shrink (CountedBy xs) = CountedBy <$> shrink xs  +-- | Parse out and skip @n@ elements of type @ty@.+--+-- Serializing this type produces no bytes. data SkipCount ty (n :: Nat) = SkipCount deriving (Eq, Ord, Show)  instance (Num ty, Binary ty, KnownNat n) => Binary (SkipCount ty n) where@@ -78,6 +105,9 @@   arbitrary = pure SkipCount  +-- | Skip any number of bytes with value @n@.+--+-- Serializing this type produces no bytes. data SkipByte (n :: Nat) = SkipByte deriving (Eq, Ord, Show)  instance (KnownNat n) => Binary (SkipByte n) where@@ -94,7 +124,14 @@   arbitrary = pure SkipByte  -data MatchBytes :: Symbol -> [Nat] -> Type where+-- | @MatchBytes str bytes@ ensures that the subsequent bytes in the input stream are the same as @bytes@.+--+-- @str@ can be used to denote the context in which @MatchBytes@ is used for better parse failure messages.+-- For example, @MatchBytes "my format header" '[ 0xd3, 0x4d, 0xf0, 0x0d ]@ consumes four bytes from the input stream+-- if they are equal to @[ 0xd3, 0x4d, 0xf0, 0x0d ]@ respectively, or fails otherwise.+--+-- Serializing this type produces the @bytes@.+data MatchBytes (ctx :: Symbol) (bytes :: [Nat]) :: Type where   ConsumeNil  :: MatchBytes ctx '[]   ConsumeCons :: KnownNat n => Proxy n -> MatchBytes ctx ns -> MatchBytes ctx (n ': ns) @@ -136,8 +173,12 @@ instance (KnownNat n, MatchBytesSing ctx ns) => MatchBytesSing ctx (n ': ns) where   matchBytesSing = ConsumeCons Proxy matchBytesSing +-- | Produce the (singleton) value of type 'MatchBytes' @ctx ns@. matchBytes :: MatchBytesSing ctx ns => MatchBytes ctx ns matchBytes = matchBytesSing  instance MatchBytesSing ctx ns => Arbitrary (MatchBytes ctx ns) where   arbitrary = pure matchBytes++-- | An alias for 'MatchBytes' when you only need to match a single byte.+type MatchByte ctx byte = MatchBytes ctx '[ byte ]
src/Data/Binary/DerivingVia.hs view
@@ -1,5 +1,11 @@ {-# LANGUAGE TypeOperators, FlexibleContexts, UndecidableInstances #-}+{-# LANGUAGE Safe #-} +{-| Description : Deriving strategies for making 'GHC.Generics.Generic'-based 'Binary' instances more expressive.++This module defines some types to be used with @DerivingVia@ when deriving 'Binary' instances.+-}+ module Data.Binary.DerivingVia ( Alternatively(..) ) where@@ -7,6 +13,23 @@ import Control.Applicative import Data.Binary import GHC.Generics++-- | Try to deserialize each constructor of @a@ in order.+--+-- For sum types, stock 'Binary' writes (and expects to read) an integer denoting the index of the constructor.+-- This isn't always what's needed. In the following example, the constructor is uniquely identified by the marker byte,+-- and its index in the Haskell ADT is irrelevant:+--+-- > data JfifSegment+-- >   = App0Segment (MatchByte "app0 segment" 0xdb, JfifApp0)+-- >   | DqtSegment  (MatchByte "dqt segment"  0xdb, QuantTable)+-- >   | SofSegment  (MatchByte "sof segment"  0xc0, SofInfo)+-- >   | DhtSegment  (MatchByte "dht segment"  0xc4, HuffmanTable)+-- >   | DriSegment  (MatchByte "dri segment"  0xdd, RestartInterval)+-- >   | SosSegment  (MatchByte "sos segment"  0xda, SosImage)+-- >   | UnknownSegment RawSegment+-- >   deriving Generic+-- >   deriving Binary via Alternatively JfifSegment  newtype Alternatively a = Alternatively { getAlt :: a }