F# list of discriminated union sub-types

406 Views Asked by At

I want to present the user with a list of 'FParsec parsers'-plus-'test data' from which they can interactively select and see the results of the parser run on the supplied text.

Specifically, am trying to collect my parser tests in a list of records of this type:

type FUNCTION_TEST = 
    | XELEMENT_MATCH of label : string * func : Parser<(int * string), unit> * data : string
    | XKEYVALUE_MATCH of label : string * func : Parser<(string * string), unit> * data : string

let testable_parsers = [( "xElement", xElement, xElement_text3)
                        ( "xKeyValue", xKeyValue, xKeyValue_text)]

xKeyValue above throws the error...

val xKeyValue: Parser<(string * string), unit>

Full name CustomParser.xKeyValue

FS0001:the type '(int * string)' does not match the type 'string * string'

I want the user to see and choose a label string and see the results of running the parser.

I understand that the parser xElement : Parser<int * string, unit> does not match parser xKeyValue : Parser<(string * string), unit>. Both sub-types are part of the FUNCTION_TEST discriminated union BUT I cannot put the parsers in the same list because their sub-types (XELEMENT_MATCH versus XKEYVALUE_MATCH) disagree.

I wanted to handle this using a match ... with over the discriminated union.

I am new to F# and FParsec and out of elegant ideas. Do I have to hard-code a menu with do! and printfs?

How do experienced F# and FParsec developers allow users to select from a menu of options of different types?

2

There are 2 best solutions below

1
On

I'm a bit confounded by the FUNCTION_TEST type. It looks like you're never using the type or any of its constructors, and if so, why did you define that type in the first place?

After pondering the possible motives you may have had, it looks like perhaps you wanted the testable_parsers list to contain values of the FUNCTION_TEST type so that you can later match on them?

If that is the case, then what you have is not what you intended: the list that you have constructed contains tuples of three values.

In order to construct values of the FUNCTION_TEST type, you need to apply its constructors:

testableParsers = [
    XELEMENT_MATCH ("xElement", xElement, xElement_text3)
    XKEYVALUE_MATCH ("xKeyValue", xKeyValue, xKeyValue_text)
]

But if your only purpose is to present a choice to the user, then I wouldn't even bother with a special type to represent the options. You can just pair labels with functions to be called when the user chooses the label:

testableParsers = [
    "xElement", fun() -> runParser xElement xElement_text3
    "xKeyValue", fun() -> runParser xKeyValue xKeyValue_text
]

This way you don't have to do a match on the values of FUNCTION_TEST either, just call the function.

3
On

I think the best approach would be to restructure your data so that you keep all the common things (label and data) in a record, together with a value representing the function - which can then be a discriminated union with a case for each type:

type Function = 
  | Element of Parser<(int * string), unit>
  | KeyValue of Parser<(string * string), unit>

type Test = 
  { Label : string
    Data : string 
    Function : Function }

Now you can create a list of Test values, which you can easily iterate over to get all the labels & data:

let testableParsers = 
  [ { Label = "xElement"; Function = Element(xElement); Data = xElement_text3 }
    { Label = "xKeyValue"; Function = KeyValue(xKeyValue); Datta = xKeyValue_text } ]

You will still need to use pattern matching when you want to run a specific Function, because this will have to handle the different values you get as a result, but you won't need pattern matching elsewhere.