Type extension for discriminated union in F #

I have identified the following discriminatory union:

type Expr = | Con of Num | Var of Name | Add of Expr * Expr | Sub of Expr * Expr | Mult of Expr * Expr | Div of Expr * Expr | Pow of Expr * Expr 

Then I created a pretty printed function as follows:

 let rec stringify expr = match expr with | Con(x) -> string x | Var(x) -> string x | Add(x, y) -> sprintf "(%s + %s)" (stringify x) (stringify y) | Sub(x, y) -> sprintf "(%s - %s)" (stringify x) (stringify y) | Mult(x, y) -> sprintf "(%s * %s)" (stringify x) (stringify y) | Div(x, y) -> sprintf "(%s / %s)" (stringify x) (stringify y) | Pow(x, y) -> sprintf "(%s ** %s)" (stringify x) (stringify y) 

Now I want my Expr type Expr use this function for its ToString() method. For example:

 type Expr = | Con of Num | Var of Name | Add of Expr * Expr | Sub of Expr * Expr | Mult of Expr * Expr | Div of Expr * Expr | Pow of Expr * Expr override this.ToString() = stringify this 

But I can not do this because stringify is not defined yet. The answer is to define stringify as a member of Expr , but I don't want to pollute my initial type declaration with this specialized method, which will grow over time. Therefore, I decided to use an abstract method, which I could implement with the extension of the internal type further in the file. Here is what I did:

 type Expr = | Con of Num | Var of Name | Add of Expr * Expr | Sub of Expr * Expr | Mult of Expr * Expr | Div of Expr * Expr | Pow of Expr * Expr override this.ToString() = this.Stringify() abstract member Stringify : unit -> string 

But I get the following compiler error:

error FS0912: This ad element is not allowed in Increase

The message does not even seem to be correct (I am not creating a type extension yet), but I understand why it complains. He doesn’t want me to create an abstract element on the discriminatory union type, because it cannot be inherited. Although I really don't want inheritance, I want it to behave like a partial class in C #, where I can finish defining it somewhere else (in this case the same file).

I ended up cheating using the late binding power of the StructuredFormatDisplay attribute along with sprintf :

 [<StructuredFormatDisplay("{DisplayValue}")>] type Expr = | Con of Num | Var of Name | Add of Expr * Expr | Sub of Expr * Expr | Mult of Expr * Expr | Div of Expr * Expr | Pow of Expr * Expr override this.ToString() = sprintf "%A" this /* stringify function goes here */ type Expr with member public this.DisplayValue = stringify this 

Although now sprintf and ToString output the same line, there is no way to get the output of Add (Con 2,Con 3) unlike (2 + 3) if I want to.

So is there any other way to do what I'm trying to do?

PS I also noticed that if I put the StructuredFormatDisplay attribute in addition to the original type, this will not work. This behavior does not seem right to me. It seems that either the F # compiler should add an attribute to the type definition, or prohibit the attributes when expanding the type.

+8
f # discriminated-union
source share
3 answers

In fact, stringify must grow with the data type, otherwise it would end the incomplete pattern matching. Any significant modification to the data type will require modification to stringify . As a personal opinion, I would think about keeping both in the same place if the project is not really complex.

However, since you prefer your DU type to be clear, consider moving the data type to a single-bit DU:

 // precede this with your definitions of Expr and stringify type ExprWrapper = InnerExpr of Expr with static member Make (x: Expr) = InnerExpr x override this.ToString() = match this with | InnerExpr x -> stringify x // usage let x01 = Add(Con 5, Con 42) |> ExprWrapper.Make printfn "%O" x01 // outputs: (5 + 42) printfn "%s" (x01.ToString()) // outputs: (5 + 42) printfn "%A" x01 // outputs: InnerExpr(Add (Con 5,Con 42)) 

Link from this answer :

In complex programs, clear type signatures really make it easier to maintain compositional ability.

Not only to simplify the addition of more cases to single DUs, but also to simplify the expansion of DUs using elements and static methods.

+5
source share

Have you considered the definition of your ToString in an add-on?

 type Num = int type Name = string type Expr = | Con of Num | Var of Name | Add of Expr * Expr | Sub of Expr * Expr | Mult of Expr * Expr | Div of Expr * Expr | Pow of Expr * Expr let rec stringify expr = match expr with | Con(x) -> string x | Var(x) -> string x | Add(x, y) -> sprintf "(%s + %s)" (stringify x) (stringify y) | Sub(x, y) -> sprintf "(%s - %s)" (stringify x) (stringify y) | Mult(x, y) -> sprintf "(%s * %s)" (stringify x) (stringify y) | Div(x, y) -> sprintf "(%s / %s)" (stringify x) (stringify y) | Pow(x, y) -> sprintf "(%s ** %s)" (stringify x) (stringify y) type Expr with override this.ToString() = stringify this 

However, it has an ugly side effect

 warning FS0060: Override implementations in augmentations are now deprecated. Override implementations should be given as part of the initial declaration of a type. 
+8
source share

How about a solution that doesn't even require type expansion.

Instead, define a type with a static member that is a string member (we need a dummy type because type a ... and b requires b be a type

 type Num = string //missing type Name = string //missing type Expr = | Con of Num | Var of Name | Add of Expr * Expr | Sub of Expr * Expr | Mult of Expr * Expr | Div of Expr * Expr | Pow of Expr * Expr override this.ToString() = type_dummy.stringify this and type_dummy = static member stringify expr = let stringify = type_dummy.stringify match expr with | Con(x) -> string x | Var(x) -> string x | Add(x, y) -> sprintf "(%s + %s)" (stringify x) (stringify y) | Sub(x, y) -> sprintf "(%s - %s)" (stringify x) (stringify y) | Mult(x, y) -> sprintf "(%s * %s)" (stringify x) (stringify y) | Div(x, y) -> sprintf "(%s / %s)" (stringify x) (stringify y) | Pow(x, y) -> sprintf "(%s ** %s)" (stringify x) (stringify y) 
+6
source share

All Articles