Good pattern for parallel variant types

(Sorry for the long question, if I could phrase this shorter I would :slight_smile: )

What’s the pattern to use with let’s call them parallel variant types? E.g. suppose I have three data types Fields, Params and Results, and I want to indicate that the XFields always goes with XParams and XResults, and same for the Ys and Zs?

data Fields = XFields XF | YFields YF | ZFields ZF
  deriving (Eq, Show)

data Params = XParams XP | YParams YP | ZParams ZP
  deriving (Eq, Show)

data Results = XResults XR | YResults YR | ZResults ZR
  deriving (Eq, Show)

I want to write a function

computeSomething: Fields -> Params -> Results 
computeSomething f p = ...

that only makes sense for XFields and XParams yielding an XResult, or Y... or Z... Is there a way to express that elegantly? I could write

computeSomething (XFields xf) (XParams xp) = XResult ...
computeSomething (XFields xf) p = error "Message" 
computeSomething (YFields yf) (YParams yp) = YResult ...
computeSomething (YFields yf) p = error "Message" 

But that doesn’t look aesthetic to me, and I don’t make as many guarantees about the Result as I want.

Here’s some accompanying daml for how I’d use these types:

template Agreement 
  with 
    a: Party 
    b: Party 
    operator: Party
    fields: Fields 
  where 
    signatory a, b, operator

    nonconsuming choice DoSomething: ContractId ResultInstance
    -- I want to guarantee that the `result` in the `ResultInstance` matches the `fields`
      with 
        params: Params -- I want an error if the params don't match the fields
      controller operator 
      do 
        create ResultInstance 
          with 
            result = computeSomething fields params
            ..

This comes up frequently in Daml when we want the same workflow for slightly different types of legal agreements.

Fundamentally, the type of an argument cannot depend on the value of a prior argument-that-is-not-a-type1. So, for example, there is no way to make the choice of data constructor in a prior argument (e.g. XFields in f) determine the type of a following argument (e.g. p).

There is no way to relate the type of params in your choice to the ResultInstance result, because there is nowhere to put a type parameter. For example, with generic templates you would have a three-type-parameter template, passing in XF XP XR to the first instantiation, YF YP YR to the second, and so on. Alas, we don’t have those.

There are some interesting techniques to do this with ordinary Daml functions, though. So you can still define polymorphic functions on the side that relate XF/XP/XR, YF/YP/YR. That combined with other techniques (make a record type for a, b, operator and instance IsParties for it?) will reduce the cost of the “Daml model” way of doing this now, which is to have different templates when different choices are available.

1 [a] -> [a] means forall a. [a] -> [a], so the “second” argument (written the first [a]) depends on the “first” argument-that-is-a-type (the plain a). (This is a function with two arguments, a type and a value, with the first usually inferred.) This type dependency preserves phase separation so it is allowed. Fortunately, you can usually ignore all this :smiley: