Rank 2 Classes
==============
### The standard constructor type classes in the parallel rank-2 universe ###
The rank2 package exports module `Rank2`, meant to be imported qualified like this:
~~~ {.haskell}
{-# LANGUAGE RankNTypes, TemplateHaskell, TypeOperators #-}
module MyModule where
import qualified Rank2
import qualified Rank2.TH
~~~
Several more imports for the examples...
~~~ {.haskell}
import Data.Functor.Classes (Show1, showsPrec1)
import Data.Functor.Identity (Identity(..))
import Data.Functor.Const (Const(..))
import Data.List (find)
~~~
The `Rank2` import will make available the following type classes:
* [Rank2.Functor](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Functor)
* [Rank2.Apply](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Apply)
* [Rank2.Applicative](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Applicative)
* [Rank2.Foldable](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Foldable)
* [Rank2.Traversable](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Traversable)
* [Rank2.Distributive](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Distributive)
* [Rank2.Logistic](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Logistic)
The methods of these type classes all have rank-2 types. The class instances are data types of kind `(k -> *) -> *`,
one example of which would be a database record with different field types but all wrapped by the same type
constructor:
~~~ {.haskell}
data Person f = Person{
forall (f :: * -> *). Person f -> f String
name :: f String,
forall (f :: * -> *). Person f -> f Int
age :: f Int,
forall (f :: * -> *). Person f -> f (Maybe PersonVerified)
mother, forall (f :: * -> *). Person f -> f (Maybe PersonVerified)
father :: f (Maybe PersonVerified)
}
~~~
By wrapping each field we have declared a generalized record type. It can made to play different roles by switching the
value of the parameter `f`. Some examples would be
~~~ {.haskell}
type PersonVerified = Person Identity
type PersonText = Person (Const String)
type PersonWithErrors = Person (Either String)
type PersonDatabase = [PersonVerified]
type PersonDatabaseByColumns = Person []
~~~
If you wish to have the standard [Eq](http://hackage.haskell.org/package/base/docs/Data-Eq.html#t:Eq) and
[Show](http://hackage.haskell.org/package/base/docs/Text-Show.html#t:Show) instances for a record type like `Person`,
it's best if they refer to the
[Eq1](http://hackage.haskell.org/package/base-4.9.1.0/docs/Data-Functor-Classes.html#t:Eq1) and
[Show1](http://hackage.haskell.org/package/base-4.9.1.0/docs/Data-Functor-Classes.html#t:Show1) instances for its
parameter `f`:
~~~ {.haskell}
instance Show1 f => Show (Person f) where
showsPrec :: Int -> Person f -> ShowS
showsPrec Int
prec Person f
person String
rest =
String
"Person{" String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
separator String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
"name=" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> f String -> ShowS
forall (f :: * -> *) a. (Show1 f, Show a) => Int -> f a -> ShowS
showsPrec1 Int
prec' (Person f -> f String
forall (f :: * -> *). Person f -> f String
name Person f
person)
(String
"," String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
separator String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
"age=" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> f Int -> ShowS
forall (f :: * -> *) a. (Show1 f, Show a) => Int -> f a -> ShowS
showsPrec1 Int
prec' (Person f -> f Int
forall (f :: * -> *). Person f -> f Int
age Person f
person)
(String
"," String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
separator String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
"mother=" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> f (Maybe PersonVerified) -> ShowS
forall (f :: * -> *) a. (Show1 f, Show a) => Int -> f a -> ShowS
showsPrec1 Int
prec' (Person f -> f (Maybe PersonVerified)
forall (f :: * -> *). Person f -> f (Maybe PersonVerified)
mother Person f
person)
(String
"," String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
separator String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
"father=" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> f (Maybe PersonVerified) -> ShowS
forall (f :: * -> *) a. (Show1 f, Show a) => Int -> f a -> ShowS
showsPrec1 Int
prec' (Person f -> f (Maybe PersonVerified)
forall (f :: * -> *). Person f -> f (Maybe PersonVerified)
father Person f
person)
(String
"}" String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
rest))))
where prec' :: Int
prec' = Int -> Int
forall a. Enum a => a -> a
succ Int
prec
separator :: String
separator = String
"\n" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> Char -> String
forall a. Int -> a -> [a]
replicate Int
prec' Char
' '
~~~
You can create the rank-2 class instances for your data types manually, or you can generate the instances using the
templates imported from the `Rank2.TH` module with a single line of code per data type:
~~~ {.haskell}
$(Rank2.TH.deriveAll ''Person)
~~~
Either way, once you have the rank-2 type class instances, you can use them to easily convert between records with
different parameters `f`.
### Record construction and modification examples ###
In case of our `Person` record, a couple of helper functions will prove handy:
~~~ {.haskell}
findPerson :: PersonDatabase -> String -> Maybe PersonVerified
findPerson :: PersonDatabase -> String -> Maybe PersonVerified
findPerson PersonDatabase
db String
nameToFind = (PersonVerified -> Bool) -> PersonDatabase -> Maybe PersonVerified
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find ((String
nameToFind String -> String -> Bool
forall a. Eq a => a -> a -> Bool
==) (String -> Bool)
-> (PersonVerified -> String) -> PersonVerified -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Identity String -> String
forall a. Identity a -> a
runIdentity (Identity String -> String)
-> (PersonVerified -> Identity String) -> PersonVerified -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PersonVerified -> Identity String
forall (f :: * -> *). Person f -> f String
name) PersonDatabase
db
personByName :: PersonDatabase -> String -> Either String (Maybe PersonVerified)
personByName :: PersonDatabase -> String -> Either String (Maybe PersonVerified)
personByName PersonDatabase
db String
personName
| String -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null String
personName = Maybe PersonVerified -> Either String (Maybe PersonVerified)
forall a b. b -> Either a b
Right Maybe PersonVerified
forall a. Maybe a
Nothing
| p :: Maybe PersonVerified
p@Just{} <- PersonDatabase -> String -> Maybe PersonVerified
findPerson PersonDatabase
db String
personName = Maybe PersonVerified -> Either String (Maybe PersonVerified)
forall a b. b -> Either a b
Right Maybe PersonVerified
p
| Bool
otherwise = String -> Either String (Maybe PersonVerified)
forall a b. a -> Either a b
Left (String
"Nobody by name of " String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
personName)
~~~
Now we can start by constructing a `Person` record with rank-2 functions for fields. This record is not so much a person
as a field-by-field person verifier:
~~~ {.haskell}
personChecker :: PersonDatabase -> Person (Const String Rank2.~> Either String)
personChecker :: PersonDatabase -> Person (Const String ~> Either String)
personChecker PersonDatabase
db =
Person{name :: Arrow (Const String) (Either String) String
name= (Const String String -> Either String String)
-> Arrow (Const String) (Either String) String
forall {k} (p :: k -> *) (q :: k -> *) (a :: k).
(p a -> q a) -> Arrow p q a
Rank2.Arrow (String -> Either String String
forall a b. b -> Either a b
Right (String -> Either String String)
-> (Const String String -> String)
-> Const String String
-> Either String String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Const String String -> String
forall {k} a (b :: k). Const a b -> a
getConst),
age :: Arrow (Const String) (Either String) Int
age= (Const String Int -> Either String Int)
-> Arrow (Const String) (Either String) Int
forall {k} (p :: k -> *) (q :: k -> *) (a :: k).
(p a -> q a) -> Arrow p q a
Rank2.Arrow ((Const String Int -> Either String Int)
-> Arrow (Const String) (Either String) Int)
-> (Const String Int -> Either String Int)
-> Arrow (Const String) (Either String) Int
forall a b. (a -> b) -> a -> b
$ \(Const String
age)->
case ReadS Int
forall a. Read a => ReadS a
reads String
age
of [(Int
n, String
"")] -> Int -> Either String Int
forall a b. b -> Either a b
Right Int
n
[(Int, String)]
_ -> String -> Either String Int
forall a b. a -> Either a b
Left (String
age String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
" is not an integer"),
mother :: Arrow (Const String) (Either String) (Maybe PersonVerified)
mother= (Const String (Maybe PersonVerified)
-> Either String (Maybe PersonVerified))
-> Arrow (Const String) (Either String) (Maybe PersonVerified)
forall {k} (p :: k -> *) (q :: k -> *) (a :: k).
(p a -> q a) -> Arrow p q a
Rank2.Arrow (PersonDatabase -> String -> Either String (Maybe PersonVerified)
personByName PersonDatabase
db (String -> Either String (Maybe PersonVerified))
-> (Const String (Maybe PersonVerified) -> String)
-> Const String (Maybe PersonVerified)
-> Either String (Maybe PersonVerified)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Const String (Maybe PersonVerified) -> String
forall {k} a (b :: k). Const a b -> a
getConst),
father :: Arrow (Const String) (Either String) (Maybe PersonVerified)
father= (Const String (Maybe PersonVerified)
-> Either String (Maybe PersonVerified))
-> Arrow (Const String) (Either String) (Maybe PersonVerified)
forall {k} (p :: k -> *) (q :: k -> *) (a :: k).
(p a -> q a) -> Arrow p q a
Rank2.Arrow (PersonDatabase -> String -> Either String (Maybe PersonVerified)
personByName PersonDatabase
db (String -> Either String (Maybe PersonVerified))
-> (Const String (Maybe PersonVerified) -> String)
-> Const String (Maybe PersonVerified)
-> Either String (Maybe PersonVerified)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Const String (Maybe PersonVerified) -> String
forall {k} a (b :: k). Const a b -> a
getConst)}
~~~
We can apply it using the [Rank2.<*>](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#v:-60
method of the [Rank2.Apply](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#t:Apply) type class to a bunch
of textual fields for `Person`, and get back either errors or proper field values:
~~~ {.haskell}
verify :: PersonDatabase -> PersonText -> PersonWithErrors
verify :: PersonDatabase -> PersonText -> PersonWithErrors
verify PersonDatabase
db PersonText
person = PersonDatabase -> Person (Const String ~> Either String)
personChecker PersonDatabase
db Person (Const String ~> Either String)
-> PersonText -> PersonWithErrors
forall {k} (g :: (k -> *) -> *) (p :: k -> *) (q :: k -> *).
Apply g =>
g (p ~> q) -> g p -> g q
forall (p :: * -> *) (q :: * -> *).
Person (p ~> q) -> Person p -> Person q
Rank2.<*> PersonText
person
~~~
If there are no errors, we can get a fully verified record by applying
[Rank2.traverse](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#v:traverse) to the result:
~~~ {.haskell}
completeVerified :: PersonWithErrors -> Either String PersonVerified
completeVerified :: PersonWithErrors -> Either String PersonVerified
completeVerified = (forall a. Either String a -> Either String (Identity a))
-> PersonWithErrors -> Either String PersonVerified
forall {k} (g :: (k -> *) -> *) (m :: * -> *) (p :: k -> *)
(q :: k -> *).
(Traversable g, Applicative m) =>
(forall (a :: k). p a -> m (q a)) -> g p -> m (g q)
forall (m :: * -> *) (p :: * -> *) (q :: * -> *).
Applicative m =>
(forall a. p a -> m (q a)) -> Person p -> m (Person q)
Rank2.traverse (a -> Identity a
forall a. a -> Identity a
Identity (a -> Identity a) -> Either String a -> Either String (Identity a)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$>)
~~~
or we can go in the opposite direction with
[Rank2.<$>](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#v:-60
~~~ {.haskell}
uncompleteVerified :: PersonVerified -> PersonWithErrors
uncompleteVerified :: PersonVerified -> PersonWithErrors
uncompleteVerified = (forall a. Identity a -> Either String a)
-> PersonVerified -> PersonWithErrors
forall {k} (g :: (k -> *) -> *) (p :: k -> *) (q :: k -> *).
Functor g =>
(forall (a :: k). p a -> q a) -> g p -> g q
Rank2.fmap (a -> Either String a
forall a b. b -> Either a b
Right (a -> Either String a)
-> (Identity a -> a) -> Identity a -> Either String a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Identity a -> a
forall a. Identity a -> a
runIdentity)
~~~
If on the other hand there *are* errors, we can collect them using
[Rank2.foldMap](http://hackage.haskell.org/package/rank2classes/docs/Rank2.html#v:foldMap):
~~~ {.haskell}
verificationErrors :: PersonWithErrors -> [String]
verificationErrors :: PersonWithErrors -> [String]
verificationErrors = (forall a. Either String a -> [String])
-> PersonWithErrors -> [String]
forall m (p :: * -> *).
Monoid m =>
(forall a. p a -> m) -> Person p -> m
forall {k} (g :: (k -> *) -> *) m (p :: k -> *).
(Foldable g, Monoid m) =>
(forall (a :: k). p a -> m) -> g p -> m
Rank2.foldMap ((String -> [String])
-> (a -> [String]) -> Either String a -> [String]
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either (String -> [String] -> [String]
forall a. a -> [a] -> [a]
:[]) ([String] -> a -> [String]
forall a b. a -> b -> a
const []))
~~~
Here is an example GHCi session:
~~~ {.haskell}
~~~
### Related works ###
This package is one of several implementations of a pattern that is often called *Higher-Kinded Data*. Other examples
include [hkd-lens](https://hackage.haskell.org/package/hkd-lens),
[barbies](https://hackage.haskell.org/package/barbies), and [higgledy](https://hackage.haskell.org/package/higgledy).
Grammars are another use case that is almost, but not quite, entirely unlike database records. See
[grammatical-parsers](https://github.com/blamario/grampa/tree/master/grammatical-parsers) or
[construct](https://hackage.haskell.org/package/construct) for examples.
Both database records and grammars are flat structures. If your use case involves trees of rank-2 records, this
package will probably not suffice. Consider using
[deep-transformations](https://hackage.haskell.org/package/deep-transformations) instead.