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

luk*_*san 8 f# discriminated-union type-extension

我已经定义了以下歧视联盟:

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
Run Code Online (Sandbox Code Playgroud)

然后我创建了一个漂亮的打印功能,如下所示:

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)
Run Code Online (Sandbox Code Playgroud)

现在我想让我的Expr类型使用此函数作为其ToString()方法.例如:

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
Run Code Online (Sandbox Code Playgroud)

但我不能这样做,因为stringify还没有定义.答案是定义Stringify为成员Expr,但我不想用这种随着时间的推移而不断增长的专门方法污染我的初始类型声明.因此,我决定使用一个抽象方法,我可以在文件中进一步向下实现内部类型扩展.这是我做的:

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
Run Code Online (Sandbox Code Playgroud)

但是我得到以下编译器错误:

错误FS0912:扩充中不允许使用此声明元素

该消息甚至看起来都不正确(我还没有创建类型扩充),但我明白为什么它在抱怨.它不希望我在有区别的联合类型上创建一个抽象成员,因为它不能被继承.即使我真的不想继承,我希望它在C#中表现得像一个部分类,我可以在其他地方完成定义(在本例中是相同的文件).

我最后通过使用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
Run Code Online (Sandbox Code Playgroud)

虽然现在sprintfToString两个输出相同的字符串,并且没有办法获得Add (Con 2,Con 3)输出而不是(2 + 3)我想要它.

那么有什么其他方法可以做我想做的事情吗?

PS我也注意到,如果我将StructuredFormatDisplay属性放在扩充而不是原始类型上,它就不起作用.这种行为对我来说似乎不正确.似乎F#编译器应该将属性添加到类型定义中,或者禁用类型augmentations上的属性.

Dan*_*ian 8

你有没有考虑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
Run Code Online (Sandbox Code Playgroud)

然而,它确实具有丑陋的副作用

warning FS0060: Override implementations in augmentations are now deprecated. Override implementations should be given as part of the initial declaration of a type.
Run Code Online (Sandbox Code Playgroud)

  • 好吧,类型*扩充*在编译时会在同一个类中结束.而类型*扩展*最终作为扩展方法.但是,语法是相同的,所以我可以理解你必须同时执行它.(不一定说,它非常直观) (3认同)

Joh*_*mer 6

一个甚至不需要类型扩展的解决方案怎么样?

取而代之的是,用一个静态成员是字符串化(我们需要的虚拟类型为类型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)
Run Code Online (Sandbox Code Playgroud)


byt*_*ter 6

实际上,stringify 必须与数据类型一起增长,否则最终会出现不完整的模式匹配.对数据类型进行任何必要的修改都需要修改stringify.作为个人意见,我会考虑将两者放在同一个地方,除非项目非常复杂.

但是,由于您希望清楚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))
Run Code Online (Sandbox Code Playgroud)

引用这个答案:

在复杂的程序中,清晰类型签名确实可以更容易地保持可组合性.

不仅可以更简单地向单个案例的DU添加更多案例,而且使用成员和静态方法扩展DU也更容易.