【问题标题】:Type extension for discriminated union in F#F# 中可区分联合的类型扩展
【发布时间】:2013-08-04 20:46:46
【问题描述】:

我已经定义了以下可区分的联合:

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)

现在我想让我的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

但我不能这样做,因为stringify 尚未定义。答案是将Stringify 定义为Expr 的成员,但我不想用这种随着时间不断增长的专用方法污染我的初始类型声明。因此,我决定使用一个抽象方法,我可以在文件中进一步使用intrinsic 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
    override this.ToString() = this.Stringify()
    abstract member Stringify : unit -> string

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

错误 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

虽然现在sprintfToString 都输出相同的字符串,但如果我想要的话,没有办法获得Add (Con 2,Con 3) 输出而不是(2 + 3)

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

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

【问题讨论】:

    标签: f# discriminated-union type-extension


    【解决方案1】:

    事实上,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))
    

    来自this answer的引用:

    在复杂的程序中,清晰的类型签名确实更容易维护可组合性。

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

    【讨论】:

    • 我想我会接受这个,即使我可能不会使用它,因为它至少是我没有考虑过的一种技术。
    【解决方案2】:

    您是否考虑在扩充中定义您的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
    

    但是,它确实具有a的丑陋副作用

    warning FS0060: Override implementations in augmentations are now deprecated. Override implementations should be given as part of the initial declaration of a type.
    

    【讨论】:

    • 是的,这就是我真正开始的方式,但我认为我认为该消息是错误而不是警告。尽管如此,您的方法可能是最直接的,即使它已“弃用”。我仍然很好奇这里的其他人是否知道另一种简单但不被弃用的方法。
    • 有趣的是,这仅在我一次执行整个脚本时才有效(通过在 VS 中突出显示它并按 ALT-ENTER)。如果我尝试分别执行类型定义和扩充,我会得到error FS0854: Method overrides and interface implementations are not permitted here,因为 F# 编译器无法覆盖现有类型的方法。也许这种双重行为是他们弃用这种行为的原因。
    • 嗯,类型 augmentation 在编译时会在同一个类中结束。而类型 extension 最终成为扩展方法。但是,语法是相同的,所以我可以理解您必须同时执行它。 (不一定说,很直观)
    • 啊,我忘记了这个区别。 MSDN 似乎避开了 augmentation 一词,而是使用 intrinsic extensionoptional extension 来区分它们:msdn.microsoft.com/en-us/library/dd233211.aspx。然后文章再往下滑,说隐式扩展。它仍然没有说明当您以立即模式执行时会发生什么(无论是内在的还是可选的);但我想您执行的每个代码块都被视为一个单独的“文件”,所以我猜这会使它可选
    • 这个答案对 .net 的其余部分(C# 等)是否真的起作用,就好像它是直接在类型上定义的一样?
    【解决方案3】:

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

    相反,使用 stringify 的静态成员定义一个类型(我们需要虚拟类型,因为 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)
    

    【讨论】:

    • 是的,但这要求这两种类型在源文件中是相邻的,这是我试图避免的。仍然可能比需要直接放在类型上更好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-22
    • 2011-11-19
    • 1970-01-01
    相关资源
    最近更新 更多