I have some code for a RPC-style services, where the calls to a service are represented as a GADT with the result type as a type parameter, alike
data Service a where
Get :: Service Int
Set :: Int -> Service ()
Then, I have some code to call said service, a la
call ::
forall msg.
(forall res. msg res -> Encoding) ->
(forall res. msg res -> Decoder RealWorld res) ->
msg res ->
m res
call encode mkDecoder msg = _
This Encoding
and Decoder
comes from the cborg
package. Now, a consumer of the library may have Serialise
instances for forall res. msg res
around, as well as for every possible res
. I can use the former easily, but I’m stuck on how to consume the latter.
I can define a function like
callSerialise ::
(forall res. Serialise (msg res)) =>
(forall res. msg res -> Decoder RealWorld res) ->
msg res ->
m res
callSerialise mkDecoder msg = call encode mkDecoder msg
with encode
a class member of Serialise
, but this leaves this ugly mkDecoder
to be provided by the user, which would be something boilerplate like
myMkDecoder = \case
Get{} -> decode
Set{} -> decode
where decode
is a class member of Serialise
.
Is there a way, in callSerialise
, to express
- For every possible
msg res
, there should be an instanceSerialise res
, and - Given an
msg res
and the above requirement fulfilled, somehow get the instance (and itsdecode
member) in scope to be used
I looked into the constraints
package with its Dict
, but that doesn’t seem to suit what I’d like to achieve. Any pointers would be appreciated!