binary-typed 0.2.2 → 0.2.3
raw patch · 7 files changed
+178/−77 lines, 7 files
Files
- benchmark/CriterionOverview.hs +1/−6
- binary-typed.cabal +8/−7
- changelog.md +53/−0
- doc/bench-overview.png binary
- src/Data/Binary/Typed.hs +57/−28
- src/Data/Binary/Typed/Internal.hs +57/−33
- src/Data/Binary/Typed/Tutorial.hs +2/−3
benchmark/CriterionOverview.hs view
@@ -23,10 +23,6 @@ value = Right (Left ("Lorem ipsum dolor sit amet, consectetur adipiscing elit.\ \ Nam vitae lacinia tellus. Maecenas posuere.")) --- | Pre-define encoding function so sharing can work properly-encodeTypedEncoder :: Complicated -> ByteString-encodeTypedEncoder = encodeTyped mode- -- Precalcualte encoded values for decoding benchmark value_encodedBinary, value_encodedTyped :: ByteString value_encodedBinary = encode value@@ -39,7 +35,6 @@ evaluate (value `deepseq` ()) evaluate (value_encodedBinary `deepseq` ()) evaluate (value_encodedTyped `deepseq` ())- evaluate (encodeTypedEncoder `seq` ()) defaultMain [ bgroup "encode" bench_encode , bgroup "decode" bench_decode@@ -66,7 +61,7 @@ bench_encode_typed :: Benchmark bench_encode_typed = bench d (nf f value) where d = "Typed with " ++ show mode- f = encodeTypedEncoder+ f = encodeTyped mode
binary-typed.cabal view
@@ -1,5 +1,5 @@ name: binary-typed-version: 0.2.2+version: 0.2.3 synopsis: Type-safe binary serialization Description: `Binary` serialization tagged with type information, allowing for@@ -14,10 +14,10 @@ * Error messages can provide details on type errors at the cost of longer message lengths to include the necessary information. .- * Serialization computationally almost as efficient as "Data.Binary" when- sharing type representations; decoding however is slower.- These values obviously depend a lot on the involved data and its type;- an example benchmark is shown in the picture below.+ * Very small performance overhead compared to using standard `Binary` due+ to caching when using hashed type information. Since shown and full+ types are not cached, decoding them has a significant performance hit+ (ballpark: factor of two). . * No depencency on @Internal@ modules of other libraries, and a very small dependency footprint in general.@@ -28,7 +28,7 @@ type @Either (Char, Int) (Either String (Maybe Integer))@ benchmarked using the @Hashed64@ type representation: .- <<http://i.imgur.com/Jsiaokx.png>>+ <<http://i.imgur.com/UMPUpp4.png>> . <doc/bench-overview.png (local copy)> homepage: https://github.com/quchen/binary-typed@@ -43,6 +43,7 @@ cabal-version: >= 1.10 tested-with: GHC == 7.8.2 extra-source-files: doc/*.png+ , changelog.md source-repository head type: git@@ -56,7 +57,7 @@ ghc-options: -Wall hs-source-dirs: src other-extensions: GADTs, DeriveGeneric- build-depends: base >= 4.6 && < 5+ build-depends: base >= 4.7 && < 5 , binary >= 0.7 , bytestring >= 0.9 , murmur-hash >= 0.1
+ changelog.md view
@@ -0,0 +1,53 @@+`binary-typed` changelog+========================++## 0.1++### 0.1.0.0++Initial release.++### 0.1.0.1++- Add testsuite to compare generated message lengths with vanilla `Binary` and+ the various different typed serializations.+- The .cabal file is hopefully fixed so that Hackage is able to build the docs+ automatically.++++## 0.2++### 0.2.0.0++- Add `Hashed32` type representation with 32 bit, more suitable for short data+ than the previous `Hashed`, which was implicitly 64 bit long. `Hashed64` is+ also available in case the longer hash is needed.+- Type representations are now automatically cached by `encodeTyped`. If this+ data is long enough, it is even serialized in advance as well.++### 0.2.0.1++- Fix whitespace in docs to make the docs build properly on Hackage, which+ presumably has an older Haddock version++### 0.2.1.0++- Fix sharing not working at all (bad `encodeTyped` implementation)+- Pre-calculate certain functions in the benchmarks so they can be properly+ shared among invocations++### 0.2.2++- Add `Hashed5` type representation that has no size overhead compared to+ `Untyped`+- Add script to upload documentation to Hackage, since with the use of the new+ `Typeable` GHC 7.6.3, which Hackage currently runs, cannot build the docs+ itself.++### 0.2.3++- Decoding now caches as well for `Hashed*` representations. This saves+ recalculating the `TypeRep` of the expected type, which previously was the+ bottleneck. Both encoding and decoding speeds are now very close to untyped+ `Binary`.
doc/bench-overview.png view
binary file changed (21436 → 21578 bytes)
src/Data/Binary/Typed.hs view
@@ -1,10 +1,14 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS_HADDOCK show-extensions #-}+++++ -- | Defines a type-safe 'Data.Binary.Binary' instance to ensure data is -- decoded with the type it was serialized from. -- -- For usage information, see the "Data.Binary.Typed.Tutorial" module.--{-# LANGUAGE ScopedTypeVariables #-}- module Data.Binary.Typed ( -- * Core functions@@ -48,6 +52,7 @@ + -- | Modify the value contained in a 'Typed', keeping the same sort of type -- representation. In other words, calling 'mapTyped' on something that is -- typed using 'Hashed' will yield a 'Hashed' value again.@@ -101,8 +106,10 @@ where typeInfo = preserialize (makeTypeInformation format typerep) typerep = typeRep (Proxy :: Proxy a) +{-# INLINE encodeTyped #-} + encodeTypedLike :: (Typeable a, Binary a) => Typed a@@ -115,8 +122,8 @@ --- | Decode a typed value, throwing an error at runtime on failure.--- Typed cousin of 'Data.Binary.decode'.+-- | Decode a typed value, throwing a descriptive 'error' at runtime on failure.+-- Typed cousin of 'Data.Binary.decode'. Based on 'decodeTypedOrFail'. -- -- @ -- encoded = 'encodeTyped' 'Full' ("hello", 1 :: 'Int', 2.34 :: 'Double')@@ -124,36 +131,23 @@ -- -- \<value\> -- 'unsafeDecodeTyped' encoded :: ('String', 'Int', 'Double') ----- -- (Descriptive) runtime error+-- -- (Descriptive) runtime 'error' -- 'unsafeDecodeTyped' encoded :: ('Char', 'Int', 'Double') -- @--- unsafeDecodeTyped :: (Typeable a, Binary a) => BSL.ByteString -> a-unsafeDecodeTyped = erase . decode--+unsafeDecodeTyped = \x -> case decodeTypedOrFail x of+ Left (_, _, err) -> error ("unsafeDecodeTyped' failure: " ++ err)+ Right (_, _, value) -> value --- | Safely decode data, yielding 'Either' an error 'String' or the value,--- along with meta-information of the consumed binary data.------ * Typed cousin of 'Data.Binary.decodeOrFail'.------ * Like 'decodeTyped', but with additional data.----decodeTypedOrFail :: (Typeable a, Binary a)- => BSL.ByteString- -> Either (BSL.ByteString, ByteOffset, String)- (BSL.ByteString, ByteOffset, a)-decodeTypedOrFail input = case decodeOrFail input of- Right (rest, offset, value) -> Right (rest, offset, erase value)- Left l -> Left l+{-# INLINE unsafeDecodeTyped #-} -- | Safely decode data, yielding 'Either' an error 'String' or the value. -- Equivalent to 'decodeTypedOrFail' stripped of the non-essential data.+-- Based on 'decodeTypedOrFail'. -- -- @ -- encoded = 'encodeTyped' 'Full' ("hello", 1 :: 'Int', 2.34 :: 'Double')@@ -164,10 +158,45 @@ -- -- Left "Type error: expected (Char, Int, Double), got (String, Int, Double)" -- 'decodeTyped' encoded :: 'Either' 'String' ('Char', 'Int', 'Double') -- @--- decodeTyped :: (Typeable a, Binary a) => BSL.ByteString -> Either String a-decodeTyped bs = case decodeTypedOrFail bs of- Left (_rest, _offset, err) -> Left err- Right (_rest, _offset, value) -> Right value+decodeTyped = \x -> case decodeTypedOrFail x of+ Left (_, _, err) -> Left err+ Right (_, _, value) -> Right value++{-# INLINE decodeTyped #-}++++-- | Safely decode data, yielding 'Either' an error 'String' or the value,+-- along with meta-information of the consumed binary data.+--+-- * Typed cousin of 'Data.Binary.decodeOrFail'.+--+-- * Like 'decodeTyped', but with additional data.+--+-- * Automatically caches 'Hashed5', 'Hashed32' and 'Hashed64' representations,+-- so that typechecking does not need to recalculate them on every decoding.+decodeTypedOrFail :: forall a.+ (Typeable a, Binary a)+ => BSL.ByteString+ -> Either (BSL.ByteString, ByteOffset, String)+ (BSL.ByteString, ByteOffset, a)+decodeTypedOrFail = \input -> do+ (rest, offset, typed'@(Typed' ty value)) <- decodeOrFail input+ let addMeta x = (rest, offset, x)+ if isCached ty+ then Right (addMeta value) -- cache hit, don't typecheck+ else case typecheck' typed' of -- cache miss, typecheck manually+ Left err -> Left (addMeta err)+ Right _ -> Right (addMeta value)++ where++ exTypeRep = typeRep (Proxy :: Proxy a)+ cache = map (\format -> makeTypeInformation format exTypeRep)+ [Hashed5, Hashed32, Hashed64] -- List of formats to be cached+ isCached = (`elem` cache)++{-# INLINE decodeTypedOrFail #-} -- Inlining is crucial for caching to work!
src/Data/Binary/Typed/Internal.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE LambdaCase #-}+{-# OPTIONS_HADDOCK show-extensions #-} ++ -- | Internals, exposed mostly for potential use by testsuites and benchmarks. -- -- __Not recommended to be used from within other independent libraries.__@@ -10,6 +13,7 @@ -- * 'Typed' Typed(..)+ , Typed'(..) , TypeInformation(..) , Hash5(..) , mkHash5@@ -20,6 +24,7 @@ , TypeFormat(..) , getFormat , typecheck+ , typecheck' , erase , preserialize @@ -68,7 +73,8 @@ | Hashed64' Hash64 | Shown' Hash32 String | Full' TypeRep- | Cached' BSL.ByteString+ | Cached' BSL.ByteString -- ^ Pre-serialized representation+ -- of one of the other fields. deriving (Eq, Ord, Show, Generic) instance Binary TypeInformation where@@ -125,16 +131,16 @@ --- | A hash value of a 'TypeRep'. Currently a 5-bit value created using--- the MurmurHash2 algorithm.+-- | A 5-bit hash value. -- -- Since 'TypeInformation' needs 3 bit to store the sort of the -- 'TypeInformation', the remaining 5 bit per 'Word8' can be used to store a -- hash value at no additional space cost. For this reason, it is important that--- the three rightmost bits of any 'Hashed5' are set to zero.+-- the three rightmost bits of any 'Hashed5' are set to zero, i.e. @('.&.' 7)@+-- is 'id' on the contained 'Word8'. -- -- This type intentionally doesn't have a 'Binary' instance, since its--- serialization is part of the 'TypeInformation' 'Binary' class exclusively.+-- serialization is part of the 'TypeInformation' 'Binary' instance exclusively. newtype Hash5 = Hash5 Word8 deriving (Eq, Ord, Show) @@ -146,16 +152,14 @@ --- | A hash value of a 'TypeRep'. Currently a 32-bit value created using--- the MurmurHash2 algorithm.+-- | A 32-bit hash value. newtype Hash32 = Hash32 Word32 deriving (Eq, Ord, Show, Generic) instance Binary Hash32 --- | A hash value of a 'TypeRep'. Currently a 64-bit value created using--- the MurmurHash2 algorithm.+-- | A 64-bit hash value. newtype Hash64 = Hash64 Word64 deriving (Eq, Ord, Show, Generic) instance Binary Hash64@@ -169,7 +173,7 @@ -- construction of ill-typed 'Typed' data. Use the 'typed' smart -- constructor unless you really need 'Typed'. --- | \"typed \<format\> \<value\>\"+-- | "typed \<format\> \<value\>" instance Show a => Show (Typed a) where show (Typed ty x) = printf "typed %s (%s)" (show (getFormat ty))@@ -179,13 +183,36 @@ -- confidence (depending on with what 'TypeFormat' the 'Typed' was -- constructed). instance (Binary a, Typeable a) => Binary (Typed a) where- get = do (ty, value) <- get+ get = do -- Explicitly get both values instead of a (ty,value) tuple+ -- in case Binary changes in the future. This ensures caching+ -- in 'decodeTyped' can rely on the two values coming in+ -- in this particular way.+ ty <- get+ value <- get either fail return (typecheck (Typed ty value)) -- NB: 'fail' is safe in Get Monad- put (Typed ty value) = put (ty, value)+ put (Typed ty value) = put ty >> put value +-- | Like 'Typed', but the type information is not checked. Useful to read type+-- and value, and do the typechecking externally, as required by the caching+-- of 'Data.Binary.Typed.decodeTyped'. Using 'typecheck'', this can be promoted+-- to a proper 'Typed' value.+data Typed' a = Typed' TypeInformation a++-- | "Typed' \<format\> \<value\>"+instance Show a => Show (Typed' a) where+ show (Typed' ty x) = printf "Typed' %s (%s)"+ (show (getFormat ty))+ (show x)++instance (Binary a) => Binary (Typed' a) where+ get = liftA2 Typed' get get+ put (Typed' ty value) = put ty >> put value+++ -- | Sometimes it can be beneficial to serialize the type information in -- advance, so that the maybe costly serialization step does not have to be -- repeated on every invocation of 'encode'. Preserialization comes at a price@@ -231,12 +258,11 @@ -- | Include no type information. --- -- * Requires one byte more compared to using 'Binary' directly- -- (to tag the data as untyped, required for the decoding step).- --- -- * Encoding and decoding require negligible amount of additional- -- computational cost compared to direct (intrinsically untyped)- -- 'Binary'.+ -- * Requires one byte more compared to using 'Binary' directly+ -- (to tag the data as untyped, required for the decoding step).+ -- * Encoding and decoding require negligible amount of additional+ -- computational cost compared to direct (intrinsically untyped)+ -- 'Binary'. Untyped -- | Like 'Hashed32', but uses a 5-bit hash value.@@ -244,8 +270,7 @@ -- * Requires the same amount of space as 'Untyped', i.e. the only -- overhead compared to it is the computational cost to calculate -- the hash, which is almost identical to the one of 'Hashed32'.- --- -- * Collisions occur with a probability of 1/2^5 = 1/32. For this+ -- * Collisions occur with a probability of 1\/2^5 = 1\/32. For this -- reason, this format is only recommended when minimal data size -- is top priority. --@@ -257,23 +282,18 @@ -- * Requires five bytes more compared to using 'Binary' directly for -- the type information (one to tag as 'Hashed32', four for the -- hash value)- -- -- * Subject to false positive due to hash collisions, although in -- practice this should almost never happen.- -- -- * Type errors cannot tell the provided type ("Expected X, received -- type with hash H")- -- -- * Computational cost similar to 'Hashed64'. | Hashed32 -- | Like 'Hashed32', but uses a 64-bit hash value. -- -- * Requires nine bytes more compared to using 'Binary'.- -- -- * Hash collisions are even less likely to occur than with -- 'Hashed32'.- -- -- * Computational cost similar to 'Hashed32'. | Hashed64 @@ -284,10 +304,8 @@ -- -- * Data size larger than 'Hashed32', but usually smaller than -- 'Full'.- -- -- * Both the hash and the shown type must match to satisfy the -- typechecker.- -- -- * Useful type errors ("expected X, received Y"). All types are -- shown unqualified though, making @Foo.X@ and @Bar.X@ look -- identical in error messages. Remember this when you get a@@ -296,9 +314,9 @@ -- | Compare the full representation of a data type. --- -- * More verbose than 'Hashed' and 'Shown'. As a rule of thumb,- -- transmitted data is roughly the same as 'Shown', but all names- -- are fully qualified (package, module, type name).+ -- * More verbose than 'Shown'. As a rule of thumb, transmitted data is+ -- roughly the same as 'Shown', but all names are fully qualified+ -- (package, module, type name). -- * Correct comparison (no false positives). An semi-exception here -- is when types change between package versions: -- @package-1.0 Foo.X@ and @package-1.1 Foo.X@ count as the same@@ -319,7 +337,7 @@ -- -- @ -- value = 'typed' 'Full' ("hello", 1 :: 'Int', 2.34 :: 'Double')--- encded = 'encode' value+-- encoded = 'encode' value -- @ -- -- The decode site can now verify whether decoding happens with the right type.@@ -329,7 +347,7 @@ -- | Create the 'TypeInformation' to be stored inside a 'Typed' value from--- a 'Ty.TypeRep'.+-- a 'Ty.TypeRep'. makeTypeInformation :: TypeFormat -> Ty.TypeRep -> TypeInformation makeTypeInformation format ty = case format of Untyped -> Untyped'@@ -358,7 +376,7 @@ -- | Typecheck a 'Typed'. Returns the (well-typed) input, or an error message--- if the types don't work out.+-- if the types don't work out. typecheck :: Typeable a => Typed a -> Either String (Typed a) typecheck ty@(Typed typeInformation x) = case typeInformation of Cached' cache -> decode' cache >>= \ty' -> typecheck (Typed ty' x)@@ -394,6 +412,12 @@ decode' bs = case decodeOrFail bs of Left (_,_,err) -> Left ("Cache error! " ++ err) Right (_,_,val) -> Right val++++-- | Typecheck a 'Typed\'' value so it can be used as a safe 'Typed' value.+typecheck' :: Typeable a => Typed' a -> Either String (Typed a)+typecheck' (Typed' ty value) = typecheck (Typed ty value)
src/Data/Binary/Typed/Tutorial.hs view
@@ -77,9 +77,8 @@ -- encoding can be used, with minimal overhead compared to using 'Binary' -- directly. ----- For convenience, this module exports a couple of convenience functions that--- have the type-mangling baked in already. The above example could have been--- written as+-- This module exports a couple of convenience functions that have the+-- type-mangling baked in already. The above example could have been written as -- -- @ -- test3 = let val = 10 :: 'Int'