Цепочка инфиксных операторов со статически разрешенным параметром типа - PullRequest
3 голосов
/ 04 августа 2020

Я пытаюсь создать инфиксный оператор, чтобы 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'

Можно ли создать такой оператор ?

Edit

Хакерское «решение», похоже, состоит в том, чтобы передавать промежуточные результаты через функцию идентификации всякий раз, когда изменяется тип аргумента:

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

Ответы [ 4 ]

4 голосов
/ 04 августа 2020

Это довольно странно - и я бы сказал, что это ошибка компилятора. Тот факт, что вы можете исправить это, разделив конвейер на отдельные привязки let, заставляет меня думать, что это ошибка. Фактически:

// The following does not work
(StringBuilder() << "A") << 1

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

Я думаю, что компилятор каким-то образом не может понять, что результат снова равен StringBuilder, который может иметь другие Append члены. Очень хакерская версия вашего оператора будет:

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

Это выполняет небезопасное приведение к StringBuilder, так что тип возврата всегда StringBuilder. Это заставляет ваш код работать (и он выбирает правильные Append наложения), но также позволяет вам писать код, который использует Append для вещей, отличных от StringBuilder, и этот код не будет работать во время выполнения.

2 голосов
/ 06 августа 2020

Я, возможно, могу добавить данные к этой загадке, хотя я могу только предполагать, что такое поведение может иметь какое-то отношение к конкретной перегрузке для 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"
2 голосов
/ 05 августа 2020

Я думаю, что есть решение в следующем:

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

при компиляции этого с дополнительным << box " XX " компилятор теряется где-то в процессе, и его компиляция занимает довольно много времени (только при использовании StringBuilder() - не MyStringBuilder()), а также интеллекта и раскраски и т. д. c. кажется, исчезает - по крайней мере, в моей Visual Studio 2019.

  • Сначала я подумал, что это как-то связано со значением box, но, скорее, похоже, как-то связано с числом связанных значений ???
1 голос
/ 04 августа 2020

Работает следующее:

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

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

Я думаю, вам нужно указать c о типе StringBuilder, иначе он выберет только одну из перегрузок.

...