使用静态解析的类型参数链接中缀运算符
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"
我正在尝试创建一个中缀运算符,使 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"