Safe Haskell | None |
---|---|
Language | Haskell2010 |
This module provides preliminary Haskell supprot for decoding and encoding the Candid data format. See https://github.com/dfinity/candid/blob/master/spec/Candid.md for the official Candid specification.
Warning: The interface of this library is still in flux, as we are yet learning the best idioms around Candid and Haskell.
Synopsis
- encode :: CandidArg a => a -> ByteString
- encodeBuilder :: forall a. CandidArg a => a -> Builder
- decode :: forall a. CandidArg a => ByteString -> Either String a
- class (Typeable a, CandidVal (AsCandid a)) => Candid a where
- type AsCandid a
- toCandid :: a -> AsCandid a
- fromCandid :: AsCandid a -> a
- type CandidRow r = (Typeable r, AllUniqueLabels r, AllUniqueLabels (Map (Either String) r), Forall r Candid, Forall r Unconstrained1)
- type CandidArg a = (CandidSeq (AsTuple a), Tuplable a)
- class Typeable a => CandidVal a
- seqDesc :: forall a. CandidArg a => SeqDesc
- data SeqDesc
- tieKnot :: SeqDesc -> [Type Void]
- typeDesc :: forall a. Candid a => Type Void
- newtype Unary a = Unary {
- unUnary :: a
- newtype Principal = Principal {}
- prettyPrincipal :: Principal -> Text
- parsePrincipal :: Text -> Either String Principal
- data Reserved = Reserved
- newtype AsRecord a = AsRecord {
- unAsRecord :: a
- newtype AsVariant a = AsVariant {
- unAsVariant :: a
- type CandidService m r = (Forall r (CandidMethod m), AllUniqueLabels r)
- type RawService m = Text -> ByteString -> m ByteString
- toCandidService :: forall m r. CandidService m r => (forall a. String -> m a) -> RawService m -> Rec r
- fromCandidService :: forall m r. CandidService m r => (forall a. Text -> m a) -> (forall a. String -> m a) -> Rec r -> RawService m
- candid :: QuasiQuoter
- candidFile :: QuasiQuoter
- candidType :: QuasiQuoter
- data Type a
- type Fields a = [(FieldName, Type a)]
- data FieldName
- labledField :: Text -> FieldName
- hashedField :: Word32 -> FieldName
- fieldHash :: FieldName -> Word32
- escapeFieldName :: FieldName -> Text
- unescapeFieldName :: Text -> FieldName
- candidHash :: Text -> Word32
- data Value
- = NumV Scientific
- | NatV Natural
- | Nat8V Word8
- | Nat16V Word16
- | Nat32V Word32
- | Nat64V Word64
- | IntV Integer
- | Int8V Int8
- | Int16V Int16
- | Int32V Int32
- | Int64V Int64
- | Float32V Float
- | Float64V Double
- | BoolV Bool
- | TextV Text
- | NullV
- | ReservedV
- | OptV (Maybe Value)
- | VecV (Vector Value)
- | RecV [(FieldName, Value)]
- | TupV [Value]
- | VariantV FieldName Value
- | PrincipalV Principal
- | BlobV ByteString
- | AnnV Value (Type Void)
- decodeVals :: ByteString -> Either String [Value]
- fromCandidVals :: CandidArg a => [Value] -> Either String a
- toCandidVals :: CandidArg a => a -> [Value]
- encodeDynValues :: [Value] -> Either String Builder
- encodeTextual :: String -> Either String ByteString
- data DidFile
- parseDid :: String -> Either String DidFile
- parseValue :: String -> Either String Value
- parseValues :: String -> Either String [Value]
Tutorial
Candid is inherently typed, so before encoding or decoding, you have to indicate the types to use. In most cases, you can use Haskell types for that:
Haskell types
The easiest way is to use this library is to use the canonical Haskell types. Any type that is an instance of Candid
can be used:
>>>
encode ([True, False], Just 100)
"DIDL\STXm~n|\STX\NUL\SOH\STX\SOH\NUL\SOH\228\NUL">>>
decode (encode ([True, False], Just 100)) == Right ([True, False], Just 100)
True
Here, no type annotations are needed, the library can infer them from the types of the Haskell values. You can see the Candid types used using typeDesc
and seqDesc
:
>>>
:type +d ([True, False], Just 100)
([True, False], Just 100) :: ([Bool], Maybe Integer)>>>
:set -XTypeApplications
>>>
pretty (tieKnot (seqDesc @([Bool], Maybe Integer)))
(vec bool, opt int)
This library is integrated with the row-types
library, so you can use their
records directly:
>>>
:set -XOverloadedLabels
>>>
import Data.Row
>>>
encode (#foo .== [True, False] .+ #bar .== Just 100)
"DIDL\ETXl\STX\211\227\170\STX\SOH\134\142\183\STX\STXn|m~\SOH\NUL\SOH\228\NUL\STX\SOH\NUL">>>
:set -XDataKinds -XTypeOperators
>>>
pretty (typeDesc @(Rec ("bar" .== Maybe Integer .+ "foo" .== [Bool])))
record {bar : opt int; foo : vec bool}
Custom types
If you want to use your own types directly, you have to declare an instance of the Candid
type class. In this instance, you indicate a canonical Haskel type to describe how your type should serialize, and provide conversion functions to the corresponding AsCandid
.
>>>
:set -XTypeFamilies
>>>
newtype Age = Age Integer
>>>
:{
instance Candid Age where type AsCandid Age = Integer toCandid (Age i) = i fromCandid = Age :}
>>>
encode (Age 42)
"DIDL\NUL\SOH|*"
This is more or less the only way to introduce recursive types:
>>>
data Peano = N | S Peano deriving (Show, Eq)
>>>
:{
instance Candid Peano where type AsCandid Peano = Maybe Peano toCandid N = Nothing toCandid (S p) = Just p fromCandid Nothing = N fromCandid (Just p) = S p :}
>>>
peano = S (S (S N))
>>>
encode peano
"DIDL\SOHn\NUL\SOH\NUL\SOH\SOH\SOH\NUL"
Generic types
Especially for Haskell record types, you can use magic involving generic types to create the Candid
instance automatically. The best way is using the DerivingVia
langauge extension,using the AsRecord
new type to indicate that this strategy should be used:
>>>
:set -XDerivingVia -XDeriveGeneric -XUndecidableInstances
>>>
import GHC.Generics (Generic)
>>>
:{
data SimpleRecord = SimpleRecord { foo :: [Bool], bar :: Maybe Integer } deriving Generic deriving Candid via (AsRecord SimpleRecord) :}
>>>
pretty (typeDesc @SimpleRecord)
record {bar : opt int; foo : vec bool}>>>
encode (SimpleRecord { foo = [True, False], bar = Just 100 })
"DIDL\ETXl\STX\211\227\170\STX\SOH\134\142\183\STX\STXn|m~\SOH\NUL\SOH\228\NUL\STX\SOH\NUL"
Unfortunately, this feature requires UndecidableInstances
.
This works for variants too:
>>>
:{
data Shape = Point () | Sphere Double | Rectangle (Double, Double) deriving Generic deriving Candid via (AsVariant Shape) :}
>>>
pretty (typeDesc @Shape)
variant {Point; Rectangle : record {0 : float64; 1 : float64}; Sphere : float64}>>>
encode (Rectangle (100,100))
"DIDL\STXk\ETX\176\200\244\205\ENQ\DEL\143\232\190\218\v\SOH\173\198\172\140\SIrl\STX\NULr\SOHr\SOH\NUL\SOH\NUL\NUL\NUL\NUL\NUL\NULY@\NUL\NUL\NUL\NUL\NUL\NULY@"
Because data constructors are capitalized in Haskell, you cannot derive enums or variants with lower-case names. Also, nullary data constructors are not supported by row-types
, and thus here, even though they would nicely map onto variants with arguments of type 'null
.
Candid services
Very likely you want to either implement or use whole Candid interfaces. In order to apply the encoding/decoding in one go, you can use fromCandidService
and toCandidService
. These convert between a raw service (RawService
, takes a method name and bytes, and return bytes), and a typed CandidService
(expressed as an Rec
record).
Let us create a simple service:
>>>
:set -XOverloadedLabels
>>>
import Data.Row
>>>
import Data.Row.Internal
>>>
import Data.IORef
>>>
c <- newIORef 0
>>>
let service = #get .== (\() -> readIORef c) .+ #inc .== (\d -> modifyIORef c (d +))
>>>
service .! #get $ ()
0>>>
service .! #inc $ 5
>>>
service .! #get $ ()
5
For convenience, we name its type
>>>
:t service
service :: Rec ('R '[ "get" ':-> (() -> IO Integer), "inc" ':-> (Integer -> IO ())])>>>
:set -XTypeOperators -XDataKinds -XFlexibleContexts
>>>
type Interface = 'R '[ "get" ':-> (() -> IO Integer), "inc" ':-> (Integer -> IO ())]
Now we can turn this into a raw service operating on bytes:
>>>
let raw = fromCandidService (error . show) error service
>>>
raw (T.pack "get") (BS.pack "DUDE")
*** Exception: Failed reading: Expected magic bytes "DIDL", got "DUDE" ...>>>
raw (T.pack "get") (BS.pack "DIDL\NUL\NUL")
"DIDL\NUL\SOH|\ENQ">>>
raw (T.pack "inc") (BS.pack "DIDL\NUL\SOH|\ENQ")
"DIDL\NUL\NUL">>>
service .! #get $ ()
10
And finally, we can turn this raw function back into a typed interface:
>>>
let service' :: Rec Interface = toCandidService error raw
>>>
service .! #get $ ()
10>>>
service .! #inc $ 5
>>>
service .! #get $ ()
15
In a real application you would more likely pass some networking code to toCandidService
.
Importing Candid
In the example above, we wrote the type of the service in Haskell. But very
likely you want to talk to a service whose is given to you in the form of a
.did
files, like
service : { get : () -> (int); inc : (int) -> (); }
You can parse such a description:
>>>
either error pretty $ parseDid "service : { get : () -> (int); inc : (int) -> (); }"
service : {get : () -> (int); inc : (int) -> ();}
And you can even, using Template Haskell, turn this into a proper Haskell type. The candid
antiquotation produces a type, and expects a free type variable m
for the monad you want to use.
>>>
:set -XQuasiQuotes
>>>
import Data.Row.Internal
>>>
type Counter m = [candid| service : { get : () -> (int); inc : (int) -> (); } |]
>>>
:info Counter
type Counter :: (* -> *) -> Row * type Counter m = ("get" .== (() -> m Integer)) .+ ("inc" .== (Integer -> m ())) :: Row * ...
You can then use this with toCandidService
to talk to a service.
If you want to read the description from a .did
file, you can use candidFile
.
If this encounters a Candid type definition, it will just inline them. This means that cyclic type definitions are not supported.
Dynamic use
Sometimes one needs to interact with Candid in a dynamic way, without static type information.
This library allows the parsing and pretty-printing of candid values. The binary value was copied from above:
>>>
import Data.Row
>>>
:set -XDataKinds -XTypeOperators
>>>
let bytes = encode (#bar .== Just 100 .+ #foo .== [True,False])
>>>
let Right vs = decodeVals bytes
>>>
pretty vs
(record {4895187 = opt +100; 5097222 = vec {true; false}})
As you can see, the binary format does not preserve the field names. Future versions of this library will allow you to specify the (dynamic) Type
at which you want to decode these values, to overcome that problem.
Conversely, you can encode from the textual representation:
>>>
let Right bytes = encodeTextual "record { foo = vec { true; false }; bar = opt 100 }"
>>>
bytes
"DIDL\ETXl\STX\211\227\170\STX\STX\134\142\183\STX\SOHm~n}\SOH\NUL\SOHd\STX\SOH\NUL">>>
decode @(Rec ("bar" .== Maybe Integer .+ "foo" .== [Bool])) bytes
Right (#bar .== Just 100 .+ #foo .== [True,False])
This function does not support the full textual format yet; in particular type annotation can only be used around number literals.
Missing features
- Generating interface descriptions (.did files) from Haskell functions
- Service and function types
- Future types
- Parsing the textual representation dynamically against an expected type
- Method annotations in service types
Reference
Encoding and decoding
encode :: CandidArg a => a -> ByteString Source #
Encode based on Haskell type
encodeBuilder :: forall a. CandidArg a => a -> Builder Source #
Encode to a Builder
based on Haskell type
Type classes
class (Typeable a, CandidVal (AsCandid a)) => Candid a where Source #
The class of Haskell types that can be converted to Candid.
You can create intances of this class for your own types, see the tutorial above for examples. The default instance is mostly for internal use.
Nothing
toCandid :: a -> AsCandid a Source #
fromCandid :: AsCandid a -> a Source #
default fromCandid :: a ~ AsCandid a => AsCandid a -> a Source #
Instances
type CandidRow r = (Typeable r, AllUniqueLabels r, AllUniqueLabels (Map (Either String) r), Forall r Candid, Forall r Unconstrained1) Source #
type CandidArg a = (CandidSeq (AsTuple a), Tuplable a) Source #
The class of types that can be used as Candid argument sequences.
Essentially all types that are in Candid
, but tuples need to be treated specially.
class Typeable a => CandidVal a Source #
The internal class of Haskell types that canonically map to Candid.
You would add instances to the Candid
type class.
asType, toCandidVal', fromCandidVal'
Instances
Special types
A newtype to stand in for the unary tuple
Instances
Instances
Eq Principal Source # | |
Ord Principal Source # | |
Defined in Codec.Candid.Data | |
Show Principal Source # | |
Candid Principal Source # | |
CandidVal Principal Source # | |
Defined in Codec.Candid.Class | |
type AsCandid Principal Source # | |
Defined in Codec.Candid.Class |
prettyPrincipal :: Principal -> Text Source #
Instances
Eq Reserved Source # | |
Ord Reserved Source # | |
Defined in Codec.Candid.Data | |
Show Reserved Source # | |
Candid Reserved Source # | |
CandidVal Reserved Source # | |
Defined in Codec.Candid.Class | |
type AsCandid Reserved Source # | |
Defined in Codec.Candid.Class |
Generics
This newtype encodes a Haskell record type using generic programming. Best used with DerivingVia
, as shown in the tutorial.
AsRecord | |
|
This newtype encodes a Haskell data type as a variant using generic programming. Best used with DerivingVia
, as shown in the tutorial.
AsVariant | |
|
Candid services
type CandidService m r = (Forall r (CandidMethod m), AllUniqueLabels r) Source #
A Candid service. The r
describes the type of a Rec
.
type RawService m = Text -> ByteString -> m ByteString Source #
A raw service, operating on bytes
:: forall m r. CandidService m r | |
=> (forall a. String -> m a) | What to do if the raw service returns unparsable data |
-> RawService m | |
-> Rec r |
Turns a raw service (function operating on bytes) into a typed Candid service (a record of typed methods). The raw service is typically code that talks over the network.
:: forall m r. CandidService m r | |
=> (forall a. Text -> m a) | What to do if the method name does not exist |
-> (forall a. String -> m a) | What to do when the caller provides unparsable data |
-> Rec r | |
-> RawService m |
Turns a typed candid service into a raw service. Typically used in a framework warpping Candid services.
Meta-programming
candid :: QuasiQuoter Source #
This quasi-quoter turns a Candid description into a Haskell type. It assumes a type variable m
to be in scope.
candidFile :: QuasiQuoter Source #
As candid
, but takes a filename
candidType :: QuasiQuoter Source #
This quasi-quoter turns works on individual candid types, e.g.
type InstallMode = [candidType| variant {install : null; reinstall : null; upgrade : null}; |]
Types and values
NatT | |
Nat8T | |
Nat16T | |
Nat32T | |
Nat64T | |
IntT | |
Int8T | |
Int16T | |
Int32T | |
Int64T | |
Float32T | |
Float64T | |
BoolT | |
TextT | |
NullT | |
ReservedT | |
EmptyT | |
OptT (Type a) | |
VecT (Type a) | |
RecT (Fields a) | |
VariantT (Fields a) | |
PrincipalT | |
BlobT | |
RefT a | A reference to a named type |
Instances
Monad Type Source # | |
Functor Type Source # | |
Applicative Type Source # | |
Foldable Type Source # | |
Defined in Codec.Candid.Types fold :: Monoid m => Type m -> m # foldMap :: Monoid m => (a -> m) -> Type a -> m # foldMap' :: Monoid m => (a -> m) -> Type a -> m # foldr :: (a -> b -> b) -> b -> Type a -> b # foldr' :: (a -> b -> b) -> b -> Type a -> b # foldl :: (b -> a -> b) -> b -> Type a -> b # foldl' :: (b -> a -> b) -> b -> Type a -> b # foldr1 :: (a -> a -> a) -> Type a -> a # foldl1 :: (a -> a -> a) -> Type a -> a # elem :: Eq a => a -> Type a -> Bool # maximum :: Ord a => Type a -> a # | |
Traversable Type Source # | |
Eq a => Eq (Type a) Source # | |
Ord a => Ord (Type a) Source # | |
Show a => Show (Type a) Source # | |
Pretty a => Pretty (Type a) Source # | |
Defined in Codec.Candid.Types |
A type for a Candid field name. Essentially a Word32
with maybe a textual label attached
escapeFieldName :: FieldName -> Text Source #
unescapeFieldName :: Text -> FieldName Source #
The inverse of escapeFieldName
candidHash :: Text -> Word32 Source #
The Candid field label hashing algorithm
Dynamic use
decodeVals :: ByteString -> Either String [Value] Source #
Decode to value representation
toCandidVals :: CandidArg a => a -> [Value] Source #
encodeDynValues :: [Value] -> Either String Builder Source #
Encodes a Candid value given in the dynamic Value
form, at inferred type.
This may fail if the values have inconsistent types. It does not use the
reserved
supertype (unless explicitly told to).
encodeTextual :: String -> Either String ByteString Source #
Encodes a Candid value given in textual form.
This may fail if the textual form cannot be parsed or has inconsistent
types. It does not use the reserved
supertype (unless explicitly told to).