| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
IsomorphismClass
Description
Lawful solution to the conversion problem.
Conversion problem
Have you ever looked for a toString function? How often do you
import Data.Text.Lazy only to call its fromStrict? How
about importing Data.ByteString.Builder only to call its
toLazyByteString and then importing
Data.ByteString.Lazy only to call its 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, making it evident what any of its instances do and it provides property-tests for you to validate your instances.
The insight
The key insight of this library is that if you add a requirement for the conversion to be lossless and to have a mirror conversion in the opposite direction, there usually appears to be only one way of defining it. That makes it very clear what the conversion does to the user and how to define it to the author of the conversion. It also gives a clear criteria for validating whether the instances are correct, which can be encoded in property-tests.
That insight itself stems from an observation that almost all of the practical conversions in Haskell share a property: you can restore the original data from its converted form. E.g., you can get a text from a text-builder and you can create a text-builder from a text, you can convert a bytestring into a list of bytes and vice-versa, bytestring to/from bytearray, strict bytestring to/from lazy, list to/from sequence, sequence to/from vector, set of ints to/from int-set. In other words, it's always a two-way street with them and there's a lot of instances of this pattern.
UX
A few other accidental findings like encoding this property with recursive
typeclass constraints and fine-tuning for the use of
the TypeApplications extension resulted in a terse and clear API.
Essentially the whole API is just two functions: to and from. Both
perform a conversion between two types. The only difference between them
is in what the first type application parameter specifies. E.g.:
toString = to @String
fromText = from @Text
The types are self-evident:
> :t to @String to @String :: Is String b => b -> String
> :t from @Text from @Text :: Is Text b => Text -> b
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.
Here are more practical examples:
renderNameAndHeight ::Text->Int->TextrenderNameAndHeight name height =from@StrictTextBuilder$ "Height of " <>toname <> " is " <>to(show height)
combineEncodings ::ShortByteString->ByteArray->ByteString-> [Word8] combineEncodings a b c =from@Builder$toa <>tob <>toc
Partial conversions
Atop of all said this library also captures the notion of smart constructors via the IsSome class, which associates a total to conversion with partial 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.
Synopsis
- class (IsSome a b, Is b a) => Is a b
- class IsSome sup sub where
- from :: Is a b => a -> b
- isSomePrism :: (IsSome a b, Choice p, Applicative 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 sup sub = ViaIsSome sub
- isSomeLawsProperties :: (IsSome a b, Eq a, Eq b, Show a, Arbitrary b, Show b) => Proxy a -> Proxy b -> [(String, Property)]
- isLawsProperties :: (Is a b, Eq a, Eq b, Arbitrary a, Show a, Arbitrary b, Show b) => Proxy a -> Proxy b -> [(String, Property)]
Typeclasses
class (IsSome 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
B is isomorphic to A if and only if there exists a conversion from B
to A (to) and a conversion from A to B (from) such that:
- 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.from.to=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.to.from=id
For testing whether your instances conform to these laws use isLawsProperties.
Instance Definition
For each pair of isomorphic types (A and B) the compiler will require
you to define four instances, namely: Is A B and Is B A as well as IsSome A B and IsSome B A.
Instances
class IsSome sup sub where Source #
Evidence that all values of type sub form a subset of all values of type sup.
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 sub that are not equal converting with to will always produce values that are not equal.
\(a, b) -> a == b || to a /= to b
maybeFrom is an inverse 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.
\a -> maybeFrom (to a) == Just a
For testing whether your instances conform to these laws use isSomeLawsProperties.
Minimal complete definition
Methods
Convert a value of a subset type to a superset type.
This function is injective non-surjective.
maybeFrom :: sup -> Maybe sub Source #
Partial inverse of to.
This function is a partial bijection.
Instances
Optics
isSomePrism :: (IsSome a b, Choice p, Applicative f) => p b (f b) -> p a (f a) Source #
Van-Laarhoven-style Prism, compatible with the "lens" library.
isIso :: (Is a b, Profunctor p, Functor f) => p b (f b) -> p a (f a) Source #
Van-Laarhoven-style Isomorphism, compatible with the "lens" library.
Instance derivation
Proxy data-types useful for deriving various standard instances using the DerivingVia extension.
newtype ViaIsSome sup sub Source #
Helper for deriving common instances on types which have an instance of using the IsSome supDerivingVia extension.
E.g.,
newtype Percent = Percent Double
deriving newtype (Show, Eq, Ord)
deriving (Read, IsString, 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 sub |
Instances
Testing
isSomeLawsProperties :: (IsSome a b, Eq a, Eq b, Show a, Arbitrary b, Show 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)
(isSomeLawsProperties @Int32 @Int16 Proxy Proxy)isLawsProperties :: (Is a b, Eq a, Eq b, Arbitrary a, Show a, Arbitrary b, Show 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)
(isLawsProperties @Int32 @Word32 Proxy Proxy)