F# how to specify type restriction in recursive discriminated unions

237 views Asked by At

I am trying to define my grammar as a discriminated union. It has two possible types: int and datetime and mathemetical operators of Add and Mul. Add works on int and datetime (as add days in int) Mul works only on int and not on datetime Grammar can be recursive

My grammar looks like

type MyExpression =
|Integer of int
|Date of datetime
|Add of MyExpression * MyExpression
|Mul of MyExpression * MyExpression

I have written a parser (fparsec) that can parse text in my grammar, but I am not sure how to handle the condition that Mul can be recursive but only on Integer.

Is there an option to define this restriction on my MyExpression type or do I have to handle this in my parsed input?

2

There are 2 answers

0
Mark Seemann On BEST ANSWER

The design suggested by Asti would also be my first choice. Depending on your requirements, that may be all you need.

It does, however, also enable you to compile an expression like

Add(Val(System.Console.Out), Val(System.Console.Error))

which is probably not what you want.

Alternatively, you could model expressions like this:

open System

type IntExpression =
| Integer of int
| Mul of IntExpression * IntExpression
| Add of IntExpression * IntExpression

type DateTimeExpression =
| Date of DateTime
| Add of DateTimeExpression * DateTimeExpression

type MyExpression =
| IntExpression of IntExpression
| DateTimeExpression of DateTimeExpression

This is clearly a more verbose type definition, but it does embody the rule that an expression can contain leaf nodes of either integers or DateTime values, and no other values - if that's the rule you want to enforce.

I'm not claiming that this is better; I'm only supplying an alternative.

Usage:

> IntExpression(Mul(IntExpression.Add(Integer(1), Integer(2)),Integer 3));;
val it : MyExpression =
  IntExpression (Mul (Add (Integer 1,Integer 2),Integer 3))
> DateTimeExpression(Add(Date(DateTime.MinValue),Date(DateTime.MinValue)));;
val it : MyExpression =
  DateTimeExpression
    (Add
       (Date 01.01.0001 00:00:00 {Date = 01.01.0001 00:00:00;
                                  Day = 1;
                                  DayOfWeek = Monday;
                                  DayOfYear = 1;
                                  Hour = 0;
                                  Kind = Unspecified;
                                  Millisecond = 0;
                                  Minute = 0;
                                  Month = 1;
                                  Second = 0;
                                  Ticks = 0L;
                                  TimeOfDay = 00:00:00;
                                  Year = 1;},
        Date 01.01.0001 00:00:00 {Date = 01.01.0001 00:00:00;
                                  Day = 1;
                                  DayOfWeek = Monday;
                                  DayOfYear = 1;
                                  Hour = 0;
                                  Kind = Unspecified;
                                  Millisecond = 0;
                                  Minute = 0;
                                  Month = 1;
                                  Second = 0;
                                  Ticks = 0L;
                                  TimeOfDay = 00:00:00;
                                  Year = 1;}))
1
Asti On

If you have type based constraints, it may be easier going for a generic approach:

type MyExpression<'t> =
|Val of 't
|Mul of MyExpression<int> * MyExpression<int>
|Add of MyExpression<'t> * MyExpression<'t>

let Integer (x:int) = Val(x)
let Date (x:DateTime) = Val(x)

Usage:

Mul(Integer(1), Integer(2)) //compiles
Mul(Date(DateTime.Now), Date(DateTime.Now)) //error