简体   繁体   中英

Pattern for constraining a type to another type

I know the title isn't great, so I want to just give a specific example. I have a tree-like structure, modeling a test suite:

data Metadata = Metadata { id :: Id, disabled :: Bool }
data Node =
    Suite { metadata :: Metadata, config :: Config, children :: [Node] }
    Test { metadata :: Metadata, code :: string } 

So basically, we have Suite s, which themselves could own other test Suite s, or Test s. This is really quite similar to a Tree definition (with parent Tree s and terminal Leaf s). So far nothing too crazy, except for I've left out other details from the constructors (I used a hypothetical Config type for the Suite constructor, since the details of that aren't pertinent to this question, except to demonstrate that the two constructors are actually significantly different so you wouldn't want a directly recursive structure Node = Node { stuff :: Stuff, children: Maybe [Node] }

Now, I have a type that relates to this type. Specifically, as the tests are running, I keep track of their respective statuses:

Status =
   Waiting |
   Skipped { dueTo :: Id } |
   Failure { reason :: Reason } |
   Success { duration :: int }

The Test s (and Suite s) could at any given moment be in any one of these states (there are more statuses left out since I do not believe them to be relevant). . I store the states in a hash table of sorts from Id to Status : Table Id Status .

The problem arrises that, while Skipped and Waiting are actually totally fine for both Suite and Test , Failure and Success actually want to store slightly different data depending on for which they are. Without getting into the weeds of the specific information, say we want the Failure for Suites to store the failed child Id 's to avoid repeatedly re-calculating this (in our case there is actual non-generative data stored). One option is to just break it up into two Status es:

   ...
   TestFailure { reason :: Reason } |
   SuiteFailure { failedChildren :: [Id] } |
   ...

This isn't great though because you need something like isFailure = Status -> Bool since there are TWO failures, and because we can't guarantee that TestFailure s will be associated with Test s in our hash table. We can solve the first problem by splitting out the internal info into a separate type:

data FailureInfo =
   TestInfo { reason :: Reason } |
   SuiteInfo { failedChildren :: [Id], otherStuff :: Whatever }

data Status =
   ...
   Failure { info :: FailureInfo }
   ...

This is certainly better, but there is still the issue that I can't guarantee that a Failure { TestInfo } is only associated with a Test . This is the crux of my question: given a type with multiple constructors, how can a variate a supporting type on those constructors in a way that I get the maximum support from the compiler.

If you imagine temporarily that Suite and Test were actually distinct types (instead of merely constructors of one type), I would maybe want a type parameter Status a , and a hash table mapping from a to Status a (but this would also not completely answer the question).

If you have a single hashmap it's going to be difficult to statically guarantee that tests are not mapped to suite-failures or vice versa. The ideas you already have are pretty much what you can do unless you want to have two different ID-types and require each lookup to specify if a test or suite is being queried for status.

If I may suggest a more drastic change: How about getting rid of the hashtable and storing the tests and statuses in the same structure?

Something like:

-- s is suite data, t is test data
data Node s t =
    Suite { metadata :: Metadata, children :: [Node s t], suiteStuff :: s }
    Test { metadata :: Metadata, testStuff :: t } 

Status f =
   Waiting |
   Skipped { dueTo :: Id } |
   Failure { failStuff :: f } |
   Success { duration :: int }

-- Like the old node type, containing test cases
type TestTree = Node Config String

-- A status tree contains IO actions for retrieving the current status of tests/testsuites
type StatusTree = Node (IO (Status [Id])) (IO (Status Reason))

-- Running tests
startRunning :: TestTree -> IO StatusTree

Depending on how you typically traverse the statuses, this could work quite nicely. Of course you can still have a HashMap (or two hashmaps) underneath it all if you want (eg Table Id (Status (Either [ID] Reason)) ) and the IO-actions in the tree are just lookups. Or you could just have an IORef for each suite/test.

Writing or deriving a Functor instance for Node may be very useful.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM