module LawfulConversions.Algebra where import LawfulConversions.Prelude -- | -- Evidence that all values of type @b@ form a subset of all values of type @a@. -- -- [From Wikipedia](https://en.wikipedia.org/wiki/Subset): -- -- In mathematics, a set A is a subset of a set B if all elements of A are also elements of B; B is then a superset of A. It is possible for A and B to be equal; if they are unequal, then A is a proper subset of B. The relationship of one set being a subset of another is called inclusion (or sometimes containment). A is a subset of B may also be expressed as B includes (or contains) A or A is included (or contained) in B. A k-subset is a subset with k elements. -- -- === Laws -- -- ==== 'to' is [injective](https://en.wikipedia.org/wiki/Injective_function) -- -- For every two values of type @b@ that are not equal converting with 'to' produces values that are not equal as well: -- -- > \(b1, b2) -> b1 == b2 || to @a b1 /= to @a b2 -- -- ==== 'maybeFrom' is a [partial inverse](https://en.wikipedia.org/wiki/Inverse_function#Partial_inverses) of 'to' -- -- 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: -- -- > \b -> maybeFrom (to @a b) == Just b -- -- ==== 'maybeFrom' characterizes the image of 'to' -- -- 'maybeFrom' succeeds exactly on values that are in the image of 'to': -- -- > \a b -> maybeFrom a == Just b ==> to b == a -- -- ==== Mathematical foundation -- -- These laws establish that type @b@ forms a subset of type @a@ in the mathematical sense. -- The 'to' function provides the canonical injection, while 'maybeFrom' recognizes which values of @a@ -- correspond to values from the subset @b@. -- -- === Testing -- -- For testing whether your instances conform to these laws use 'LawfulConversions.isSomeProperties'. class IsSome a b where -- | -- Convert a value of a subset type to a superset type. to :: b -> a -- | -- [Partial inverse](https://en.wikipedia.org/wiki/Inverse_function#Partial_inverses) of 'to'. maybeFrom :: a -> Maybe b -- | -- Requires the presence of 'IsSome' in reverse direction. default maybeFrom :: (IsSome b a) => a -> Maybe b maybeFrom = b -> Maybe b forall a. a -> Maybe a Just (b -> Maybe b) -> (a -> b) -> a -> Maybe b forall b c a. (b -> c) -> (a -> b) -> a -> c forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k). Category cat => cat b c -> cat a b -> cat a c . a -> b forall a b. IsSome a b => b -> a to -- | -- Convert a value of a subset type to a superset type, specifying the source subset type first. -- -- Alias to 'to' with the only difference in the argument order. -- -- E.g., -- -- > fromText = from @Text from :: forall b a. (IsSome a b) => b -> a from :: forall b a. IsSome a b => b -> a from = b -> a forall a b. IsSome a b => b -> a to -- | -- Try to convert a value of a superset type to a subset type specifying the target subset type first. -- -- Alias to 'maybeFrom' with the only difference in the argument order. -- -- Particularly useful in combination with the @TypeApplications@ extension, -- where it allows to specify the target type, e.g.: -- -- > maybeToInt16 :: Int32 -> Maybe Int16 -- > maybeToInt16 = maybeTo @Int16 -- -- > percent = maybeTo @Percent someDouble maybeTo :: forall b a. (IsSome a b) => a -> Maybe b maybeTo :: forall b a. IsSome a b => a -> Maybe b maybeTo = a -> Maybe b forall a b. IsSome a b => a -> Maybe b maybeFrom -- | -- Like 'maybeFrom', but returns an 'Either' with the provided error on failure. -- -- Useful for raising custom errors when conversion fails. tryFrom :: (IsSome a b) => e -> a -> Either e b tryFrom :: forall a b e. IsSome a b => e -> a -> Either e b tryFrom e err a a = case a -> Maybe b forall a b. IsSome a b => a -> Maybe b maybeFrom a a of Just b b -> b -> Either e b forall a b. b -> Either a b Right b b Maybe b Nothing -> e -> Either e b forall a b. a -> Either a b Left e err -- | -- Alias to 'tryFrom', which lets you specify the target type of the conversion first using @TypeApplications@. tryTo :: forall b a e. (IsSome a b) => e -> a -> Either e b tryTo :: forall b a e. IsSome a b => e -> a -> Either e b tryTo = e -> a -> Either e b forall a b e. IsSome a b => e -> a -> Either e b tryFrom -- | -- Lossy or canonicalizing conversion. -- Captures mappings from multiple alternative inputs into one output. -- -- E.g., -- -- - `ByteString` can be decoded into `Text` with UTF-8 leniently, replacing the invalid chars with a default char. -- -- - `String` has a wider range of supported chars than `Text`, so some chars get replaced too. -- -- === Laws -- -- ==== 'onfrom' is an [inverse](https://en.wikipedia.org/wiki/Inverse_function) of 'to' -- -- > \b -> b == onfrom (to @a b) -- -- ==== 'onfrom' is [surjective](https://en.wikipedia.org/wiki/Surjective_function) -- -- Every value of type @b@ can be obtained by applying 'onfrom' to some value of type @a@: -- -- > \b -> exists a. onfrom @a @b a == b -- -- Note: This property cannot be directly tested with QuickCheck as it requires existential quantification. -- -- ==== Law hierarchy -- -- 'IsMany' extends 'IsSome', so all laws from 'IsSome' also apply here. -- The combination ensures that 'onfrom' provides a canonical (possibly lossy) conversion from @a@ to @b@, -- while 'to' provides the lossless injection from @b@ to @a@. -- -- === Testing -- -- For testing whether your instances conform to these laws use 'LawfulConversions.isManyProperties'. class (IsSome a b) => IsMany a b where -- | -- Possibly lossy inverse of 'to'. -- [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.: -- -- > fromString :: IsMany String b => String -> b -- > fromString = onfrom @String -- -- If you want to specify the output type instead, use 'onto'. onfrom :: a -> b -- | -- Requires the presence of 'IsSome' in reverse direction. default onfrom :: (IsSome b a) => a -> b onfrom = a -> b forall a b. IsSome a b => b -> a to -- | -- Alias to 'onfrom', which lets you specify the target type of the conversion first using @TypeApplications@. -- -- In mathematics @onto@ is another name for [Surjective function](https://en.wikipedia.org/wiki/Surjective_function). -- -- E.g., -- -- > lenientDecodeUtf8 = onto @Text -- -- @ -- combineTexts :: 'Text' -> 'ByteString' -> 'Int' -> 'Text' -- combineTexts name email height = -- 'from' @'Data.Text.Encoding.StrictTextBuilder' $ -- "Height of " <> 'to' name <> " is " <> 'onto' (show height) <> " and email is " <> 'onto' email -- @ onto :: forall b a. (IsMany a b) => a -> b onto :: forall b a. IsMany a b => a -> b onto = a -> b forall a b. IsMany a b => a -> b onfrom -- | Bidirectional conversion between two types with no loss of information. -- -- The bidirectionality is encoded via a recursive dependency with arguments -- flipped. -- -- You can read the signature @Is a b@ as \"/B/ is /A/\". -- -- === Laws -- -- ==== 'from' is an [inverse](https://en.wikipedia.org/wiki/Inverse_function) of 'to' -- -- For all values of /b/ converting from /b/ to /a/ and then converting from /a/ to /b/ produces the original value: -- -- > \b -> b == from @a (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 == to @a (from @a @b a) -- -- ==== Mathematical relationship -- -- These two laws together establish that 'to' and 'from' form a true [isomorphism](https://en.wikipedia.org/wiki/Isomorphism) between types @a@ and @b@. -- Note that 'from' is implemented as 'to' from the reverse 'Is' instance, ensuring the symmetry required for isomorphisms. -- -- ==== 'from' equals 'onfrom' -- -- For isomorphic types, both ways of converting should be equivalent: -- -- > \a -> from @a @b a == onfrom @a @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@. -- -- 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