core-data 0.3.3.1 → 0.3.4.0
raw patch · 4 files changed
+238/−18 lines, 4 filesdep +uuiddep ~core-textPVP ok
version bump matches the API change (PVP)
Dependencies added: uuid
Dependency ranges changed: core-text
API changes (from Hackage documentation)
+ Core.Encoding.External: class Externalize ξ
+ Core.Encoding.External: formatExternal :: Externalize ξ => ξ -> Rope
+ Core.Encoding.External: instance (GHC.Read.Read a, GHC.Show.Show a) => Core.Encoding.External.Externalize a
+ Core.Encoding.External: instance Core.Encoding.External.Externalize Core.Data.Clock.Time
+ Core.Encoding.External: instance Core.Encoding.External.Externalize Core.Text.Rope.Rope
+ Core.Encoding.External: instance Core.Encoding.External.Externalize Data.Scientific.Scientific
+ Core.Encoding.External: instance Core.Encoding.External.Externalize Data.UUID.Types.Internal.UUID
+ Core.Encoding.External: instance Core.Encoding.External.Externalize GHC.Base.String
+ Core.Encoding.External: instance Core.Encoding.External.Externalize GHC.Int.Int32
+ Core.Encoding.External: instance Core.Encoding.External.Externalize GHC.Int.Int64
+ Core.Encoding.External: instance Core.Encoding.External.Externalize GHC.Types.Double
+ Core.Encoding.External: instance Core.Encoding.External.Externalize GHC.Types.Float
+ Core.Encoding.External: instance Core.Encoding.External.Externalize GHC.Types.Int
+ Core.Encoding.External: parseExternal :: Externalize ξ => Rope -> Maybe ξ
Files
- core-data.cabal +5/−2
- lib/Core/Data/Clock.hs +17/−16
- lib/Core/Encoding.hs +3/−0
- lib/Core/Encoding/External.hs +213/−0
core-data.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: core-data-version: 0.3.3.1+version: 0.3.4.0 synopsis: Convenience wrappers around common data structures and encodings description: Wrappers around common data structures and encodings. .@@ -36,10 +36,12 @@ exposed-modules: Core.Data Core.Data.Clock- Core.Data.Format Core.Data.Structures Core.Encoding+ Core.Encoding.External Core.Encoding.Json+ other-modules:+ Core.Data.Format hs-source-dirs: lib ghc-options: -Wall -Wwarn -fwarn-tabs@@ -56,5 +58,6 @@ , text , time , unordered-containers+ , uuid , vector default-language: Haskell2010
lib/Core/Data/Clock.hs view
@@ -79,38 +79,39 @@ {- | Number of nanoseconds since the Unix epoch. -The 'Show' instance displays the 'Time' as seconds with the nanosecond-precision expressed as a decimal amount after the interger, ie:+The 'Show' and 'Core.Encoding.External.Externalize' instances display the+'Time' as seconds with the nanosecond precision expressed as a decimal amount+after the interger, ie: >>> t <- getCurrentTimeNanoseconds->>> show t-2014-07-31T23:09:35.274387031Z+>>> formatExternal t+"2014-07-31T23:09:35.274387031Z" However this doesn't change the fact the underlying representation counts nanoseconds since epoch: >>> show $ unTime t-1406848175274387031+"1406848175274387031" -There is a 'Read' instance that is reasonably accommodating:+There is a 'Externalize' instance that is reasonably accommodating: ->>> read "2014-07-31T13:05:04.942089001Z" :: Time-2014-07-31T13:05:04.942089001Z+>>> parseExternal "2014-07-31T13:05:04.942089001Z" :: Maybe Time+Just 2014-07-31T13:05:04.942089001Z ->>> read "1406811904.942089001" :: Time-2014-07-31T13:05:04.942089001Z+>>> parseExternal "1406811904.942089001" :: Maybe Time+Just 2014-07-31T13:05:04.942089001Z ->>> read "1406811904" :: Time-2014-07-31T13:05:04.000000000Z+>>> parseExternal "1406811904" :: Maybe Time+Just 2014-07-31T13:05:04.000000000Z In case you're wondering, the valid range of nanoseconds that fits into the underlying 'Int64' is: ->>> show $ minBound :: Time-1677-09-21T00:12:43.145224192Z+>>> formatExternal (minBound :: Time)+"1677-09-21T00:12:43.145224192Z" ->>> show $ maxBound :: Time-2262-04-11T23:47:16.854775807Z+>>> formatExternal (maxBound :: Time)+"2262-04-11T23:47:16.854775807Z" so in a quarter millenium's time, yes, you'll have the Y2262 Problem. Haskell code from today will, of course, still be running, so in the mid
lib/Core/Encoding.hs view
@@ -18,6 +18,9 @@ -} module Core.Encoding ( module Core.Encoding.Json,++ module Core.Encoding.External, ) where +import Core.Encoding.External import Core.Encoding.Json
+ lib/Core/Encoding/External.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_HADDOCK prune #-}++{- |+Quite frequently you will find yourself needing to convert between a rich+semantic Haskell data type and a textual representation of that type which we+call the /external/ representation of a value. The external representation of+the value is authoriative and is meant to be re-readable even in the face of+changing implemetations on the program side.++Note that /externalizing/ is not quite the same as /serializing/. If you have+more complex (ie rich types or nested) data structures then a simple text+string will probably not be sufficient to convey sufficient information to+represent it accurately. Serializing is focused on both performance encoding+and decoding, and efficiency of the representation when transmitted over the+wire. Of course, the obvious benefits of efficiency didn't stop the entire+computer industry from near universal adoption of JSON as an interchange+format, so there is, perhaps, no hope for us.++You can, however, regain some of your sanity by ensuring that the individual+fields of a larger structure are safe, and that's where the externalizing+machinery in this module comes in.++If you have read this far and think we are describing something similar to+'Show' or @toString@ you are correct, but at the level of primative and simple+types we are providing the ability to marshall them to a clean UTF-8+representation and to unmarshall them back into Haskell values again.++The other major use case for this module is as a helper to read user input;+see 'Core.Program.Execute.queryOptionValue'' for an example that makes use of+this.++/Notes for implementators/++Postel's dictum to \"be conservative in what you produce but liberal in what+you accept\" describes the intent of this module. If you are implementing an+instance of 'Externalize' then you might consider being flexible as possible+when parsing with 'parseExternal', within the constraints of having to read a+given value with exact fidelity. But when outputing a value with+'formatExternal' you should be correcting the representation of the value to a+canonical, stable form, even if the original input was written differently.+See the discussion of creating 'Core.Data.Clock.Time' types from varying+inputs for an example.+-}+module Core.Encoding.External (+ -- * Conversions+ Externalize (formatExternal, parseExternal),+) where++import Core.Data.Clock+import Core.Text.Rope+import Data.ByteString.Builder qualified as Builder+import Data.Int (Int32, Int64)+import Data.Scientific (FPFormat (Exponent), Scientific, formatScientific)+import Data.UUID qualified as Uuid (UUID, fromText, toText)+import Text.Read (readMaybe)++{- |+Convert between the internal Haskell representation of a data type and an+external, textual form suitable for visualization, onward transmission, or+archival storage.++It is expected that a valid instance of 'Externalize' allows you to round-trip+through it:++>>> formatExternal (42 :: Int))+"42"++>>> fromJust (parseExternal "42") :: Int+42++with the usual caveat about needing to ensure you've given enough information+to the type-checker to know which instance you're asking for.++There is a general implementatation that goes though 'Show' and 'Read' via+'String' but if you know you have a direct way to render or parse a type into+a sequence of characters then you can offer an instance of 'Externalize' which+does so more efficiently.++@since 0.3.4+-}+class Externalize ξ where+ -- | Convert a value into an authoritative, stable textual representation+ -- for use externally.+ formatExternal :: ξ -> Rope++ -- | Attempt to read an external textual representation into a Haskell value.+ parseExternal :: Rope -> Maybe ξ++--+-- We use this general instance here rather than as a super class constraint+-- for Externalize so as to allow us to have things that can be externalized+-- without necessarily needing those two instances. Most things have Show, but+-- not everything, as many many types haven't bothered with Read.+--++instance {-# OVERLAPPABLE #-} (Read a, Show a) => Externalize a where+ formatExternal = intoRope . show+ parseExternal = readMaybe . fromRope++instance Externalize Rope where+ formatExternal = id+ parseExternal = Just++instance Externalize String where+ formatExternal = packRope+ parseExternal = Just . fromRope++--+-- These weren't really necessary, but they're worth it as an example of+-- avoiding Show & Read+--++{- |+Integers are represented in decimal:++@+42+@+-}+instance Externalize Int where+ formatExternal = intoRope . Builder.toLazyByteString . Builder.intDec+ parseExternal = readMaybe . fromRope++{- |+Integers are represented in decimal:++@+42+@+-}+instance Externalize Int32 where+ formatExternal = intoRope . Builder.toLazyByteString . Builder.int32Dec+ parseExternal = readMaybe . fromRope++{- |+Integers are represented in decimal:++@+42+@+-}+instance Externalize Int64 where+ formatExternal = intoRope . Builder.toLazyByteString . Builder.int64Dec+ parseExternal = readMaybe . fromRope++{- |+IEEE 754 floating point:++@+3.1415927+@+-}+instance Externalize Float where+ formatExternal = intoRope . Builder.toLazyByteString . Builder.floatDec+ parseExternal = readMaybe . fromRope++{- |+IEEE 754 floating point:++@+3.141592653589793+@+-}+instance Externalize Double where+ formatExternal = intoRope . Builder.toLazyByteString . Builder.doubleDec+ parseExternal = readMaybe . fromRope++--+-- More than anything, THIS was the example that motivated creating this+-- module.+--++{- |+Unique identifiers are formatted as per RFC 4122:++@+6937e157-d041-4919-8690-4d6c12b7e0e3+@+-}+instance Externalize Uuid.UUID where+ formatExternal = intoRope . Uuid.toText+ parseExternal = Uuid.fromText . fromRope++--+-- This is a placeholder to remind that if we ever improve the machinery in+-- Core.Data.Clock to not use **hourglass** (which uses String) we could quite+-- likely get a better implementation here.+--++{- |+Timestamps are formatted as per ISO 8601:++@+2022-06-20T14:51:23.544826062Z+@+-}+instance Externalize Time where+ formatExternal = intoRope . show+ parseExternal = readMaybe . fromRope++{- |+Numbers are converted to scientific notation:++@+2.99792458e8+@+-}+instance Externalize Scientific where+ formatExternal = intoRope . formatScientific Exponent Nothing+ parseExternal = readMaybe . fromRope