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.
Did you consider defining your ToString
in the augmentation?
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 does have the ugly side-effect of a
warning FS0060: Override implementations in augmentations are now deprecated. Override implementations should be given as part of the initial declaration of a type.
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.
How about a solution that doesn't even require a type extension.
Instead, define a type with a static member which is stringify (we need the dummy type as type a ... and b
requires b
to 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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With