generics-eot 0.2.1.2 → 0.4.1.0
raw patch · 13 files changed
Files
- README.md +7/−3
- examples/Catamorphisms.hs +4/−3
- examples/Docs.hs +1/−0
- examples/MinBound.hs +1/−0
- examples/ToString.hs +2/−1
- generics-eot.cabal +34/−39
- src/Generics/Eot.hs +7/−5
- src/Generics/Eot/Datatype.hs +4/−3
- src/Generics/Eot/Eot.hs +13/−20
- src/Generics/Eot/Tutorial.lhs +0/−596
- test/Generics/Eot/Tutorial.lhs +598/−0
- test/Generics/Eot/TutorialSpec.hs +1/−14
- test/quickcheck/DatatypeSpec.hs +3/−1
README.md view
@@ -1,5 +1,9 @@ `generics-eot` is a library for datatype generic programming that tries to be-very simple to understand and use. It's heavily inspired by the awesome-`generics-sop` package (http://hackage.haskell.org/package/generics-sop).+very simple to understand and use. -Documentation can be found here: http://generics-eot.readthedocs.org/en/latest/+Documentation can be found here: https://generics-eot.readthedocs.io/++## Alternatives++- [`generics-sop`](http://hackage.haskell.org/package/generics-sop) - `generics-eot` is heavily inspired by this awesome package.+- There's a fork of `generics-eot` vendored into [`knit`](https://github.com/pkamenarsky/knit): [`knit/vendor/generics-eot`](https://github.com/pkamenarsky/knit/tree/master/vendor/generics-eot). It gives access to field names of record types at the type-level, which the version at [github.com/soenkehahn/generics-eot](https://github.com/soenkehahn/generics-eot) does not. For more discussion, see [here](https://github.com/soenkehahn/generics-eot/pull/26).
examples/Catamorphisms.hs view
@@ -8,6 +8,7 @@ module Catamorphisms where +import Data.Kind import Generics.Eot catamorphism :: (HasEot a, Cata (Eot a) dest) =>@@ -15,7 +16,7 @@ catamorphism a proxy = cata (toEot a) proxy class Cata eot dest where- type Typ eot dest :: *+ type Typ eot dest :: Type cata :: eot -> Proxy dest -> Typ eot dest cataConst :: Proxy eot -> dest -> Typ eot dest @@ -35,11 +36,11 @@ instance Cata Void dest where type Typ Void dest = dest cata :: Void -> Proxy dest -> dest- cata void = seq void (error "impossible")+ cata = absurd cataConst Proxy = id class Cons fields dest where- type ConsFunc fields dest :: *+ type ConsFunc fields dest :: Type eotConsFunc :: ConsFunc fields dest -> fields -> dest instance Cons () dest where
examples/Docs.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-}
examples/MinBound.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MonoLocalBinds #-} module MinBound (minBoundG) where
examples/ToString.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE ScopedTypeVariables #-} module ToString (ToString(..)) where@@ -46,7 +47,7 @@ toStringConss [] _ = error "impossible" instance ToStringG Void where- toStringConss _ void = seq void (error "impossible")+ toStringConss _ = absurd class ToStringFields a where toStringFields :: a -> [String]
generics-eot.cabal view
@@ -1,21 +1,22 @@--- This file has been generated from package.yaml by hpack version 0.27.0.+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.38.2. -- -- see: https://github.com/sol/hpack ----- hash: b474ea69101a3a9e75802091552ed5f46295c83b037837798edc3c3b21dee721+-- hash: 61b1b8512afc75a1284a5d2b9a257971eb7121e996fc24221e1c3e6c2b6470a4 name: generics-eot-version: 0.2.1.2+version: 0.4.1.0 synopsis: A library for generic programming that aims to be easy to understand+description: Documentation is here: <https://generics-eot.readthedocs.io/> category: Generics-homepage: https://github.com/soenkehahn/generics-eot#readme+homepage: https://generics-eot.readthedocs.io/ bug-reports: https://github.com/soenkehahn/generics-eot/issues maintainer: soenkehahn@gmail.com license: BSD3 license-file: LICENSE build-type: Simple-cabal-version: >= 1.10- extra-source-files: README.md @@ -24,80 +25,74 @@ location: https://github.com/soenkehahn/generics-eot library- hs-source-dirs:- src- ghc-options: -Wall -fno-warn-name-shadowing -pgmL markdown-unlit- build-depends:- base ==4.*- , markdown-unlit- build-tool-depends: markdown-unlit:markdown-unlit exposed-modules: Generics.Eot- Generics.Eot.Tutorial other-modules: Generics.Eot.Datatype Generics.Eot.Eot Paths_generics_eot+ hs-source-dirs:+ src+ ghc-options: -Wall -fno-warn-name-shadowing+ build-depends:+ base >=4.8 && <5 default-language: Haskell2010 test-suite quickcheck type: exitcode-stdio-1.0 main-is: Spec.hs+ other-modules:+ DatatypeSpec+ Generics.Eot+ Generics.Eot.Datatype+ Generics.Eot.Eot+ Paths_generics_eot hs-source-dirs: test/quickcheck src- ghc-options: -Wall -fno-warn-name-shadowing -pgmL markdown-unlit+ ghc-options: -Wall -fno-warn-name-shadowing build-depends: QuickCheck- , base ==4.*+ , base >=4.8 && <5 , directory , filepath , hspec , interpolate- , markdown-unlit , mockery , shake- other-modules:- DatatypeSpec- Generics.Eot- Generics.Eot.Datatype- Generics.Eot.Eot- Generics.Eot.Tutorial- Paths_generics_eot+ , silently default-language: Haskell2010- build-tool-depends: markdown-unlit:markdown-unlit, hspec-discover:hspec-discover test-suite spec type: exitcode-stdio-1.0 main-is: Spec.hs- hs-source-dirs:- test- src- examples- ghc-options: -Wall -fno-warn-name-shadowing -pgmL markdown-unlit- build-depends:- QuickCheck- , base ==4.*- , doctest- , hspec- , interpolate- , markdown-unlit other-modules: Examples.CatamorphismsSpec Examples.DocsSpec Examples.MinBoundSpec Examples.ToStringSpec Generics.DatatypeSpec+ Generics.Eot.Tutorial Generics.Eot.TutorialSpec Generics.EotSpec Generics.Eot Generics.Eot.Datatype Generics.Eot.Eot- Generics.Eot.Tutorial Catamorphisms Docs MinBound ToString Paths_generics_eot+ hs-source-dirs:+ test+ src+ examples+ ghc-options: -Wall -fno-warn-name-shadowing -pgmL markdown-unlit+ build-depends:+ QuickCheck+ , base >=4.8 && <5+ , doctest+ , hspec+ , interpolate+ , markdown-unlit default-language: Haskell2010- build-tool-depends: markdown-unlit:markdown-unlit, hspec-discover:hspec-discover
src/Generics/Eot.hs view
@@ -2,13 +2,14 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -- | @generics-eot@ tries to be a library for datatype generic programming -- that is easy to understand. "eot" stands for "eithers of tuples". -- -- A tutorial on how to use @generics-eot@ can be found here:--- http://generics-eot.readthedocs.org/en/latest/.+-- https://generics-eot.readthedocs.io/. module Generics.Eot ( HasEot(..), @@ -17,15 +18,16 @@ Constructor(..), Fields(..), - -- * Void- Void, -- * Useful Re-exports Generic, Proxy(..),+ Void,+ absurd, ) where +import Data.Kind import Data.Proxy-import GHC.Exts (Constraint)+import Data.Void import GHC.Generics hiding (Datatype, Constructor) import Generics.Eot.Datatype@@ -65,7 +67,7 @@ -- These rules (and the end-markers) are necessary to make sure generic -- functions know exactly which parts of the generic representation are field -- types and which parts belong to the generic skeleton.- type Eot a :: *+ type Eot a :: Type -- | Convert a value of type @a@ to its generic representation. toEot :: a -> Eot a
src/Generics/Eot/Datatype.hs view
@@ -12,6 +12,7 @@ module Generics.Eot.Datatype where import Data.Maybe+import Data.Kind import Data.Proxy import qualified GHC.Generics as GHC import GHC.Generics hiding (Datatype(..), Constructor(..))@@ -45,7 +46,7 @@ -- * datatype -class GenericDatatype (a :: * -> *) where+class GenericDatatype (a :: Type -> Type) where datatypeC :: Proxy a -> Datatype instance (GHC.Datatype c, GenericConstructors f) =>@@ -57,7 +58,7 @@ -- * constructors -class GenericConstructors (a :: * -> *) where+class GenericConstructors (a :: Type -> Type) where getConstructors :: Proxy a -> [Constructor] instance (GenericConstructors a, GenericConstructors b) =>@@ -85,7 +86,7 @@ l@(Nothing : _) -> NoSelectors (length l) l@(Just _ : _) -> Selectors (catMaybes l) -class GenericFields (a :: * -> *) where+class GenericFields (a :: Type -> Type) where getFieldsC :: Proxy a -> [Maybe String] instance (GenericFields a, GenericFields b) =>
src/Generics/Eot/Eot.hs view
@@ -15,16 +15,17 @@ module Generics.Eot.Eot ( HasEotG(..),- Void, ) where +import Data.Kind import Data.Proxy+import Data.Void import GHC.Generics -- * datatype -class HasEotG (a :: * -> *) where- type EotG a :: *+class HasEotG (a :: Type -> Type) where+ type EotG a :: Type toEotG :: a x -> EotG a fromEotG :: EotG a -> a x @@ -35,8 +36,8 @@ -- * constructors -class HasConstructorsG (a :: * -> *) where- type Constructors a :: *+class HasConstructorsG (a :: Type -> Type) where+ type Constructors a :: Type toEotConstructors :: a x -> Constructors a fromEotConstructors :: Constructors a -> a x @@ -55,25 +56,17 @@ toEotConstructors = Left . toEotFields . unM1 fromEotConstructors = \ case Left fields -> M1 $ fromEotFields fields- Right void -> seq void (error "impossible")---- | Uninhabited type.-data Void- deriving (Generic)--deriving instance Show Void-deriving instance Eq Void-deriving instance Ord Void+ Right void -> absurd void instance HasConstructorsG V1 where type Constructors V1 = Void toEotConstructors v1 = seq v1 (error "impossible")- fromEotConstructors void = seq void (error "impossible")+ fromEotConstructors = absurd -- * GEither class Normalize a b where- type GEither a b :: *+ type GEither a b :: Type gLeft :: a -> Proxy b -> GEither a b gRight :: Proxy a -> b -> GEither a b gEither :: GEither a b -> Either a b@@ -92,15 +85,15 @@ instance Normalize Void b where type GEither Void b = b- gLeft void Proxy = seq void (error "impossible")+ gLeft void Proxy = absurd void gRight Proxy b = b gEither :: b -> Either Void b gEither = Right -- * fields -class HasFieldsG (a :: * -> *) where- type Fields a :: *+class HasFieldsG (a :: Type -> Type) where+ type Fields a :: Type toEotFields :: a x -> Fields a fromEotFields :: Fields a -> a x @@ -124,7 +117,7 @@ -- * heterogenous lists class Concat a b where- type a +++ b :: *+ type a +++ b :: Type (+++) :: a -> b -> (a +++ b) unConcat :: (a +++ b) -> (a, b)
− src/Generics/Eot/Tutorial.lhs
@@ -1,596 +0,0 @@-# `generics-eot` tutorial--This tutorial is meant to be read alongside with the haddock comments in-[Generics.Eot](http://hackage.haskell.org/package/generics-eot-0.1/docs/Generics-Eot.html).-Its source is a compiled haskell file, so we have to get some language pragmas-and imports out of the way first:--``` haskell-{-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE UndecidableInstances #-}--module Generics.Eot.Tutorial where--import Data.Char-import Data.List-import Data.Typeable--import Generics.Eot-```--`generics-eot` allows roughly three different kinds of operations:--1. Accessing meta information about ADTs (`datatype` for names, `Proxy` and- `Eot` for field types). Example: Generation of database schemas for ADTs.-2. Deconstructing values generically (`toEot`). Example: Serialization to a- binary format.-3. Constructing values of an ADT generically (`fromEot`).- Example: Deserialization from a binary format.--Sometimes only one of the three forms is used but often multiple have to be-combined. For example serialization to JSON usually requires both `datatype`-and `toEot`.--## 1st Example: Meta Information Without Types: Field Names--This simple function extracts the names of all field selectors and returns them-as a list:--``` haskell-namesOfFields :: HasEot a => Proxy a -> [String]-namesOfFields proxy =- nub $- concatMap (fieldNames . fields) $- constructors $ datatype proxy- where- fieldNames :: Fields -> [String]- fieldNames fields = case fields of- Selectors names -> names- _ -> []-```--And here's proof that it works (using-[doctest](https://github.com/sol/doctest)):--``` haskell-data A = A1 {- foo :: String,- bar :: Int- }- | A2 {- bar :: Int,- baz :: Bool- }- deriving (Generic, Show)---- $ >>> namesOfFields (Proxy :: Proxy A)--- ["foo","bar","baz"]-```--## The `Generic` instance: Don't forget!!!--To be able to use generic functions that are written with `generics-eot` you-need to derive an instance for `GHC.Generics.Generic` (using `DeriveGeneric`)-for your ADTs. This will automatically give you an instance for `HasEot`.--When the instance for `GHC.Generics.Generic` is missing the type error messages-are unfortunately very confusing and unhelpful. They go something like this:-- Couldn't match type ‘GHC.Generics.Rep WithoutGeneric’- with ‘GHC.Generics.D1 c f’- The type variables ‘c’, ‘f’ are ambiguous- In the expression: namesOfFields (Proxy :: Proxy WithoutGeneric)--So don't forget: you need a `Generic` instance.--## `Eot`: Isomorphic representations--Part of the type class `HasEot` is the type-level function `Eot` that maps ADTs-to isomorphic types. These isomorphic types are always a combination of-`Either`s, tuples and the uninhabited type `Void`. For example this type:--``` haskell-data B = B1 Int | B2 String Bool | B3- deriving (Generic)-```--would be mapped to:--```-Either (Int, ()) (Either (String, (Bool, ())) (Either () Void))-```--Tip: Here's how you can execute the type-level function `Eot` in `ghci`:--``` haskell--- $ >>> :kind! Eot B--- Eot B :: *--- = Either (Int, ()) (Either ([Char], (Bool, ())) (Either () Void))-```--For the exact rules of this mapping see here:-[Eot](http://hackage.haskell.org/package/generics-eot-0.1/docs/Generics-Eot.html#t:Eot).--If we have an ADT `a` then we can convert values of type `a` to this isomorphic-representation `Eot a` with `toEot` and we can convert in the other direction-with `fromEot`. Generic functions always operate on these isomorphic-representations and then convert from or to the real ADTs with `fromEot` and-`toEot`.--These generic isomorphic types are referred to as "eot" -- short for-"`Either`s of tuples".--## 2nd Example: Deconstructing Values: Serialization--We start by writing a function that operates on the eot representations. The-eot representations follow simple patterns and always look similar, but they-don't look exactly the same for different ADTs. For this reason we have to use-a type class:--``` haskell-class EotSerialize eot where- eotSerialize :: Int -- ^ The number of the constructor being passed in- -> eot -- ^ The eot representation- -> [Int] -- ^ A simple serialization format-```--Now we need to write instances for the types that occur in eot types. Usually-these are:--- `Either this next`:-- - If as eot value we get `Left this` it means that the original value- was constructed with the constructor that corresponds to `this`. In this- case we put the number of the constructor into the output and continue- with serializing the fields of type `this`.- - If we get `Right rest` it means that one of the following constructors was- the one that the original value was built with. We- continue by increasing the constructor counter and serializing the value- of type `rest`.-- Note that this results in `EotSerialize` class constraints for both- `this` and `rest`. If we write the correct instances for all eot types- these constraints should always be fulfilled.--``` haskell-instance (EotSerialize this, EotSerialize next) =>- EotSerialize (Either this next) where-- eotSerialize n (Left fields) = n : eotSerialize n fields- eotSerialize n (Right next) = eotSerialize (succ n) next-```--- `Void`:- We need this instance to make the compiler happy, but it'll never be- used. If you look at the type you can also see that: an argument of type- `Void` cannot be constructed.--``` haskell-instance EotSerialize Void where- eotSerialize _ void = seq void $ error "impossible"-```--- `(x, xs)`:- Right-nested 2-tuples are used to encode all the fields for one specific- constructor. So `x` is the current field and `xs` are the remaining- fields. To serialize this we serialize `x` (using `serialize`)- and also write the length of the- resulting list into the output. This will allow deserialization.-- Note: We could use `EotSerialize` to serialize the fields. But that would- be a bit untrue to the spirit, since the fields are not eot types. Apart- from that we might want to encode a field of e.g. type `Either a b`- differently than the eot type `Either a b`. So we use a very similar- but distinct type class called `Serialize`.-- The value of type `xs` contains the remaining fields and will be encoded- recursively with `eotSerialize`:--``` haskell-instance (Serialize x, EotSerialize xs) => EotSerialize (x, xs) where- eotSerialize n (x, xs) =- let xInts = serialize x- in length xInts : xInts ++ eotSerialize n xs-```--- `()`:- Finally we need an instance for the unit type that marks the end of the- fields encoded in 2-tuples. Since `()` doesn't carry any information, we- can encode it as the empty list:--``` haskell-instance EotSerialize () where- eotSerialize _ () = []-```--This is the class `Serialize`:--``` haskell-class Serialize a where- serialize :: a -> [Int]-```--We give `serialize` a default implementation, but please ignore that for now.-It'll be explained later in the section about-[DefaultSignatures](#defaultsignatures):--``` haskell- default serialize :: (HasEot a, EotSerialize (Eot a)) => a -> [Int]- serialize = genericSerialize-```--`Serialize` is used to serialize every field of the used ADTs, so we need-instances for all of them:--``` haskell-instance Serialize Int where- serialize i = [i]--instance Serialize String where- serialize = map ord--instance Serialize Bool where- serialize True = [1]- serialize False = [0]--instance Serialize () where- serialize () = []-```--To tie everything together we provide a function `genericSerialize` that-converts a value of some ADT into an eot value using `toEot` and then uses-`eotSerialize` to convert that eot value into a list of `Int`s.--``` haskell-genericSerialize :: (HasEot a, EotSerialize (Eot a)) => a -> [Int]-genericSerialize = eotSerialize 0 . toEot-```--And it works too:--``` haskell--- $ >>> genericSerialize (A1 "foo" 42)--- [0,3,102,111,111,1,42]--- >>> genericSerialize (A2 23 True)--- [1,1,23,1,1]-```--## 3rd Example: Constructing Values: Deserialization--Deserialization works very similarly. It differs in that the functions turn-lists of `Int`s into eot values.--Here's the `EotDeserialize` class with instances for:--- `Either this next`-- `Void`-- `(x, xs)`-- `()`--``` haskell-class EotDeserialize eot where- eotDeserialize :: [Int] -> eot--instance (EotDeserialize this, EotDeserialize next) =>- EotDeserialize (Either this next) where-- eotDeserialize (0 : r) = Left $ eotDeserialize r- eotDeserialize (n : r) = Right $ eotDeserialize (pred n : r)- eotDeserialize [] = error "invalid input"--instance EotDeserialize Void where- eotDeserialize _ = error "invalid input"--instance (Deserialize x, EotDeserialize xs) =>- EotDeserialize (x, xs) where-- eotDeserialize (len : r) =- let (this, rest) = splitAt len r- in (deserialize this, eotDeserialize rest)- eotDeserialize [] = error "invalid input"--instance EotDeserialize () where- eotDeserialize [] = ()- eotDeserialize (_ : _) = error "invalid input"-```--And here's the `Deserialize` class plus all instances to deserialize the-fields:--``` haskell-class Deserialize a where- deserialize :: [Int] -> a--instance Deserialize Int where- deserialize [n] = n- deserialize _ = error "invalid input"--instance Deserialize String where- deserialize = map chr--instance Deserialize () where- deserialize [] = ()- deserialize (_ : _) = error "invalid input"--instance Deserialize Bool where- deserialize [0] = False- deserialize [1] = True- deserialize _ = error "invalid input"-```--And here's `genericDeserialize` to tie it together. It uses-`eotDeserialize` to convert a list of `Int`s into an eot value and then-`fromEot` to construct a value of the wanted ADT.--``` haskell-genericDeserialize :: (HasEot a, EotDeserialize (Eot a)) => [Int] -> a-genericDeserialize = fromEot . eotDeserialize-```--Here you can see it in action:--``` haskell--- $ >>> genericDeserialize [0,3,102,111,111,1,42] :: A--- A1 {foo = "foo", bar = 42}--- >>> genericDeserialize [1,1,23,1,1] :: A--- A2 {bar = 23, baz = True}-```--And it is the inverse of `genericSerialize`:--``` haskell--- $ >>> (genericDeserialize $ genericSerialize $ A1 "foo" 42) :: A--- A1 {foo = "foo", bar = 42}-```--## 4th Example: Meta Information with types: generating SQL schemas--Accessing meta information __including__ the types works very-similarly to deconstructing or constructing values. It uses the same-structure of type classes and instances for the eot-types. The difference is:-since we don't want actual values of our ADT as input or output we operate on-`Proxy`s of our eot-types.--As an example we're going to implement a function that generates SQL statements-that create tables that our ADTs would fit into. To be able to use nice names-for the table and columns we're going to traverse the type-less meta-information (see-[1st Example](#st-example-meta-information-without-types-field-names)) at the-same time.--(Note that the generated SQL statements are targeted at a fictional-database implementation that magically understands Haskell types like-`Int` and `String`, or rather `[Char]`.)--Again we start off by writing a class that operates on the eot-types. Besides-the eot-type the class has an additional parameter, `meta`, that will be-instantiated by the corresponding types used for untyped meta information.--``` haskell-class EotCreateTableStatement meta eot where- eotCreateTableStatement :: meta -> Proxy eot -> [String]-```--Our first instance is for the complete datatype. `eot` is instantiated to-`Either fields Void`. Note that this instance only works for ADTs with-exactly one constructor as we don't support types with multiple constructors.-`meta` is instantiated to `Datatype` which is the type for meta information-for ADTs.--``` haskell-instance EotCreateTableStatement [String] fields =>- EotCreateTableStatement Datatype (Either fields Void) where-- eotCreateTableStatement datatype Proxy = case datatype of- Datatype name [Constructor _ (Selectors fields)] ->- "CREATE TABLE " :- name :- " COLUMNS " :- "(" :- intercalate ", " (eotCreateTableStatement fields (Proxy :: Proxy fields)) :- ");" :- []- Datatype _ [Constructor name (NoSelectors _)] ->- error ("constructor " ++ name ++ " has no selectors, this is not supported")- Datatype name _ ->- error ("type " ++ name ++ " must have exactly one constructor")-```--The second instance is responsible for creating the parts of the SQL-statements that declare the columns. As such it has to traverse the fields-of our ADT. `eot` is instantiated to the usual `(x, xs)`. `meta` is-instantiated to `[String]`, representing the field names. The name of the-field type is obtained using `typeRep`, therefore we need a `Typeable x`-constraint.--``` haskell-instance (Typeable x, EotCreateTableStatement [String] xs) =>- EotCreateTableStatement [String] (x, xs) where-- eotCreateTableStatement (field : fields) Proxy =- (field ++ " " ++ show (typeRep (Proxy :: Proxy x))) :- eotCreateTableStatement fields (Proxy :: Proxy xs)- eotCreateTableStatement [] Proxy = error "impossible"-```--The last instances is for `()`. It's needed as the base case for-traversing the fields and as such returns just an empty list.--``` haskell-instance EotCreateTableStatement [String] () where- eotCreateTableStatement [] Proxy = []- eotCreateTableStatement (_ : _) Proxy = error "impossible"-```--`createTableStatement` ties everything together. It obtaines the meta-information through `datatype` passing a `Proxy` for `a`. And it creates a-`Proxy` for the eot-type `Proxy :: Proxy (Eot a)`. Then it calls-`eotCreateTableStatement` and just `concat`s the resulting snippets.--``` haskell-createTableStatement :: forall a . (HasEot a, EotCreateTableStatement Datatype (Eot a)) =>- Proxy a -> String-createTableStatement proxy =- concat $ eotCreateTableStatement (datatype proxy) (Proxy :: Proxy (Eot a))-```--As an example, we're going to use `Person`:--``` haskell-data Person- = Person {- name :: String,- age :: Int- }- deriving (Generic)-```--And here's the created SQL statement:--``` haskell--- $ >>> putStrLn $ createTableStatement (Proxy :: Proxy Person)--- CREATE TABLE Person COLUMNS (name [Char], age Int);-```--If we try to use an ADT with multiple constructors, we get a type error-due to a missing instance:--``` haskell--- $ >>> putStrLn $ createTableStatement (Proxy :: Proxy A)--- <BLANKLINE>--- ...--- • No instance for (EotCreateTableStatement--- Datatype--- (Either ([Char], (Int, ())) (Either (Int, (Bool, ())) Void)))--- arising from a use of ‘createTableStatement’--- ...-```--If we try to use it with an ADT with a single constructor but no selectors,-we get a runtime error:--``` haskell-data NoSelectors- = NotSupported Int Bool- deriving (Generic)---- $ >>> putStrLn $ createTableStatement (Proxy :: Proxy NoSelectors)--- *** Exception: constructor NotSupported has no selectors, this is not supported--- ...-```--## DefaultSignatures--There is a GHC language extension called-[DefaultSignatures](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-class-extensions.html#class-default-signatures).-In itself it has little to do with generic programming, but it makes a good-companion.--### How DefaultSignatures work--Imagine you have a type class called `ToString` which allows to convert-values to `String`s:--``` haskell-class ToString a where- toString :: a -> String-```--You can write instances manually, but you might be tempted to give the-following default implementation for `toString`:-- toString = show--The idea is that then you can just write down an empty `ToString` instance:-- instance ToString A--and you get to use `toString` on values of type `A` for free, because `A` has-a `Show` instance.--But that default implementation doesn't work, because in the class declaration-we don't have an instance for `Show a`. `ghc` says:-- Could not deduce (Show a) arising from a use of ‘show’- from the context (ToString a)--One solution would be to make `ToString` a subclass of `Show`, but then we-cannot implement `ToString` instances manually anymore for types that don't-have a `Show` instance. `DefaultSignatures` provide a better solution. The-extension allows you to further narrow down the type for your default-implementation for class methods:--``` haskell-class ToString2 a where- toString2 :: a -> String- default toString2 :: Show a => a -> String- toString2 = show-```--Then writing down empty instances works for types that have a `Show` instance:--``` haskell-instance ToString2 Int---- $ >>> toString2 (42 :: Int)--- "42"-```--Note: if you write down an empty `ToString2` instances for a type that-does not have a `Show` instance, the error message looks like this:-- No instance for (Show NoShow)--This might be confusing especially since haddock docs don't list the default-signatures or implementations and users of the class might be wondering why-`Show` comes into play at all.--### How to use `DefaultSignatures` for generic programming--`DefaultSignatures` are especially handy when doing generic programming.-Remember the type class `Serialize` from the-[second example](#nd-example-deconstructing-values-serialization)? In that-example we used it to serialize the fields of our ADTs in the generic-serialization through `genericSerialize` and `EotSerialize`. We just assumed-that we would have a manual implementation for all field types. But we also-gave it a default implementation for `serialize` in terms of-`genericSerialize`:-- default serialize :: (HasEot a, EotSerialize (Eot a)) => a -> [Int]- serialize = genericSerialize--Note that the default implementation has the same class constraints as-`genericSerialize`.--Now we can write empty instances for custom ADTs:--``` haskell-data C- = C1 Int String- deriving (Generic)--instance Serialize C-```--You could say that by giving this empty instance we give our blessing to-use `genericSerialize` for this type, but we don't have to actually implement-anything. And it works:--``` haskell--- $ >>> serialize (C1 42 "yay!")--- [0,1,42,4,121,97,121,33]-```--Important is that we still have the option to implement instances manually-by overwriting the default implementation. This is needed for basic types-like `Int` and `Char` that don't have useful generic representations. But it-also allows us to overwrite instances for ADTs manually. For example you may-want a certain type to be serialized in a special way that deviates from the-generic implementation or you may implement an instance manually for-performance gain.
+ test/Generics/Eot/Tutorial.lhs view
@@ -0,0 +1,598 @@+# `generics-eot` tutorial++This tutorial is meant to be read alongside with the haddock comments in+[Generics.Eot](http://hackage.haskell.org/package/generics-eot-0.1/docs/Generics-Eot.html).+Its source is a compiled haskell file, so we have to get some language pragmas+and imports out of the way first:++``` haskell+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MonoLocalBinds #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE UndecidableInstances #-}++module Generics.Eot.Tutorial where++import Data.Char+import Data.List+import Data.Typeable++import Generics.Eot+```++`generics-eot` allows roughly three different kinds of operations:++1. Accessing meta information about ADTs (`datatype` for names, `Proxy` and+ `Eot` for field types). Example: Generation of database schemas for ADTs.+2. Deconstructing values generically (`toEot`). Example: Serialization to a+ binary format.+3. Constructing values of an ADT generically (`fromEot`).+ Example: Deserialization from a binary format.++Sometimes only one of the three forms is used but often multiple have to be+combined. For example serialization to JSON usually requires both `datatype`+and `toEot`.++## 1st Example: Meta Information Without Types: Field Names++This simple function extracts the names of all field selectors and returns them+as a list:++``` haskell+namesOfFields :: HasEot a => Proxy a -> [String]+namesOfFields proxy =+ nub $+ concatMap (fieldNames . fields) $+ constructors $ datatype proxy+ where+ fieldNames :: Fields -> [String]+ fieldNames fields = case fields of+ Selectors names -> names+ _ -> []+```++And here's proof that it works (using+[doctest](https://github.com/sol/doctest)):++``` haskell+data A = A1 {+ foo :: String,+ bar :: Int+ }+ | A2 {+ bar :: Int,+ baz :: Bool+ }+ deriving (Generic, Show)++-- $ >>> namesOfFields (Proxy :: Proxy A)+-- ["foo","bar","baz"]+```++## The `Generic` instance: Don't forget!!!++To be able to use generic functions that are written with `generics-eot` you+need to derive an instance for `GHC.Generics.Generic` (using `DeriveGeneric`)+for your ADTs. This will automatically give you an instance for `HasEot`.++When the instance for `GHC.Generics.Generic` is missing the type error messages+are unfortunately very confusing and unhelpful. They go something like this:++ Couldn't match type ‘GHC.Generics.Rep WithoutGeneric’+ with ‘GHC.Generics.D1 c f’+ The type variables ‘c’, ‘f’ are ambiguous+ In the expression: namesOfFields (Proxy :: Proxy WithoutGeneric)++So don't forget: you need a `Generic` instance.++## `Eot`: Isomorphic representations++Part of the type class `HasEot` is the type-level function `Eot` that maps ADTs+to isomorphic types. These isomorphic types are always a combination of+`Either`s, tuples and the uninhabited type `Void`. For example this type:++``` haskell+data B = B1 Int | B2 String Bool | B3+ deriving (Generic)+```++would be mapped to:++```+Either (Int, ()) (Either (String, (Bool, ())) (Either () Void))+```++Tip: Here's how you can execute the type-level function `Eot` in `ghci`:++``` haskell+-- $ >>> :kind! Eot B+-- Eot B :: *+-- = Either (Int, ()) (Either ([Char], (Bool, ())) (Either () Void))+```++For the exact rules of this mapping see here:+[Eot](http://hackage.haskell.org/package/generics-eot-0.1/docs/Generics-Eot.html#t:Eot).++If we have an ADT `a` then we can convert values of type `a` to this isomorphic+representation `Eot a` with `toEot` and we can convert in the other direction+with `fromEot`. Generic functions always operate on these isomorphic+representations and then convert from or to the real ADTs with `fromEot` and+`toEot`.++These generic isomorphic types are referred to as "eot" -- short for+"`Either`s of tuples".++## 2nd Example: Deconstructing Values: Serialization++We start by writing a function that operates on the eot representations. The+eot representations follow simple patterns and always look similar, but they+don't look exactly the same for different ADTs. For this reason we have to use+a type class:++``` haskell+class EotSerialize eot where+ eotSerialize :: Int -- ^ The number of the constructor being passed in+ -> eot -- ^ The eot representation+ -> [Int] -- ^ A simple serialization format+```++Now we need to write instances for the types that occur in eot types. Usually+these are:++- `Either this next`:++ - If as eot value we get `Left this` it means that the original value+ was constructed with the constructor that corresponds to `this`. In this+ case we put the number of the constructor into the output and continue+ with serializing the fields of type `this`.+ - If we get `Right rest` it means that one of the following constructors was+ the one that the original value was built with. We+ continue by increasing the constructor counter and serializing the value+ of type `rest`.++ Note that this results in `EotSerialize` class constraints for both+ `this` and `rest`. If we write the correct instances for all eot types+ these constraints should always be fulfilled.++``` haskell+instance (EotSerialize this, EotSerialize next) =>+ EotSerialize (Either this next) where++ eotSerialize n (Left fields) = n : eotSerialize n fields+ eotSerialize n (Right next) = eotSerialize (succ n) next+```++- `Void`:+ We need this instance to make the compiler happy, but it'll never be+ used. If you look at the type you can also see that: an argument of type+ `Void` cannot be constructed. (Often the function `Data.Void.absurd` comes in+ handy for implementing these cases. `generics-eot` re-exports both the type+ `Data.Void.Void` and `Data.Void.absurd` for convenience.)++``` haskell+instance EotSerialize Void where+ eotSerialize _n void = absurd void+```++- `(x, xs)`:+ Right-nested 2-tuples are used to encode all the fields for one specific+ constructor. So `x` is the current field and `xs` are the remaining+ fields. To serialize this we serialize `x` (using `serialize`)+ and also write the length of the+ resulting list into the output. This will allow deserialization.++ Note: We could use `EotSerialize` to serialize the fields. But that would+ be a bit untrue to the spirit, since the fields are not eot types. Apart+ from that we might want to encode a field of e.g. type `Either a b`+ differently than the eot type `Either a b`. So we use a very similar+ but distinct type class called `Serialize`.++ The value of type `xs` contains the remaining fields and will be encoded+ recursively with `eotSerialize`:++``` haskell+instance (Serialize x, EotSerialize xs) => EotSerialize (x, xs) where+ eotSerialize n (x, xs) =+ let xInts = serialize x+ in length xInts : xInts ++ eotSerialize n xs+```++- `()`:+ Finally we need an instance for the unit type that marks the end of the+ fields encoded in 2-tuples. Since `()` doesn't carry any information, we+ can encode it as the empty list:++``` haskell+instance EotSerialize () where+ eotSerialize _ () = []+```++This is the class `Serialize`:++``` haskell+class Serialize a where+ serialize :: a -> [Int]+```++We give `serialize` a default implementation, but please ignore that for now.+It'll be explained later in the section about+[DefaultSignatures](#defaultsignatures):++``` haskell+ default serialize :: (HasEot a, EotSerialize (Eot a)) => a -> [Int]+ serialize = genericSerialize+```++`Serialize` is used to serialize every field of the used ADTs, so we need+instances for all of them:++``` haskell+instance Serialize Int where+ serialize i = [i]++instance Serialize String where+ serialize = map ord++instance Serialize Bool where+ serialize True = [1]+ serialize False = [0]++instance Serialize () where+ serialize () = []+```++To tie everything together we provide a function `genericSerialize` that+converts a value of some ADT into an eot value using `toEot` and then uses+`eotSerialize` to convert that eot value into a list of `Int`s.++``` haskell+genericSerialize :: (HasEot a, EotSerialize (Eot a)) => a -> [Int]+genericSerialize = eotSerialize 0 . toEot+```++And it works too:++``` haskell+-- $ >>> genericSerialize (A1 "foo" 42)+-- [0,3,102,111,111,1,42]+-- >>> genericSerialize (A2 23 True)+-- [1,1,23,1,1]+```++## 3rd Example: Constructing Values: Deserialization++Deserialization works very similarly. It differs in that the functions turn+lists of `Int`s into eot values.++Here's the `EotDeserialize` class with instances for:++- `Either this next`+- `Void`+- `(x, xs)`+- `()`++``` haskell+class EotDeserialize eot where+ eotDeserialize :: [Int] -> eot++instance (EotDeserialize this, EotDeserialize next) =>+ EotDeserialize (Either this next) where++ eotDeserialize (0 : r) = Left $ eotDeserialize r+ eotDeserialize (n : r) = Right $ eotDeserialize (pred n : r)+ eotDeserialize [] = error "invalid input"++instance EotDeserialize Void where+ eotDeserialize _ = error "invalid input"++instance (Deserialize x, EotDeserialize xs) =>+ EotDeserialize (x, xs) where++ eotDeserialize (len : r) =+ let (this, rest) = splitAt len r+ in (deserialize this, eotDeserialize rest)+ eotDeserialize [] = error "invalid input"++instance EotDeserialize () where+ eotDeserialize [] = ()+ eotDeserialize (_ : _) = error "invalid input"+```++And here's the `Deserialize` class plus all instances to deserialize the+fields:++``` haskell+class Deserialize a where+ deserialize :: [Int] -> a++instance Deserialize Int where+ deserialize [n] = n+ deserialize _ = error "invalid input"++instance Deserialize String where+ deserialize = map chr++instance Deserialize () where+ deserialize [] = ()+ deserialize (_ : _) = error "invalid input"++instance Deserialize Bool where+ deserialize [0] = False+ deserialize [1] = True+ deserialize _ = error "invalid input"+```++And here's `genericDeserialize` to tie it together. It uses+`eotDeserialize` to convert a list of `Int`s into an eot value and then+`fromEot` to construct a value of the wanted ADT.++``` haskell+genericDeserialize :: (HasEot a, EotDeserialize (Eot a)) => [Int] -> a+genericDeserialize = fromEot . eotDeserialize+```++Here you can see it in action:++``` haskell+-- $ >>> genericDeserialize [0,3,102,111,111,1,42] :: A+-- A1 {foo = "foo", bar = 42}+-- >>> genericDeserialize [1,1,23,1,1] :: A+-- A2 {bar = 23, baz = True}+```++And it is the inverse of `genericSerialize`:++``` haskell+-- $ >>> (genericDeserialize $ genericSerialize $ A1 "foo" 42) :: A+-- A1 {foo = "foo", bar = 42}+```++## 4th Example: Meta Information with types: generating SQL schemas++Accessing meta information __including__ the types works very+similarly to deconstructing or constructing values. It uses the same+structure of type classes and instances for the eot-types. The difference is:+since we don't want actual values of our ADT as input or output we operate on+`Proxy`s of our eot-types.++As an example we're going to implement a function that generates SQL statements+that create tables that our ADTs would fit into. To be able to use nice names+for the table and columns we're going to traverse the type-less meta+information (see+[1st Example](#st-example-meta-information-without-types-field-names)) at the+same time.++(Note that the generated SQL statements are targeted at a fictional+database implementation that magically understands Haskell types like+`Int` and `String`, or rather `[Char]`.)++Again we start off by writing a class that operates on the eot-types. Besides+the eot-type the class has an additional parameter, `meta`, that will be+instantiated by the corresponding types used for untyped meta information.++``` haskell+class EotCreateTableStatement meta eot where+ eotCreateTableStatement :: meta -> Proxy eot -> [String]+```++Our first instance is for the complete datatype. `eot` is instantiated to+`Either fields Void`. Note that this instance only works for ADTs with+exactly one constructor as we don't support types with multiple constructors.+`meta` is instantiated to `Datatype` which is the type for meta information+for ADTs.++``` haskell+instance EotCreateTableStatement [String] fields =>+ EotCreateTableStatement Datatype (Either fields Void) where++ eotCreateTableStatement datatype Proxy = case datatype of+ Datatype name [Constructor _ (Selectors fields)] ->+ "CREATE TABLE " :+ name :+ " COLUMNS " :+ "(" :+ intercalate ", " (eotCreateTableStatement fields (Proxy :: Proxy fields)) :+ ");" :+ []+ Datatype _ [Constructor name (NoSelectors _)] ->+ error ("constructor " ++ name ++ " has no selectors, this is not supported")+ Datatype name _ ->+ error ("type " ++ name ++ " must have exactly one constructor")+```++The second instance is responsible for creating the parts of the SQL+statements that declare the columns. As such it has to traverse the fields+of our ADT. `eot` is instantiated to the usual `(x, xs)`. `meta` is+instantiated to `[String]`, representing the field names. The name of the+field type is obtained using `typeRep`, therefore we need a `Typeable x`+constraint.++``` haskell+instance (Typeable x, EotCreateTableStatement [String] xs) =>+ EotCreateTableStatement [String] (x, xs) where++ eotCreateTableStatement (field : fields) Proxy =+ (field ++ " " ++ show (typeRep (Proxy :: Proxy x))) :+ eotCreateTableStatement fields (Proxy :: Proxy xs)+ eotCreateTableStatement [] Proxy = error "impossible"+```++The last instance is for `()`. It's needed as the base case for+traversing the fields and as such returns just an empty list.++``` haskell+instance EotCreateTableStatement [String] () where+ eotCreateTableStatement [] Proxy = []+ eotCreateTableStatement (_ : _) Proxy = error "impossible"+```++`createTableStatement` ties everything together. It obtains the meta+information through `datatype` passing a `Proxy` for `a`. And it creates a+`Proxy` for the eot-type `Proxy :: Proxy (Eot a)`. Then it calls+`eotCreateTableStatement` and just `concat`s the resulting snippets.++``` haskell+createTableStatement :: forall a . (HasEot a, EotCreateTableStatement Datatype (Eot a)) =>+ Proxy a -> String+createTableStatement proxy =+ concat $ eotCreateTableStatement (datatype proxy) (Proxy :: Proxy (Eot a))+```++As an example, we're going to use `Person`:++``` haskell+data Person+ = Person {+ name :: String,+ age :: Int+ }+ deriving (Generic)+```++And here's the created SQL statement:++``` haskell+-- $ >>> putStrLn $ createTableStatement (Proxy :: Proxy Person)+-- CREATE TABLE Person COLUMNS (name [Char], age Int);+```++If we try to use an ADT with multiple constructors, we get a type error+due to a missing instance:++``` haskell+-- $ >>> putStrLn $ createTableStatement (Proxy :: Proxy A)+-- ...+-- • No instance for ‘EotCreateTableStatement+-- Datatype+-- (Either ([Char], (Int, ())) (Either (Int, (Bool, ())) Void))’+-- arising from a use of ‘createTableStatement’+-- ...+```++If we try to use it with an ADT with a single constructor but no selectors,+we get a runtime error:++``` haskell+data NoSelectors+ = NotSupported Int Bool+ deriving (Generic)++-- $ >>> putStrLn $ createTableStatement (Proxy :: Proxy NoSelectors)+-- *** Exception: constructor NotSupported has no selectors, this is not supported+-- ...+```++## DefaultSignatures++There is a GHC language extension called+[DefaultSignatures](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-class-extensions.html#class-default-signatures).+In itself it has little to do with generic programming, but it makes a good+companion.++### How DefaultSignatures work++Imagine you have a type class called `ToString` which allows to convert+values to `String`s:++``` haskell+class ToString a where+ toString :: a -> String+```++You can write instances manually, but you might be tempted to give the+following default implementation for `toString`:++ toString = show++The idea is that then you can just write down an empty `ToString` instance:++ instance ToString A++and you get to use `toString` on values of type `A` for free, because `A` has+a `Show` instance.++But that default implementation doesn't work, because in the class declaration+we don't have an instance for `Show a`. `ghc` says:++ Could not deduce (Show a) arising from a use of ‘show’+ from the context (ToString a)++One solution would be to make `ToString` a subclass of `Show`, but then we+cannot implement `ToString` instances manually anymore for types that don't+have a `Show` instance. `DefaultSignatures` provide a better solution. The+extension allows you to further narrow down the type for your default+implementation for class methods:++``` haskell+class ToString2 a where+ toString2 :: a -> String+ default toString2 :: Show a => a -> String+ toString2 = show+```++Then writing down empty instances works for types that have a `Show` instance:++``` haskell+instance ToString2 Int++-- $ >>> toString2 (42 :: Int)+-- "42"+```++Note: if you write down an empty `ToString2` instances for a type that+does not have a `Show` instance, the error message looks like this:++ No instance for (Show NoShow)++This might be confusing especially since haddock docs don't list the default+signatures or implementations and users of the class might be wondering why+`Show` comes into play at all.++### How to use `DefaultSignatures` for generic programming++`DefaultSignatures` are especially handy when doing generic programming.+Remember the type class `Serialize` from the+[second example](#nd-example-deconstructing-values-serialization)? In that+example we used it to serialize the fields of our ADTs in the generic+serialization through `genericSerialize` and `EotSerialize`. We just assumed+that we would have a manual implementation for all field types. But we also+gave it a default implementation for `serialize` in terms of+`genericSerialize`:++ default serialize :: (HasEot a, EotSerialize (Eot a)) => a -> [Int]+ serialize = genericSerialize++Note that the default implementation has the same class constraints as+`genericSerialize`.++Now we can write empty instances for custom ADTs:++``` haskell+data C+ = C1 Int String+ deriving (Generic)++instance Serialize C+```++You could say that by giving this empty instance we give our blessing to+use `genericSerialize` for this type, but we don't have to actually implement+anything. And it works:++``` haskell+-- $ >>> serialize (C1 42 "yay!")+-- [0,1,42,4,121,97,121,33]+```++Important is that we still have the option to implement instances manually+by overwriting the default implementation. This is needed for basic types+like `Int` and `Char` that don't have useful generic representations. But it+also allows us to overwrite instances for ADTs manually. For example you may+want a certain type to be serialized in a special way that deviates from the+generic implementation or you may implement an instance manually for+performance gain.
test/Generics/Eot/TutorialSpec.hs view
@@ -1,9 +1,5 @@-{-# LANGUAGE CPP #-}- module Generics.Eot.TutorialSpec where -#if MIN_VERSION_base(4,9,0)- import Test.DocTest import Test.Hspec @@ -13,13 +9,4 @@ spec = describe "tutorial" $ do it "doctests" $ do- doctest (words "src/Generics/Eot/Tutorial.lhs -isrc -pgmL markdown-unlit")--#else--import Test.Hspec--spec :: Spec-spec = return ()--#endif+ doctest (words "test/Generics/Eot/Tutorial.lhs -isrc -pgmL markdown-unlit")
test/quickcheck/DatatypeSpec.hs view
@@ -21,6 +21,7 @@ import System.Directory import System.Exit import System.FilePath+import System.IO.Silently import Test.Hspec hiding (Selector) import Test.Hspec.QuickCheck import Test.Mockery.Directory@@ -31,7 +32,7 @@ spec :: Spec spec = modifyMaxSize (const 20) $ modifyMaxSuccess (const 20) $ do describe "datatype" $ do- it "works for every ADT" $ do+ around_ silence $ it "works for every ADT" $ do property $ \ dt -> test dt [i| {-# LANGUAGE DeriveGeneric #-} @@ -50,6 +51,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE StandaloneDeriving #-} + import Prelude (Int, Show(..), (/=), ($), error, (++), unlines) import Generics.Eot import Control.Monad