Reputation: 35
I'm trying to create an infix operator to make System.Text.StringBuilder
slightly easier to use.
I have the following inline function using statically resolved type parameters:
let inline append value builder = (^T : (member Append : _ -> ^T) (builder, value))
which handles all the overloads of StringBuilder.Append
. This works fine as a regular function:
StringBuilder()
|> append 1
|> append " hello "
|> append 2m
|> string
// Result is: '1 hello 2'
When I try to use define an infix operator like so:
let inline (<<) builder value = append value builder
it works when all parameters in a chain are of the same type:
StringBuilder()
<< 1
<< 2
<< 3
|> string
// Result is: '123'
but fails with parameters of different types:
StringBuilder()
<< 1
<< "2" // <- Syntax error, expected type 'int' but got 'string'.
<< 123m // <- Syntax error, expected type 'int' but got 'decimal'.
The expected type seems to be inferred by the first usage of the <<
operator in the chain. I would assume that each <<
would be applied separately.
If the chain is split into separate steps the compiler is happy again:
let b0 = StringBuilder()
let b1 = b0 << 1
let b2 = b1 << "2"
let b3 = b2 << 123m
b3 |> string
// Result is: '12123'
Is it possible to create such an operator?
A hacky "solution" seems to be to pipe intermediate results through the identity function whenever the type of the argument changes:
StringBuilder()
<< 1 // No piping needed here due to same type (int)
<< 2 |> id
<< "A" |> id
<< 123m
|> string
// Result is: '12A123'
Upvotes: 2
Views: 145
Reputation: 5741
I can possibly add a data point to this mystery, albeit I am
only able to surmise that this behavior might have something to do with the particular overload for value: obj
. If I uncomment that line and try to run it, the compiler says:
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'
This happened while trying to map the various overloads of System.Text.StringBuilder
to statically resolved type parameters on an operator. This seems to be a fairly standard technique in similar cases, since it will produce compile-time errors for unsupported types.
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"
Upvotes: 2
Reputation:
I think there is a solution in the following:
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
As seen the return value from Append
is set to ^S
instead of ^T
and ^S
is resolved to require Append
as member.
It will find the correct overload for Append
which you can see, it you use the following mockup of a 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
Warning: There is though a peculiarity in the following setup:
let builder = StringBuilder()
let result =
builder
<< 1
<< " hello "
<< 2m
<< box " XX "
|> string
when compiling this with the extra << box " XX "
the compiler gets lost somewhere in the process and is rather long time to compile (only when using StringBuilder()
- not MyStringBuilder()
) and the intellisense and coloring etc. seems to disappear - in my Visual Studio 2019 as least.
box
value, but it rather seems to have something to do with the number of chained values???Upvotes: 2
Reputation: 243051
This is quite odd - and I would say it may be a compiler bug. The fact that you can fix this by splitting the pipeline into separate let
bindings is what makes me think this is a bug. In fact:
// The following does not work
(StringBuilder() << "A") << 1
// But the following does work
(let x = StringBuilder() << "A" in x) << 1
I think the compiler is somehow not able to figure out that the result is again just StringBuilder
, which can have other Append
members. A very hacky version of your operator would be:
let inline (<<) builder value =
append value builder |> unbox<StringBuilder>
This performs an unsafe cast to StringBuilder
so that the return type is always StringBuilder
. This makes your code work (and it chooses the right Append
overlaods), but it also lets you write code that uses Append
on non-StringBuilder things and this code will fail at runtime.
Upvotes: 3
Reputation: 2436
The below works:
let inline (<<) (builder:StringBuilder) (value:'T) = builder.Append(value)
let x = StringBuilder()
<< 1
<< 2
<< 3
<< "af"
<< 2.32m
|> string
I think you need to be specific about the StringBuilder
type otherwise it will pick only one of the overloads.
Upvotes: 1