简体   繁体   English

F#中区分联合的类型扩展

[英]Type extension for discriminated union in F#

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. 现在我想让我的Expr类型将此函数用于其ToString()方法。 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. 但我不能这样做,因为stringify尚未定义。 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. 答案是将Stringify定义为Expr的成员,但我不想用这种随着时间的推移而不断增长的专门方法污染我的初始类型声明。 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 错误FS0912:扩充中不允许使用此声明元素

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). 即使我真的不想继承,我希望它在C#中表现得像一个部分类,我可以在其他地方完成定义(在这种情况下是相同的文件)。

I ended up "cheating" by using the late-binding power of the StructuredFormatDisplay attribute along with sprintf : 我最后通过使用StructuredFormatDisplay属性的后期绑定功能以及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. 虽然现在sprintfToString都输出相同的字符串,但如果我需要它,则无法获得Add (Con 2,Con 3)输出而不是(2 + 3)

So is there any other way to do what I'm trying to do? 那么有什么其他方法可以做我想做的事情吗?

PS I also noticed that if I place the StructuredFormatDisplay attribute on the augmentation instead of the original type, it doesn't work. PS我还注意到,如果我将StructuredFormatDisplay属性放在扩充而不是原始类型上,它就不起作用。 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. 似乎F#编译器应该将属性添加到类型定义中,或者禁用类型augmentations上的属性。

Did you consider defining your ToString in the augmentation? 你有没有考虑在扩充中定义你的ToString

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.

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 相反,定义一个带有staticify的静态成员的类型(我们需要将虚拟类型作为type a ... and b要求b为类型

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)

In fact, stringify must grow along with the data type, otherwise it would end up with an incomplete pattern match. 实际上, stringify 必须随着数据类型一起增长,否则最终会出现不完整的模式匹配。 Any essential modification of the data type would require modifying the stringify as well. 对数据类型进行任何必要的修改都需要修改stringify 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: 但是,由于您希望清楚DU类型,请考虑将数据类型包装到单个案例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. 不仅可以更简单地向单个案例的DU添加更多案例,而且使用成员和静态方法扩展DU也更容易。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM