I'm in the process of implementing the Fibre protocol. The way the protocol works is that you recieve a json object which represents the operations and values you can do on a remote object. Below is an example of such a json.
{
"name": "properties",
"type": "object",
"members": [
{
"name": "foo",
"id": 1,
"type": "uint32",
"access": "rw"
},
{
"name": "bar",
"id": 2,
"type": "uint32[]",
"access": "r"
},
{
"name": "bar",
"type": "object",
"members": [
{
"name": "baz",
"id": 3,
"type": "float",
"access": "rw"
},
{
"name": "some_function",
"id": 4,
"type": "function",
"inputs": [
{
"name": "param1",
"id": 5,
"type": "float",
"access": "rw"
},
{
"name": "param2",
"id": 6,
"type": "bool",
"access": "rw"
}
],
"outputs": [
{
"name": "result",
"id": 7,
"type": "bool",
"access": "rw"
}
]
}
]
}
]
}
As can be seen it's essentially a tree with properties and functions of certain types. Some properties can only be read which is indicated by access being r and some can be read/written indicated by access being rw. Capturing this structure in Haskell is straightforward with some data definition:
data FibreType
= FibreInt8 | FibreUInt8
| FibreInt16 | FibreUInt16
| FibreInt32 | FibreUInt32
| FibreInt64 | FibreUInt64
| FibreFloat
| FibreBool
| FibreJSON
| FibreList FibreType
deriving (Show, Eq, Ord)
data FibreAccess = FibreReadable | FibreReadWriteable
deriving (Show, Eq, Ord)
data FibreObject
= FibreValue
{ _fibreValueName :: String
, _fibreValueEndpoint :: Word16
, _fibreValueType :: FibreType
, _fibreValueAccess :: FibreAccess
}
| FibreFunction
{ _fibreFunctionName :: String
, _fibreFunctionEndpoint :: Word16
, _fibreFunctionArgs :: [FibreType]
, _fibreFunctionResult :: [FibreType]
}
| FibreObject
{ _fibreObjectName :: String
, _fibreObjectMembers :: [FibreObject]
}
deriving (Show, Eq, Ord)
getProperty :: FibreObject -> IO ByteString
getProperty FibreObject{} = error "Cannot get property of FibreObject"
getProperty FibreFunction{} = error "Cannot get property of FibreFunction"
getProperty FibreValue{} = undefined -- Implementation removed for brevity
setProperty :: FibreObject -> ByteString -> IO ByteString
setProperty FibreObject{} = error "Cannot set property of FibreObject"
setProperty FibreFunction{} = error "Cannot set property of FibreFunction"
setProperty FibreValue{_fibreValueAccess = FibreReadWriteable} = undefined -- Implementation removed for brevit
setProperty FibreValue{} = error "Cannot set property of read-only FibreValue"
callFunction :: FibreObject -> [ByteString] -> IO [ByteString]
callFunction FibreObject{} _ = error "Cannot call function for FibreObject"
callFunction FibreValue{} _ = error "Cannot call function for FibreObject"
callFunction FibreFunction{} args = undefined -- Implementation removed for brevit
But I really don't like that once I make the function getProperty or callFunction I lose all type safety and I essentially have to hack around this by making all inputs and outputs ByteString(or I could define a sum type) and then later deserialize to certain concrete values. So I wonder if it's possible to lift this structure to the type level even though that json definition is only available at runtime.
The goal is that I would be able to write something like this:
getProperty :: FibreValue type access -> IO type
getProperty FibreValue{} = undefined -- Implementation removed for brevity
setProperty :: FibreValue type FibreReadWriteable -> IO ()
setProperty FibreValue{} = undefined
callFunction :: FibreFunction [argTys] [resTys] -> argTys -> IO [resTys]
callFunction FibreValueFunction args = undefined -- Implementation removed for brevity
This would give full type safety without having to match on a result type which really could only be a single value. I release there are problems with the function definition I have given for callFunction but it's more to illustrate the point.
Is this possible in Haskell? Any help is appreciated.