I am trying to create complex data structures with composite logic. That is, the data structure has a common format (essentially, a record with some fields, the type of which can be changed) and some common functions. Concrete structures have a specific implementation of common functions.
There are two approaches that I have tried. One of them is to use a type system (with class types, type families, functional dependencies, etc.). Another creates my own “virtual table” and uses GADT. Both methods do not work in the same way - it seems that there is something basic that I'm missing here. Or maybe there is an even better way for Haskell-ish to do this?
Here is the unsuccessful "typed" code:
{-
This results in the following error:
Typed.hs:39:7: Could not deduce (block ~ Block state1 ports1) from the context (LogicBlock block incoming outgoing) bound by the class declaration for `LogicBlock' at Typed.hs:(27,1)-(41,19) `block' is a rigid type variable bound by the class declaration for `LogicBlock' at Typed.hs:26:18 Expected type: StateT block Data.Functor.Identity.Identity outgoing Actual type: State (Block state1 ports1) outgoing In the return type of a call of `convert' In a stmt of a 'do' block: convert inputMessage
And here is the failed "vtable" code:
{-
This results in the following error:
VTable.hs:44:5: Could not deduce (block1 ~ Block state1 ports1) from the context (block ~ Block state ports) bound by the type signature for runLogic :: block ~ Block state ports => BlockLogic block incoming outgoing -> State block outgoing at VTable.hs:(37,1)-(46,17) or from (block ~ Block state1 ports1) bound by a pattern with constructor BlockLogic :: forall incoming outgoing state ports block. Lens state LogicState -> Lens ports (LogicPorts incoming outgoing) -> (incoming -> State block outgoing) -> BlockLogic (Block state ports) incoming outgoing, in an equation for `runLogic' at VTable.hs:37:10-26 `block1' is a rigid type variable bound by a pattern with constructor BlockLogic :: forall incoming outgoing state ports block. Lens state LogicState -> Lens ports (LogicPorts incoming outgoing) -> (incoming -> State block outgoing) -> BlockLogic (Block state ports) incoming outgoing, in an equation for `runLogic' at VTable.hs:37:10 Expected type: block1 Actual type: block Expected type: StateT block1 Data.Functor.Identity.Identity outgoing Actual type: State block outgoing In the return type of a call of `convert' In a stmt of a 'do' block: convert inputMessage
I don’t understand why the GHC is going for "block1" when all of this is explicitly specified under ScopedTypeVariables and "forall block".
Edit # 1: Functional dependencies added, thanks to Chris Kuklevich to point this out. The problem remains, though.
Edit # 2: As Chris noted, in VTable's solution, getting rid of all the “block-state blocks of the block” and instead writing “Block port states” solves the problem.
Edit # 3: Good, so the problem is that for each individual function, the GHC requires enough type information in the parameters to output all types, even for types that are not used at all. Thus, in the case of (for example) logicState above, the parameters give us only a state that is not enough to know what the ports are, incoming and outgoing types. It doesn’t matter for the logicState function; GHC wants to know and cannot, so compilation fails. If this is really the main reason, it would be better if the GHC complained directly when compiling the StateState logical statement - it seems to have enough information to detect a problem there; if I saw a problem saying "port type is not used / not defined" in this place, it would be much clearer.
Edit # 4: it's still not clear to me why (block ~ Block state ports) doesn't work; I guess I'm using it for an unintended purpose? Looks like it should work. I agree with Chris that using CPP to work around is an abomination; but writing "B trpe" (in my real code, which has more paranons) is also not a good solution.