Copyright | © 2018 Mark Karpov |
---|---|
License | BSD 3 clause |
Maintainer | Mark Karpov <markkarpov92@gmail.com> |
Stability | experimental |
Portability | portable |
Safe Haskell | None |
Language | Haskell2010 |
This package provides a solution for serialization of arbitrary Haskell values, monomorphic functions, and closures without relying on remote tables or Template Haskell, with minimum boilderplate.
To use the package, be sure to enable the following language extensions:
The following form of import is recommended:
import Data.Imprint (Imprint, Col (..), Dict (..), (<:>)) import qualified Data.Imprint as I
To serialize a value, we must first create an Imprint
of it. If the
value in question is an instance of the Binary
type class, then the
binary
function should be used:
intImprint :: Imprint 'Z Int intImprint = I.binary (static Dict) 4
The static
keyword has to do with the concept of static pointers, see
the link above. We won't go into the details here, but it suffices to say
that we need to have an evidence of existence of Binary
instance in
serializable form. static
, being a keyword, not a function, has to be
used like this and cannot be put inside binary
, because it creates a
pointer to a concrete thing that is passed to it. This little ceremony of
passing static Dict
as the first argument of binary
every time you
create an Imprint
of a value that has a Binary
instance is the only
boilderplate we have to put up with, though.
To create an Imprint
of a function or indeed almost anything that has
no Binary
instance, we use the static
function:
funImprint :: Imprint 'Z (Int -> String -> String) funImprint = I.static (static f) where f n str = str ++ show n
The f
function we want to serialize may be defined anywhere. Note that
the resulting Imprint
is opaque and has no sign of how it was created
(with binary
or with static
).
Finally, there is a way to apply an Imprint
of a value to an Imprint
of a function with (
:<:>
)
closureImprint :: Imprint ('Z ':~> Int) (String -> String) closureImprint = funImprint <:> intImprint
Note how the applied arguments are collected in the phantom type (the
first argument of Imprint
type constructor). There is no requirement to
apply all arguments, you may transmit a partially applied function all
right.
Now, to serialization. That is quite simple, because Imprint
is an
instance of Binary
and so it is perfectly serializable. On the
receiving site, you however must know the full type of Imprint
,
including the collection of applied arguments in order to restore it.
If a more dynamic approach is desirable, we could adopt the
representation of closures used in distributed-process
as a special
case with the following type of Imprint
:
Imprint ('Z ':~> ByteString) (Process ())
In that case we would need to serialize all the arguments beforehand and
put the deserializing code into the ByteString -> Process ()
function.
Finally, we give the guarantee that if you have a value of the type
, then you can have the Imprint
as aa
value back, see restore
:
restore :: Imprint bs a -> a
Types
data Imprint (bs :: Col *) a Source #
is an image of Imprint
bs aa
that is isomorphic to a
and
serializable.
This helper type is used to build the phantom type holding types of the
arguments applied to an Imprint
of a function.
data Dict a :: Constraint -> * where #
Values of type
capture a dictionary for a constraint of type Dict
pp
.
e.g.
Dict
::Dict
(Eq
Int
)
captures a dictionary that proves we have an:
instance Eq
'Int
Pattern matching on the Dict
constructor will bring this instance into scope.
a :=> (Read (Dict a)) | |
a :=> (Monoid (Dict a)) | |
a :=> (Enum (Dict a)) | |
a :=> (Bounded (Dict a)) | |
() :=> (Eq (Dict a)) | |
() :=> (Ord (Dict a)) | |
() :=> (Show (Dict a)) | |
a => Bounded (Dict a) | |
a => Enum (Dict a) | |
Eq (Dict a) | |
(Typeable Constraint p, p) => Data (Dict p) | |
Ord (Dict a) | |
a => Read (Dict a) | |
Show (Dict a) | |
a => Monoid (Dict a) | |