| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
LawfulConversions
Description
Conversions
The main part of the API is two functions: to and from. Both
perform a conversion between two types. The main difference between them
is in what the first type application parameter specifies. E.g.:
toString = to @String
fromText = from @Text
The types should be self-evident:
> :t to @String to @String :: IsSome String b => b -> String
> :t from @Text from @Text :: IsSome a Text => Text -> a
In other words to and from let you explicitly specify either the source
or the target type of a conversion when you need to help the type
inferencer or the reader.
Examples
combineEncodings ::ShortByteString->ByteArray-> [Word8] ->ByteStringcombineEncodings a b c =from@Builder$toa <>tob <>toc
Partial conversions
This library also captures the pattern of smart constructors via the IsSome class, which associates a total to conversion with its partial inverse maybeFrom.
This captures the codec relationship between types. E.g.,
- Every
Int16can be losslessly converted intoInt32, but not everyInt32can be losslessly converted intoInt16. - Every
Textcan be converted intoByteStringvia UTF-8 encoding, but not everyByteStringforms a valid UTF-8 sequence. - Every URL can be uniquely represented as
Text, but mostTexts 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):
instance IsMany Double Percent where
onfrom double =
if double < 0
then Percent 0
else if double > 1
then Percent 1
else Percent doubleHowever declaring an instance of Is would be incorrect, because this conversion is partial.
Namely, while every Percent value can be losslessly transformed into Double, not every Double can be losslessly transformed into Percent.
Synopsis
- class IsSome a b where
- class IsSome a b => IsMany a b where
- onfrom :: a -> b
- class (IsMany a b, Is b a) => Is a b
- from :: forall b a. IsSome a b => b -> a
- maybeTo :: forall b a. IsSome a b => a -> Maybe b
- tryFrom :: IsSome a b => e -> a -> Either e b
- tryTo :: forall b a e. IsSome a b => e -> a -> Either e b
- onto :: forall b a. IsMany a b => a -> b
- isSomePrism :: (IsSome a b, Choice p, Applicative f) => p b (f b) -> p a (f a)
- isManyIso :: (IsMany a b, Profunctor p, Functor f) => p b (f b) -> p a (f a)
- isIso :: (Is a b, Profunctor p, Functor f) => p b (f b) -> p a (f a)
- newtype ViaIsSome a b = ViaIsSome b
- isSomeProperties :: (IsSome a b, Eq a, Eq b, Show a, Show b, Arbitrary a, Arbitrary b) => Proxy a -> Proxy b -> [(String, Property)]
- isManyProperties :: (IsMany a b, Eq a, Eq b, Show a, Show b, Arbitrary a, Arbitrary b) => Proxy a -> Proxy b -> [(String, Property)]
- isProperties :: (Is a b, Eq a, Eq b, Show a, Show b, Arbitrary a, Arbitrary b) => Proxy a -> Proxy b -> [(String, Property)]
Typeclasses
class IsSome a b where Source #
Evidence that all values of type b form a subset of all values of type a.
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
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 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 isSomeProperties.
Minimal complete definition
Methods
Convert a value of a subset type to a superset type.
maybeFrom :: a -> Maybe b Source #
Partial inverse of to.
Instances
class IsSome a b => IsMany a b where Source #
Lossy or canonicalizing conversion. Captures mappings from multiple alternative inputs into one output.
E.g.,
ByteStringcan be decoded intoTextwith UTF-8 leniently, replacing the invalid chars with a default char.Stringhas a wider range of supported chars thanText, so some chars get replaced too.
Laws
onfrom is an inverse of to
\b -> b == onfrom (to @a b)
onfrom is surjective
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 isManyProperties.
Minimal complete definition
Nothing
Methods
Possibly lossy inverse of to.
Surjection 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.
Instances
class (IsMany a b, Is b a) => Is a b Source #
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 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 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 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 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.fromStrictinstance IsSome Data.Text.Text Data.Text.Lazy.LazyText where to = LazyText.toStrictinstance 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
Instances
Combinators
from :: forall b a. IsSome a b => b -> a Source #
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
maybeTo :: forall b a. IsSome a b => a -> Maybe b Source #
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
tryTo :: forall b a e. IsSome a b => e -> a -> Either e b Source #
Alias to tryFrom, which lets you specify the target type of the conversion first using TypeApplications.
onto :: forall b a. IsMany a b => a -> b Source #
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.
E.g.,
lenientDecodeUtf8 = onto @Text
combineTexts ::Text->ByteString->Int->TextcombineTexts name email height =from@StrictTextBuilder$ "Height of " <>toname <> " is " <>onto(show height) <> " and email is " <>ontoemail
Optics
isSomePrism :: (IsSome a b, Choice p, Applicative f) => p b (f b) -> p a (f a) Source #
Van-Laarhoven-style Prism, compatible with libraries like "lens" and "optics".
isManyIso :: (IsMany a b, Profunctor p, Functor f) => p b (f b) -> p a (f a) Source #
Van-Laarhoven-style Isomorphism, compatible with libraries like "lens" and "optics".
isIso :: (Is a b, Profunctor p, Functor f) => p b (f b) -> p a (f a) Source #
Van-Laarhoven-style Isomorphism, compatible with libraries like "lens" and "optics".
Instance derivation
Proxy data-types useful for deriving various standard instances using the DerivingVia extension.
newtype ViaIsSome a b Source #
Helper for deriving common instances on types which have an instance of using the IsSome aDerivingVia extension.
E.g.,
newtype Percent = Percent Double
deriving newtype (Show, Eq, Ord)
deriving (Read, Arbitrary) via (ViaIsSome Double Percent)
instance IsSome Double Percent where
to (Percent double) = double
maybeFrom double =
if double < 0 || double > 1
then Nothing
else Just (Percent double)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.
Constructors
| ViaIsSome b |
Instances
Testing
isSomeProperties :: (IsSome a b, Eq a, Eq b, Show a, Show b, Arbitrary a, Arbitrary b) => Proxy a -> Proxy b -> [(String, Property)] Source #
Properties testing whether an instance satisfies the laws of IsSome.
The instance is identified via the proxy types that you provide.
E.g., here's how you can integrate it into an Hspec test-suite:
spec = do
describe "IsSome laws" do
traverse_
(uncurry prop)
(isSomeProperties @Int32 @Int16 Proxy Proxy)isManyProperties :: (IsMany a b, Eq a, Eq b, Show a, Show b, Arbitrary a, Arbitrary b) => Proxy a -> Proxy b -> [(String, Property)] Source #
Properties testing whether an instance satisfies the laws of IsMany.
The instance is identified via the proxy types that you provide.
E.g., here's how you can integrate it into an Hspec test-suite:
spec = do
describe "IsMany laws" do
traverse_
(uncurry prop)
(isManyProperties @String @Text Proxy Proxy)isProperties :: (Is a b, Eq a, Eq b, Show a, Show b, Arbitrary a, Arbitrary b) => Proxy a -> Proxy b -> [(String, Property)] Source #
Properties testing whether an instance satisfies the laws of Is.
The instance is identified via the proxy types that you provide.
E.g., here's how you can integrate it into an Hspec test-suite:
spec = do
describe "Is laws" do
traverse_
(uncurry prop)
(isProperties @Int32 @Word32 Proxy Proxy)