8
votes

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.

3

3 Answers

6
votes

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.

8
votes

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.
6
votes

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)