【问题标题】:Chaining infix operators with statically resolved type parameter使用静态解析的类型参数链接中缀运算符
【发布时间】:2020-08-04 06:05:04
【问题描述】:

我正在尝试创建一个中缀运算符以使 System.Text.StringBuilder 更易于使用。

我有以下使用静态解析类型参数的内联函数:

let inline append value builder = (^T : (member Append : _ -> ^T) (builder, value))

它处理StringBuilder.Append 的所有重载。这可以作为常规功能正常工作:

StringBuilder()
|> append 1
|> append " hello "
|> append 2m
|> string
// Result is: '1 hello 2'

当我尝试使用这样定义中缀运算符时:

let inline (<<) builder value = append value builder

当链中的所有参数都属于同一类型时,它可以工作:

StringBuilder()
<< 1
<< 2
<< 3
|> string
// Result is: '123'

但使用不同类型的参数失败:

StringBuilder()
<< 1
<< "2"  // <- Syntax error, expected type 'int' but got 'string'.
<< 123m // <- Syntax error, expected type 'int' but got 'decimal'.

预期的类型似乎是通过链中&lt;&lt; 运算符的第一次使用推断出来的。我假设每个&lt;&lt; 都将单独应用。

如果链被分成单独的步骤,编译器会再次高兴:

let b0 = StringBuilder()
let b1 = b0 << 1
let b2 = b1 << "2"
let b3 = b2 << 123m
b3 |> string
// Result is: '12123'

有没有可能创建这样的算子?

编辑

一个 hacky 的“解决方案”似乎是只要参数的类型发生变化,就通过 identity 函数传递中间结果:

StringBuilder()
<< 1    // No piping needed here due to same type (int)
<< 2    |> id
<< "A"  |> id
<< 123m
|> string
// Result is: '12A123'

【问题讨论】:

    标签: f# inline


    【解决方案1】:

    这很奇怪——我会说这可能是一个编译器错误。您可以通过将管道拆分为单独的 let 绑定来解决此问题,这让我认为这是一个错误。事实上:

    // The following does not work
    (StringBuilder() << "A") << 1
    
    // But the following does work
    (let x = StringBuilder() << "A" in x) << 1
    

    我认为编译器无法确定结果再次只是StringBuilder,它可以有其他Append 成员。您的运营商的一个非常 hacky 版本将是:

    let inline (<<) builder value = 
      append value builder |> unbox<StringBuilder>
    

    这会对StringBuilder 执行不安全的强制转换,因此返回类型始终为StringBuilder。这使您的代码工作(并选择正确的Append 重叠),但它还允许您编写在非StringBuilder 事物上使用Append 的代码,并且此代码将在运行时失败。

    【讨论】:

    • 通过id 管道每个中间结果也可以:StringBuilder() &lt;&lt; 1 |&gt; id &lt;&lt; "A" |&gt; id &lt;&lt; 'B' |&gt; id &lt;&lt; 123m 并避免不安全的演员表。虽然会产生一些看起来很有趣的代码。
    【解决方案2】:

    我可能会为这个谜团添加一个数据点,尽管我是 只能推测这种行为可能与value: obj 的特定过载有关。如果我取消注释该行并尝试运行它,编译器会说:

    Script1.fsx(21,14): error FS0001: Type mismatch. Expecting a
        'a -> 'c    
    but given a
        System.Text.StringBuilder -> System.Text.StringBuilder    
    The type ''a' does not match the type 'System.Text.StringBuilder'
    

    这发生在尝试将System.Text.StringBuilder 的各种重载映射到运算符上的静态解析类型参数时。在类似情况下,这似乎是一种相当标准的技术,因为它会为不受支持的类型产生编译时错误。

    open System.Text
    type Foo = Foo with
        static member ($) (Foo, x : bool)    = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : byte)    = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : char[])  = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : char)    = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : decimal) = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : float)   = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : float32) = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : int16)   = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : int32)   = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : int64)   = fun (b : StringBuilder) -> b.Append x
        // static member ($) (Foo, x : obj)     = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : sbyte)   = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : string)  = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : uint16)  = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : uint32)  = fun (b : StringBuilder) -> b.Append x
        static member ($) (Foo, x : uint64)  = fun (b : StringBuilder) -> b.Append x
    
    let inline (<.<) b a =
        (Foo $ a) b
    // val inline ( <.< ) :
    //   b:'a -> a: ^b -> 'c
    //     when (Foo or  ^b) : (static member ( $ ) : Foo *  ^b -> 'a -> 'c)
    
    let res =
        StringBuilder()
        <.< 1
        <.< 2
        <.< 3
        <.< "af"
        <.< 2.32m
        |> string
    // val res : string = "123af2,32"
    

    【讨论】:

    • inline 说明符添加到静态成员可以避免最终结果中的闭包,但除此之外,它还可以完美运行。但是,不应添加 obj 重载,因为这会导致编译器为每个参数选择 StringBuilder.Append(obj)
    【解决方案3】:

    我认为有以下解决方案:

    let inline append value builder = (^T: (member Append: _ -> ^S) (builder, value))
    let inline (<<) builder value = append value builder
    
    let builder = new StringBuilder()
    let result = 
        builder
        << 1
        << " hello "
        << 2m
        |> string
    printfn "%s" result
    

    正如所见,Append 的返回值设置为^S 而不是^T,并且^S 被解析为要求Append 作为成员。

    它会为Append 找到正确的重载,您可以看到,如果您使用StringBuilder 的以下模型:

    type MyStringBuilder() =
        member this.Append(value: int) =
            printfn "int: %d" value;
            this
        member this.Append(value: string) =
            printfn "string: %s" value;
            this
        member this.Append(value: decimal) =
            printfn "decimal: %f" value;
            this
        member this.Append(value: obj) =
            printfn "obj: %A" value
            this
    
    let builder = new MyStringBuilder()
    let result = 
        builder
        << 1
        << " hello "
        << 2m
        |> string
    

    警告:以下设置有一个特殊性:

    let builder = StringBuilder()
    let result = 
        builder
        << 1
        << " hello "
        << 2m
        << box " XX "
        |> string
    

    当用额外的&lt;&lt; box " XX " 编译它时,编译器会在过程中的某个地方丢失并且编译时间相当长(仅在使用StringBuilder() - 不是MyStringBuilder() 时)并且智能感知和着色等似乎消失了- 至少在我的 Visual Studio 2019 中。

    • 起初,我认为它与box 值有关,但它似乎与链值的数量有关???

    【讨论】:

    • 虽然这确实有效(将 ^T 替换为 ^S),但在使用超过 4 个参数的链时会导致编译器停止。
    • @peder86:是的 - 我知道(请参阅我的警告),但我无法解释原因 - 事实上,它使 Visual Studio 或多或少地崩溃并且对输入无响应。希望一些比较有经验的F#-guru能给出一个解释。也许我应该将我的答案作为一个新问题发布?对我来说,这真的很奇怪。
    【解决方案4】:

    以下工作:

    let inline (<<) (builder:StringBuilder) (value:'T) = builder.Append(value)
    
    let x = StringBuilder()
            << 1
            << 2
            << 3
            << "af"
            << 2.32m
            |> string
    

    我认为您需要具体说明 StringBuilder 类型,否则它只会选择其中一个重载。

    【讨论】:

    • 编译器现在选择了重载StringBuilder.Append(value: obj)
    猜你喜欢
    • 1970-01-01
    • 2017-04-13
    • 2020-01-14
    • 2016-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多