使用静态解析的类型参数链接中缀运算符

Chaining infix operators with statically resolved type parameter

我正在尝试创建一个中缀运算符,使 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'.

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

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

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

是否可以创建这样的运算符?

编辑

一个骇人听闻的“解决方案”似乎是在参数类型发生变化时通过恒等函数传递中间结果:

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

以下作品:

let inline (<<) (builder:StringBuilder) (value:'T) = builder.Append(value)

let x = StringBuilder()
        << 1
        << 2
        << 3
        << "af"
        << 2.32m
        |> string

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

这很奇怪 - 我想说这可能是一个编译器错误。您可以通过将管道拆分为单独的 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 的不安全转换,因此 return 类型始终是 StringBuilder。这使您的代码工作(并且它选择正确的 Append 覆盖),但它也允许您编写在 non-StringBuilder 事物上使用 Append 的代码,并且此代码将在运行时失败。

我认为有以下解决办法:

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 中的 return 值设置为 ^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

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

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

我可以为这个谜团添加一个数据点,尽管我是 只能推测此行为可能与 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 的各种重载映射到运算符上的静态解析类型参数时发生的。在类似情况下,这似乎是一种相当标准的技术,因为它会为不受支持的类型产生 compile-time 错误。

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"