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.