8

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.

luksan
  • 7,305
  • 3
  • 32
  • 36

3 Answers3

8

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.
Daniel Fabian
  • 3,730
  • 2
  • 17
  • 26
  • Yeah, that was what I actually started out with, but I think I thought that message was an error and not a warning. Still, your method may be the most-straightforward, even though it is "deprecated." I'm still curious if anyone else on here knows another way that is as simple but not deprecated. – luksan Aug 03 '13 at 22:36
  • Interestingly, this only works if I execute the entire script at once (by highlighting it in VS and pressing ALT-ENTER). IF I try to execute the type definition and augmentation separately, I get the `error FS0854: Method overrides and interface implementations are not permitted here` because the F# compiler can't override a method on an already-existing type. Maybe that dual behavior is the reason why they deprecated the behavior. – luksan Aug 04 '13 at 16:31
  • 3
    Well, a type *augmentation* ends up in the same class, when compiled. Whereas a type *extension* ends up as an extension method. The syntax is the same, however, so I kind of can understand that you have to execute it at the same time. (Not necessarily saying, it is very intuitive) – Daniel Fabian Aug 05 '13 at 07:25
  • Ah, I'd forgot about that difference. MSDN seems to eschew the term *augmentation* and instead uses *intrinsic extension* versus *optional extension* to distinguish between them: http://msdn.microsoft.com/en-us/library/dd233211.aspx. And then the article slips up further down and says *implicit extension*. It still doesn't say what happens when you execute one in immediate mode (whether it is intrinsic or optional); but I suppose each block of code that you execute is treated like a separate "file," so that would make it *optional* I guess. – luksan Aug 05 '13 at 14:23
  • does this answer actually function, to the rest of .net (C#, etc..), as if it was defined on the type directly? – Maslow Aug 29 '16 at 20:48
  • that is the idea, yes – Daniel Fabian Aug 31 '16 at 09:44
6

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)
John Palmer
  • 24,880
  • 3
  • 45
  • 66
  • Yeah, but that requires that the two types be adjacent in the source file, which is what I was kind of trying to avoid. Still might be better than needing to put it directly on the type though. – luksan Aug 03 '13 at 22:40
6

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.

Community
  • 1
  • 1
bytebuster
  • 7,389
  • 3
  • 38
  • 60
  • 1
    I think I'll accept this even though I may not use it because it's at least a technique I didn't consider. – luksan Aug 06 '13 at 00:40