F# обобщение при перегрузке

Учитывая тип

type T = 
    static member OverloadedMethod(p:int) = ()
    static member OverloadedMethod(p:string) = ()

Предположим, мы хотим создать универсальную функцию, которая разрешает конкретную перегрузку в зависимости от типа параметра. Самый интуитивный способ был бы

//Case 1

let inline call o = T.OverloadedMethod o //error

call 3
call "3"

но это, несмотря на встроенное определение, не работает, и компилятор жалуется

Ошибка FS0041 Уникальная перегрузка для метода 'OverloadedMethod' не может быть определена на основе информации о типе до этой программной точки. Тип аннотации может быть необходимо. Кандидаты: статический член T.OverloadedMethod: p:int -> unit, статический член T.OverloadedMethod: p:string -> unit

Мы можем достичь того, чего хотим, например, используя "операторский трюк"

//Case 2

type T2 =

    static member ($) (_:T2, p:int) = T.OverloadedMethod(p)
    static member ($) (_:T2, p:string) = T.OverloadedMethod(p)

let inline call2 o = Unchecked.defaultof<T2> $ o

call2 3
call2 "3"

Компилятор F# здесь выполняет (по-видимому) еще большую работу и не просто возвращается к разрешению.NET.

Тем не менее, это выглядит некрасиво и подразумевает дублирование кода. Похоже, случай 1 должен быть возможным.

Какие технические причины оправдывают такое поведение? Я предполагаю, что есть некоторый компромисс (возможно, с совместимостью.NET), но не смог найти больше информации.

РЕДАКТИРОВАТЬ

Из постов я извлекаю это как причину:

"вызов trait - это функция компилятора F#, поэтому должно быть два разных способа написания простого вызова и вызова trait. Использование одного и того же синтаксиса для обоих неудобно, потому что это может сбивать с толку, некоторые случаи могут возникнуть, когда простой вызов случайно составлен как "вызов черты".

Давайте поставим вопрос с другой точки зрения:

Глядя на код, действительно кажется простым, что должен делать компилятор:

1) вызов является встроенной функцией, поэтому отложите компиляцию на сайт использования

2) вызов 3 является сайтом использования, где параметр имеет тип int. Но T.OverloadedMethod(int) существует, поэтому давайте сгенерируем его вызов

3) вызовите "3", как в предыдущем случае со строкой вместо int

4) ошибка вызова 3.0, поскольку T.OverloadedMethod(float) не существует

Мне бы очень хотелось увидеть пример кода, где позволить компилятору сделать это было бы проблемой, которая оправдывает требование разработчика написать дополнительный код для вызова функции.

В конце концов, не является ли одна из сильных сторон F# "краткость и интуитивность"?

Здесь мы находимся в случае, когда кажется, что это может быть лучше.

2 ответа

Компромиссы проистекают из того факта, что это полностью стертый трюк с компилятором. Это означает, что:

  1. Код C# не может видеть (и использовать) любой call2 функция, он может видеть только ваши два $ перегрузки метода.
  2. Вся информация о call2 отсутствует во время выполнения, то есть вы не можете вызвать его, например, через отражение.
  3. Это не будет отображаться в стеках вызовов. Это может быть хорошо или плохо - например, в последних версиях компилятора F# они выборочно встраивали определенные функции, чтобы сделать async трассировка стека немного приятнее.
  4. F# запекается на сайте вызова. Если ваш код вызова в это сборка А call2 происходит из сборки B, вы не можете просто заменить сборку B новой версией call2; Вы должны перекомпилировать A против новой сборки. Это может потенциально быть проблемой обратной совместимости.

Довольно интересным преимуществом является то, что оно может привести к значительному повышению производительности в специализированных случаях: почему этот код F# такой медленный?, С другой стороны, я уверен, что существуют обстоятельства, при которых это может нанести активный вред или просто раздувать полученный IL-код.

Причина такого поведения в том, что T.OverloadedMethod o в

let inline call o = T.OverloadedMethod o

это не черт вызова. Это довольно простая перегрузка.NET, которая должна быть решена на сайте вызова, но поскольку тип вашей функции не подразумевает, какую перегрузку для ее решения просто не удается скомпилировать, эта функциональность желательна.

Если вы хотите "отсрочить" разрешение перегрузки, вам нужно выполнить trait-вызов, сделать встроенную функцию необходимой, но недостаточной:

let inline call (x:'U) : unit =
    let inline call (_: ^T, x: ^I) = ((^T or ^I) : (static member OverloadedMethod: _ -> _) x)
    call (Unchecked.defaultof<T>, x)

Использование оператора избавляет вас от множества нажатий клавиш, автоматически выводя эти ограничения, но, как вы можете видеть в своем вопросе, оно требует включения фиктивного параметра в перегрузки.

Другие вопросы по тегам