Type extension for discriminated union in F#

discriminated-union, f#, type-extension

Solution

In fact, `stringify` must grow along with the data type, otherwise it would end up with an incomplete pattern match. Any essential modification of the data type would require modifying the `stringify` as well. As a personal opinion, I would consider keeping both at the same place, unless the project is really complex.

However, since you prefer your DU type to be clear, consider wrapping the data type into a single-case 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))

Citation from this answer:

In complex programs clear type signatures indeed make it easier to maintain composability.

Not only it's simpler to add more cases to single-case DUs, but also it's easier to extend DUs with member and static methods.

Problem

I have defined the following discriminated 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-printing 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 to make my `Expr` type 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't do this, because `stringify` is not yet defined. 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 that is going to keep growing over time. Therefore, I decided to use an abstract method that I could implement with an intrinsic type extension further down in the file. Here's 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 declaration element is not permitted in an augmentation The message doesn't even seem correct (I'm not creating a type augmentation yet), but I understand why it's complaining. It doesn't want me to create an abstract member on a discriminated union type because it cannot be inherited. Even though I don't really 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" by 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` both output the same string, and there is no way to get the `Add (Con 2,Con 3)` output as opposed to `(2 + 3)` if I want it. So is there any other way to do what I'm trying to do? P.S. I also noticed that if I place the `StructuredFormatDisplay` attribute on the augmentation instead of the original type, it doesn't work. This behavior doesn't seem correct to me. It seems that either the F# compiler should add the attribute to the type definition or disallow attributes on type augmentations.

Original source