lawful-conversions 0.1.2 → 0.1.2.1
raw patch · 8 files changed
+161/−98 lines, 8 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- LawfulConversions: ViaIsSome :: sub -> ViaIsSome sup sub
+ LawfulConversions: ViaIsSome :: b -> ViaIsSome a b
- LawfulConversions: class (IsSome sup sub) => IsMany sup sub
+ LawfulConversions: class (IsSome a b) => IsMany a b
- LawfulConversions: class IsSome sup sub
+ LawfulConversions: class IsSome a b
- LawfulConversions: from :: (IsMany sup sub, IsSome sub sup) => sup -> sub
+ LawfulConversions: from :: (IsMany a b, IsSome b a) => a -> b
- LawfulConversions: maybeFrom :: (IsSome sup sub, IsSome sub sup) => sup -> Maybe sub
+ LawfulConversions: maybeFrom :: (IsSome a b, IsSome b a) => a -> Maybe b
- LawfulConversions: newtype ViaIsSome sup sub
+ LawfulConversions: newtype ViaIsSome a b
- LawfulConversions: to :: IsSome sup sub => sub -> sup
+ LawfulConversions: to :: IsSome a b => b -> a
Files
- lawful-conversions.cabal +43/−2
- src/library/LawfulConversions.hs +29/−34
- src/library/LawfulConversions/Classes.hs +5/−4
- src/library/LawfulConversions/Classes/Is.hs +35/−11
- src/library/LawfulConversions/Classes/IsMany.hs +13/−9
- src/library/LawfulConversions/Classes/IsSome.hs +12/−14
- src/library/LawfulConversions/Properties.hs +1/−1
- src/library/LawfulConversions/Proxies/ViaIsSome.hs +23/−23
lawful-conversions.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: lawful-conversions-version: 0.1.2-synopsis: Lawful typeclasses for conversion between types+version: 0.1.2.1+synopsis: Lawful typeclasses for bidirectional conversion between types category: Conversion homepage: https://github.com/nikita-volkov/lawful-conversions bug-reports: https://github.com/nikita-volkov/lawful-conversions/issues@@ -12,6 +12,47 @@ license-file: LICENSE extra-source-files: CHANGELOG.md++description:+ = Summary++ Lawful typeclasses capturing three patterns of bidirectional mapping and forming a layered hierarchy with an ascending strictness of laws.++ 1. `IsSome`: Smart constructor++ 2. `IsMany`: Lossy conversion++ 3. `Is`: Isomorphism or lossless conversion++ = The conversion problem++ Have you ever looked for a @toString@ function? How often do you+ import @Data.Text.Lazy@ only to call its 'Data.Text.Lazy.fromStrict'? How+ about importing @Data.ByteString.Builder@ only to call its+ 'Data.ByteString.Builder.toLazyByteString' and then importing+ @Data.ByteString.Lazy@ only to call its 'Data.ByteString.Lazy.toStrict'?++ Those all are instances of one pattern. They are conversions between different+ representations of the same information. Codebases that don't attempt to+ abstract over this pattern tend to be sprawling with this type of+ boilerplate. It's noise to the codereader, it's a burden to the+ implementor and the maintainer.++ = Why another conversion library?++ Many libraries exist that approach the conversion problem. However most of+ them provide lawless typeclasses leaving it up to the author of the+ instance to define what makes a proper conversion. This results in+ inconsistencies across instances, their behaviour not being evident to+ the user and no way to check whether an instance is correct.++ This library tackles this problem with a lawful typeclass hierarchy, making it+ evident what any of its instances do and it provides property-tests for you+ to validate your instances.++ = Prior work and acknowledgements++ This library is a direct successor of the ["isomorphism-class"](https://hackage.haskell.org/package/isomorphism-class) library, expanding upon the patterns discovered there. It also shares some ideas with ["control-iso"](https://hackage.haskell.org/package/control-iso) and ["type-iso"](https://hackage.haskell.org/package/type-iso). source-repository head type: git
src/library/LawfulConversions.hs view
@@ -1,38 +1,4 @@ -- |--- Lawful typeclasses capturing three main patterns of bidirectional mapping. The typeclasses form a layered hierarchy with ascending strictness of laws.------ 1. `IsSome`: Smart constructor------ 2. `IsMany`: Lossy conversion------ 3. `Is`: Isomorphism------ = The conversion problem------ Have you ever looked for a @toString@ function? How often do you--- import @Data.Text.Lazy@ only to call its 'Data.Text.Lazy.fromStrict'? How--- about importing @Data.ByteString.Builder@ only to call its--- 'Data.ByteString.Builder.toLazyByteString' and then importing--- @Data.ByteString.Lazy@ only to call its 'Data.ByteString.Lazy.toStrict'?------ Those all are instances of one pattern. They are conversions between--- representations of the same information. Codebases that don't attempt to--- abstract over this pattern tend to be sprawling with this type of--- boilerplate. It's noise to the codereader, it's a burden to the--- implementor and the maintainer.------ = Why another conversion library?------ Many libraries exist that approach the conversion problem. However most of--- them provide lawless typeclasses leaving it up to the author of the--- instance to define what makes a proper conversion. This results in--- inconsistencies across instances, their behaviour not being evident to--- the user and no way to check whether an instance is correct.------ This library tackles this problem with a lawful typeclass hierarchy, making it--- evident what any of its instances do and it provides property-tests for you--- to validate your instances.--- -- = Conversions -- -- The main part of the API is two functions: 'to' and 'from'. Both@@ -83,6 +49,35 @@ -- - Every 'Text' can be converted into 'ByteString' via UTF-8 encoding, but not every 'ByteString' forms a valid UTF-8 sequence. -- -- - Every URL can be uniquely represented as 'Text', but most 'Text's are not URLs unfortunately.+--+-- - UTCTime, JSON, Email, etc.+--+-- == Examples+--+-- Here's an example of implementing the Smart Constructor pattern.+--+-- > module Percent (Percent) where+-- >+-- > import LawfulConversions+-- >+-- > newtype Percent = Percent Double+-- >+-- > instance IsSome Double Percent where+-- > to (Percent double) = double+-- > maybeFrom double =+-- > if double < 0 || double > 1+-- > then Nothing+-- > else Just (Percent double)+--+-- You can also expand upon that and provide a default handling of invalid values effectively providing a lossy canonicalizing conversion ([Surjection](https://en.wikipedia.org/wiki/Surjective_function)):+--+-- > instance IsMany Double Percent where+-- > from double =+-- > if double < 0+-- > then Percent 0+-- > else if double > 1+-- > then Percent 1+-- > else Percent double module LawfulConversions ( -- * Typeclasses IsSome (..),
src/library/LawfulConversions/Classes.hs view
@@ -1,5 +1,6 @@-module LawfulConversions.Classes (module Exports) where+module LawfulConversions.Classes+ ( module LawfulConversions.Classes.Is,+ )+where -import LawfulConversions.Classes.Is as Exports-import LawfulConversions.Classes.IsMany as Exports-import LawfulConversions.Classes.IsSome as Exports+import LawfulConversions.Classes.Is
src/library/LawfulConversions/Classes/Is.hs view
@@ -1,4 +1,8 @@-module LawfulConversions.Classes.Is where+module LawfulConversions.Classes.Is+ ( module LawfulConversions.Classes.Is,+ module LawfulConversions.Classes.IsMany,+ )+where import LawfulConversions.Classes.IsMany @@ -11,25 +15,45 @@ -- -- === Laws ----- /B/ is isomorphic to /A/ if and only if there exists a conversion from /B/--- to /A/ ('LawfulConversions.Classes.IsSome.to') and a conversion from /A/ to /B/ ('from') such that:+-- ==== 'from' is an [inverse](https://en.wikipedia.org/wiki/Inverse_function) of 'to' ----- - @'from' . 'LawfulConversions.Classes.IsSome.to' = 'id'@ - For all values of /B/ converting from /B/ to /A/--- and then converting from /A/ to /B/ produces a value that is identical--- to the original.+-- For all values of /b/ converting from /b/ to /a/ and then converting from /a/ to /b/ produces the original value: ----- - @'LawfulConversions.Classes.IsSome.to' . 'from' = 'id'@ - For all values of /A/ converting from /A/ to /B/--- and then converting from /B/ to /A/ produces a value that is identical--- to the original.+-- > \b -> b == from (to @a b) --+-- ==== 'to' is an [inverse](https://en.wikipedia.org/wiki/Inverse_function) of 'from'+--+-- For all values of /a/ converting from /a/ to /b/ and then converting from /b/ to /a/ produces the original value:+--+-- > \a -> a == from (to @b a)+-- -- === Testing -- -- For testing whether your instances conform to these laws use 'LawfulConversions.isProperties'. -- -- === Instance Definition ----- For each pair of isomorphic types (/A/ and /B/) the compiler will require--- you to define six instances, namely: @Is A B@ and @Is B A@, @IsMany A B@ and @IsMany B A@, @IsSome A B@ and @IsSome B A@.+-- For each pair of isomorphic types (/A/ and /B/) the compiler will require you to define six instances, namely: @Is A B@ and @Is B A@, @IsMany A B@ and @IsMany B A@, @IsSome A B@ and @IsSome B A@.+--+-- Instances of @Is@ do not define any functions and serve merely as a statement that the laws are satisfied.+--+-- ==== __Example: Lazy Text and Text__+--+-- @+-- instance IsSome 'Data.Text.Lazy.LazyText' 'Data.Text.Text' where+-- to = LazyText.'Data.Text.Lazy.fromStrict'+--+-- instance IsSome 'Data.Text.Text' 'Data.Text.Lazy.LazyText' where+-- to = LazyText.'Data.Text.Lazy.toStrict'+--+-- instance IsMany 'Data.Text.Lazy.LazyText' 'Data.Text.Text'+--+-- instance IsMany 'Data.Text.Text' 'Data.Text.Lazy.LazyText'+--+-- instance Is 'Data.Text.Lazy.LazyText' 'Data.Text.Text'+--+-- instance Is 'Data.Text.Text' 'Data.Text.Lazy.LazyText'+-- @ class (IsMany a b, Is b a) => Is a b -- | Any type is isomorphic to itself.
src/library/LawfulConversions/Classes/IsMany.hs view
@@ -1,4 +1,8 @@-module LawfulConversions.Classes.IsMany where+module LawfulConversions.Classes.IsMany+ ( module LawfulConversions.Classes.IsMany,+ module LawfulConversions.Classes.IsSome,+ )+where import LawfulConversions.Classes.IsSome @@ -14,31 +18,31 @@ -- -- === Laws ----- ==== 'from' is an inverse of 'to'+-- ==== 'from' is an [inverse](https://en.wikipedia.org/wiki/Inverse_function) of 'to' ----- > \sup -> sup == from (to sup)+-- > \b -> b == from (to @a b) -- -- === Testing -- -- For testing whether your instances conform to these laws use 'LawfulConversions.isManyProperties'.-class (IsSome sup sub) => IsMany sup sub where+class (IsSome a b) => IsMany a b where -- | -- Possibly lossy inverse of 'to'.- -- [Surjection](https://en.wikipedia.org/wiki/Surjective_function) from @sup@ to @sub@.+ -- [Surjection](https://en.wikipedia.org/wiki/Surjective_function) from @a@ to @b@. -- -- Particularly useful in combination with the @TypeApplications@ extension, -- where it allows to specify the input type, e.g.: --- -- > fromText :: IsMany Text sub => Text -> sub+ -- > fromText :: IsMany Text b => Text -> b -- > fromText = from @Text -- -- The first type application of the 'to' function on the other hand specifies -- the output data type.- from :: sup -> sub+ from :: a -> b -- | -- Requires the presence of 'IsSome' in reverse direction.- default from :: (IsSome sub sup) => sup -> sub+ default from :: (IsSome b a) => a -> b from = to -instance IsMany sup sup+instance IsMany a a
src/library/LawfulConversions/Classes/IsSome.hs view
@@ -3,7 +3,7 @@ import LawfulConversions.Prelude -- |--- Evidence that all values of type @sub@ form a subset of all values of type @sup@.+-- Evidence that all values of type @b@ form a subset of all values of type @a@. -- -- [From Wikipedia](https://en.wikipedia.org/wiki/Subset): --@@ -11,35 +11,33 @@ -- -- === Laws ----- ==== 'to' is injective+-- ==== 'to' is [injective](https://en.wikipedia.org/wiki/Injective_function) ----- For every two values of type @sub@ that are not equal converting with 'to' will always produce values that are not equal.+-- For every two values of type @b@ that are not equal converting with 'to' produces values that are not equal as well: ----- > \(a, b) -> a == b || to a /= to b+-- > \(b1, b2) -> b1 == b2 || to @a b1 /= to @a b2 ----- ==== 'maybeFrom' is an inverse of 'to'+-- ==== 'maybeFrom' is a [partial inverse](https://en.wikipedia.org/wiki/Inverse_function#Partial_inverses) of 'to' ----- For all values of @sub@ converting to @sup@ and then attempting to convert back to @sub@ always succeeds and produces a value that is equal to the original.+-- For all values of @b@ converting to @a@ and then attempting to convert back to @b@ always succeeds and produces a value that is equal to the original: ----- > \a -> maybeFrom (to a) == Just a+-- > \b -> maybeFrom (to @a b) == Just b -- -- === Testing -- -- For testing whether your instances conform to these laws use 'LawfulConversions.isSomeProperties'.-class IsSome sup sub where+class IsSome a b where -- | -- Convert a value of a subset type to a superset type.- --- -- This function is injective non-surjective.- to :: sub -> sup+ to :: b -> a -- | -- [Partial inverse](https://en.wikipedia.org/wiki/Inverse_function#Partial_inverses) of 'to'.- maybeFrom :: sup -> Maybe sub+ maybeFrom :: a -> Maybe b -- | -- Requires the presence of 'IsSome' in reverse direction.- default maybeFrom :: (IsSome sub sup) => sup -> Maybe sub+ default maybeFrom :: (IsSome b a) => a -> Maybe b maybeFrom = Just . to -- | Every type is isomorphic to itself.@@ -48,6 +46,6 @@ maybeFrom = Just . id -- | The empty set has no elements, and therefore is vacuously a subset of any set.-instance IsSome sup Void where+instance IsSome a Void where to = absurd maybeFrom = const Nothing
src/library/LawfulConversions/Properties.hs view
@@ -32,7 +32,7 @@ a /= b ==> to' a =/= to' b ),- ( "'maybeFrom' is an inverse of 'to'",+ ( "'maybeFrom' is a partial inverse of 'to'", property \a -> maybeFrom' (to' a) === Just a )
src/library/LawfulConversions/Proxies/ViaIsSome.hs view
@@ -5,7 +5,7 @@ import qualified Test.QuickCheck as QuickCheck -- |--- Helper for deriving common instances on types which have an instance of @'IsSome' sup@ using the @DerivingVia@ extension.+-- Helper for deriving common instances on types which have an instance of @'IsSome' a@ using the @DerivingVia@ extension. -- -- E.g., --@@ -22,51 +22,51 @@ -- -- In the code above all the instances that are able to construct the values of 'Percent' are automatically derived based on the @IsSome Double Percent@ instance. -- This guarantees that they only construct values that pass thru the checks defined in 'maybeFrom'.-newtype ViaIsSome sup sub = ViaIsSome sub+newtype ViaIsSome a b = ViaIsSome b -instance (IsSome sup sub) => IsSome sup (ViaIsSome sup sub) where+instance (IsSome a b) => IsSome a (ViaIsSome a b) where to (ViaIsSome a) = to a maybeFrom = fmap ViaIsSome . maybeFrom -instance IsSome sub (ViaIsSome sup sub) where+instance IsSome b (ViaIsSome a b) where to = coerce -instance IsSome (ViaIsSome sup sub) sub where+instance IsSome (ViaIsSome a b) b where to = coerce -instance IsMany sub (ViaIsSome sup sub)+instance IsMany b (ViaIsSome a b) -instance IsMany (ViaIsSome sup sub) sub+instance IsMany (ViaIsSome a b) b -instance Is sub (ViaIsSome sup sub)+instance Is b (ViaIsSome a b) -instance Is (ViaIsSome sup sub) sub+instance Is (ViaIsSome a b) b -instance (IsSome sup sub, Show sup) => Show (ViaIsSome sup sub) where- show (ViaIsSome a) = show (to @sup a)+instance (IsSome a b, Show a) => Show (ViaIsSome a b) where+ show (ViaIsSome a) = show (to @a a) -instance (IsSome sup sub, Read sup) => Read (ViaIsSome sup sub) where+instance (IsSome a b, Read a) => Read (ViaIsSome a b) where readPrec = do- sup <- readPrec- case maybeFrom @sup sup of+ a <- readPrec+ case maybeFrom @a a of Just a -> pure (ViaIsSome a) Nothing -> fail "Value is not from the subset" -instance (IsSome sup sub, IsString sup) => IsString (ViaIsSome sup sub) where+instance (IsSome a b, IsString a) => IsString (ViaIsSome a b) where fromString =- maybe (error "Value is not from the subset") ViaIsSome . maybeFrom @sup . fromString+ maybe (error "Value is not from the subset") ViaIsSome . maybeFrom @a . fromString -instance (IsSome sup sub, Eq sup) => Eq (ViaIsSome sup sub) where- (==) = on (==) (to @sup)+instance (IsSome a b, Eq a) => Eq (ViaIsSome a b) where+ (==) = on (==) (to @a) -instance (IsSome sup sub, Ord sup) => Ord (ViaIsSome sup sub) where- compare = on compare (to @sup)+instance (IsSome a b, Ord a) => Ord (ViaIsSome a b) where+ compare = on compare (to @a) -instance (IsSome sup sub, QuickCheck.Arbitrary sup) => QuickCheck.Arbitrary (ViaIsSome sup sub) where+instance (IsSome a b, QuickCheck.Arbitrary a) => QuickCheck.Arbitrary (ViaIsSome a b) where arbitrary =- QuickCheck.suchThatMap QuickCheck.arbitrary (maybeFrom @sup)+ QuickCheck.suchThatMap QuickCheck.arbitrary (maybeFrom @a) shrink value = do- shrunkValue <- QuickCheck.shrink (to @sup value)+ shrunkValue <- QuickCheck.shrink (to @a value) shrunkValue & maybeFrom & maybeToList