Alexey Kotlyarov
Where does this instance come from, and how to make more?
Type | Isomorphic to | |
---|---|---|
() |
Unit |
|
Maybe a |
Sum Unit a |
|
[a] |
Sum Unit (Product a [a]) |
|
User |
Product Text Text |
To write an instance for any data type:
Unit
, Sum
and Product
....but we still can't write a Show
!
The set of values is the same, yet the types are different.
-- Phantom type level strings
data Data (n :: Symbol) a = Data a
data Constructor (n :: Symbol) a = Constructor a
data Selector (n :: Symbol) a = Selector a
dataName :: KnownSymbol n => Data n a -> String
constructorName :: KnownSymbol n => Constructor n a -> String
selectorName :: KnownSymbol n => Selector n a -> String
Type | Representation | |
---|---|---|
() |
Data "()" (Constructor "()" Unit) |
|
Maybe a |
Data "Maybe" (Sum (Constructor "Nothing" Unit) (Constructor "Just" a)) |
|
[a] |
Data "[]" (Sum (Constructor "[]" Unit) (Constructor ":" (Product a [a]))) |
|
User |
Data "User" (Constructor "User" (Product (Selector "name" Text) (Selector "email" Text))) |
data U1 p = U1 -- Unit
data (:+:) f g p = L1 (f p) | R1 (g p) -- Sum
data (:*:) f g p = (f p) :*: (g p) -- Product
data R
newtype K1 i c p = K1 { unK1 :: c } -- Value of type c
type Rec0 = K1 R
Type | Representation so far | |
---|---|---|
() |
U1 |
|
Maybe Int |
U1 :+: Rec0 Int |
|
User |
Rec0 Text :*: Rec0 Text |
p
is a phantom type parameter used later.
data D
data C
data S
newtype M1 i (c :: Meta) f p = M1 { unM1 :: f p }
type D1 = M1 D -- Data type
type C1 = M1 C -- Constructor
type S1 = M1 S -- Selector
Type | Representation so far | |
---|---|---|
() |
D1 _ (C1 _ U1) |
|
Maybe Int |
D1 _ (C1 _ U1 :+: C1 _ (S1 _ (Rec0 Int))) |
|
User |
D1 _ (C1 _ (S1 _ (Rec0 Text) :*: S1 _ (Rec0 Text))) |
Meta
is information about the type name, laziness,
etc.
datatypeName :: Datatype m => d m f p -> String
conName :: Constructor m => c m f p -> String
selName :: Selector m => s m f p -> String
In practice:
λ> :kind! (Rep User)
(Rep User) :: * -> *
= D1
('MetaData "User" "Main" "main" 'False)
(C1
('MetaCons "User" 'PrefixI 'True)
(S1
('MetaSel
('Just "name")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 Text)
:*: S1
('MetaSel
('Just "email")
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(Rec0 Text)))
kind!
on GHCi prompt is most useful when working with
generics.
λ> from $ User "Spike" "spike@mail.mars"
M1 {unM1 = M1 {unM1 = M1 {unM1 = K1 {unK1 = "Spike"}} :*:
M1 {unM1 = K1 {unK1 = "spike@mail.mars"}}}}
λ> from $ Just 1
M1 {unM1 = R1 (M1 {unM1 = M1 {unM1 = K1 {unK1 = 1}}})}
M1
wrappers corresponding to the
data type, the constructor and the selector.:*:
in the product type and R1
in the
sum type.type FormData = Map Text [Maybe Text] -- a=1&b=2&b=3&c&c=4
class ToFormData a where
toFormData :: a -> FormData
Modifying to use generic instances:
Base case, single value:
-- instance ToFormData' (Rec0 Text) where
-- toFormData' (K1 a) = error "We can't write this without the selector name!"
instance Selector s => ToFormData' (S1 s (Rec0 Text)) where
toFormData' m@(M1 (K1 a)) = M.singleton (T.pack $ selName m) [Just a]
Product and sum:
instance (ToFormData' f, ToFormData' s) => ToFormData' (f :*: s) where
toFormData' (a :*: b) = M.union (toFormData' a) (toFormData' b)
instance (ToFormData' l, ToFormData' r) => ToFormData' (l :+: r) where
toFormData' (L1 l) = toFormData' l
toFormData' (R1 r) = toFormData' r
Ignore the rest of the meta:
Generic
...ToFormData'
...ToFormData
does not need any methods.Alternatively:
class FromFormData a where
fromFormData :: FormData -> Maybe a
default fromFormData :: (Generic a, FromFormData' (Rep a)) => FormData -> Maybe a
fromFormData = fmap to . fromFormData'
class FromFormData' a where
fromFormData' :: Map Text [Maybe Text] -> Maybe (a x)
Let's not care about any meta unless we say otherwise:
instance {-# OVERLAPPABLE #-} FromFormData' a => FromFormData' (M1 i c a) where
fromFormData' = fmap M1 . fromFormData'
Sum and product:
instance (FromFormData' f, FromFormData' s) =>
FromFormData' (f :*: s) where
fromFormData' form = liftA2 (:*:) (fromFormData' form) (fromFormData' form)
instance (FromFormData' l, FromFormData' r) =>
FromFormData' (l :+: r) where
fromFormData' form =
fmap L1 (fromFormData' form) <|> fmap R1 (fromFormData' form)
Base case:
-- Note, overlaps instance for M1, because S1 = M1 S
instance Selector m =>
FromFormData' (S1 m (Rec0 Text)) where
fromFormData' form =
let key = selName (undefined :: S1 m t p)
in case M.lookup (T.pack key) form of
Just [Just txt] -> Just $ M1 $ K1 txt
_ -> Nothing
selName
.selName
only needs the type of the value to work.S1 m t p
and S1 m (Rec0 Text) p
are both
fine, since selector name depends on m
and not
t
.Show
Ord
Eq
What can't be built with just Generic
?
Generic
is a class for types of kind *
, for
* -> *
there is
λ> :kind! (Rep1 Expr)
(Rep1 Expr) :: * -> *
= D1
('MetaData "Expr" "Main" "main" 'False)
(C1
('MetaCons "Var" 'PrefixI 'False)
(S1
('MetaSel ...)
(Rec0 Text))
:+: (C1
('MetaCons "Const" 'PrefixI 'False)
(S1
('MetaSel ...)
Par1)
:+: C1
('MetaCons "S" 'PrefixI 'False)
(S1
('MetaSel ...)
(Rec1 Expr)
:*: S1
('MetaSel ...)
(Rec1 Expr))))
Talk: https://www.koterpillar.com/instances-for-everyone
Links: