Как F # компилирует функции, которые могут принимать несколько различных типов параметров в IL? - PullRequest
5 голосов
/ 21 сентября 2010

Я практически ничего не знаю о F #. Я даже не знаю синтаксис, поэтому не могу привести примеры.

В ветке комментариев упоминалось, что F # может объявлять функции, которые могут принимать параметры нескольких возможных типов, например, строку или целое число. Это было бы похоже на перегрузки метода в C #:

public void Method(string str) { /* ... */ }
public void Method(int integer) { /* ... */ }

Однако в CIL вы не можете объявить делегат этой формы. Каждый делегат должен иметь один, определенный список типов параметров. Однако, поскольку функции в F # являются гражданами первого класса, может показаться, что вы должны иметь возможность передавать такую ​​функцию, и единственный способ скомпилировать ее в CIL - это использовать делегаты.

Так как же F # компилирует это в CIL?

Ответы [ 4 ]

11 голосов
/ 21 сентября 2010

Этот вопрос немного двусмысленный, поэтому я просто расскажу о том, что верно для F #.

В F # методы могут быть перегружены, как C #.Методы всегда доступны по квалифицированному имени в форме someObj.MethodName или someType.MethodName.Должен быть контекст, который может статически разрешать перегрузку во время компиляции, как в C #.Примеры:

type T() =
    member this.M(x:int) = ()
    member this.M(x:string) = ()
let t = new T()
// these are all ok, just like C#
t.M(3)
t.M("foo")
let f : int -> unit = t.M
let g : string-> unit = t.M
// this fails, just like C#
let h = t.M // A unique overload for method 'M' could not be determined 
            // based on type information prior to this program point.

В F # значения функций с привязкой не могут быть перегружены.Итак:

let foo(x:int) = ()
let foo(x:string) = ()  // Duplicate definition of value 'foo'

Это означает, что у вас никогда не будет «неквалифицированного» идентификатора foo, который имеет перегруженное значение.Каждое такое имя имеет единственный недвусмысленный тип.

Наконец, сумасшедший случай, который, вероятно, и вызывает вопрос.F # может определять inline функции, которые имеют «статические ограничения членов», которые могут быть связаны, например, со «всеми типами T, которые имеют свойство члена с именем Bar» или что-то еще.Этот вид обобщенности не может быть закодирован в CIL.Вот почему функции, использующие эту функцию, должны быть inline, чтобы на каждом сайте вызовов код, специфичный для типа используемого на месте вызова, генерировался встроенным.

let inline crazy(x) = x.Qux(3) // elided: type syntax to constrain x to 
                               // require a Qux member that can take an int
// suppose unrelated types U and V have such a Qux method
let u = new U()
crazy(u) // is expanded here into "u.Qux(3)" and then compiled
let v = new V()
crazy(v) // is expanded here into "v.Qux(3)" and then compiled

Таким образом, все это обрабатывается компилятором, и к тому времени, когда нам нужно сгенерировать код, мы снова статически решили, какой конкретный тип мы используем на этом месте вызова.«Тип» crazy не является типом, который может быть выражен в CIL, система типов F # просто проверяет каждый вызов, чтобы убедиться, что соблюдаются необходимые условия, и вставляет код в этот сайт, очень похоже на работу шаблонов C ++.

(Основное назначение / обоснование сумасшедших вещей - перегруженные математические операторы. Без функции inline оператор +, например, являющийся типом функции с привязкой по буквам, может либо «только»работать на int s "или" работать только на float s "или еще много чего. Некоторые разновидности ML (F # является родственником OCaml) делают именно это, где, например, оператор + работает только на int s,и отдельный оператор, обычно называемый +., работает на float s, тогда как в F # + - это функция inline, определенная в библиотеке F #, которая работает с любым типом с оператором + или любым другимиз простых числовых типов. Встраивание может также иметь некоторые потенциальные преимущества производительности во время выполнения, что также привлекательно для некоторых математических / вычислительных областей.)

6 голосов
/ 22 сентября 2010

Когда вы пишете на C #, и вам нужна функция, которая может принимать несколько различных наборов параметров, вы просто создаете перегрузки методов:

string f(int x)
{
    return "int " + x;
}
string f(string x)
{
    return "string " + x;
}
void callF()
{
    Console.WriteLine(f(12));
    Console.WriteLine(f("12"));
}
// there's no way to write a function like this:
void call(Func<int|string, string> func)
{
    Console.WriteLine(func(12));
    Console.WriteLine(func("12"));
}

Функция callF тривиальна, но моя выдумкаСинтаксис для функции call не работает.

Когда вы пишете F # и вам нужна функция, которая может принимать несколько различных наборов параметров, вы создаете различающееся объединение, которое может содержать все различные наборы параметров.и вы создаете одну функцию, которая принимает это объединение:

type Either = Int of int
            | String of string
let f = function Int x -> "int " + string x
               | String x -> "string " + x

let callF =
    printfn "%s" (f (Int 12))
    printfn "%s" (f (String "12"))

let call func =
    printfn "%s" (func (Int 12))
    printfn "%s" (func (String "12"))

Будучи единственной функцией, f может использоваться как любое другое значение, поэтому в F # мы можем написать callF и call f,и оба делают одно и то же.

Так как же F # реализует тип Either, который я создал выше?По сути так:

public abstract class Either
{
    public class Int : Test.Either
    {
        internal readonly int item;
        internal Int(int item);
        public int Item { get; }
    }
    public class String : Test.Either
    {
        internal readonly string item;
        internal String(string item);
        public string Item { get; }
    }
}

Сигнатура функции call:

public static void call(FSharpFunc<Either, string> f);

И f выглядит примерно так:

public static string f(Either _arg1)
{
    if (_arg1 is Either.Int)
        return "int " + ((Either.Int)_arg1).Item;
    return "string " + ((Either.String)_arg1).Item;
}

Конечно, вы могли бы реализовать тот же тип Either в C # (дух!), Но это не идиоматично, поэтому он не был очевидным ответом на предыдущий вопрос.

4 голосов
/ 21 сентября 2010

Предполагая, что я понимаю вопрос, в F # вы можете определить выражения, которые зависят от наличия членов с конкретными сигнатурами.Например,

let inline f x a = (^t : (member Method : ^a -> unit)(x,a))

Это определяет функцию f, которая принимает значение x типа ^t и значение a типа ^a, где ^t имеет метод Method, принимающийот ^a до unit (void в C #) и который вызывает этот метод.Поскольку эта функция определена как inline, определение является встроенным в точке использования, что является единственной причиной, по которой ей может быть присвоен такой тип.Таким образом, хотя вы можете передавать f как функцию первого класса, вы можете делать это только тогда, когда типы ^t и ^a статически известны, так что вызов метода может быть статически разрешен и вставлен на место (и этопочему параметры типа имеют смешной ^ символ вместо обычного ' символ).

Вот пример передачи f в качестве функции первого класса:

type T() = 
  member x.Method(i) = printfn "Method called with int: %i" i

List.iter (f (new T())) [1; 2; 3]

Это запускает метод Method против трех значений в списке.Поскольку f является встроенным, это в основном эквивалентно

List.iter ((fun (x:T) a -> x.Method(a)) (new T())) [1; 2; 3]

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

Учитывая контекст, который, кажется, привел к этому вопросу ( C #- Как я могу «перегрузить» делегата? ), я, кажется, вообще не ответил на ваш реальный вопрос.Вместо этого Гейб, похоже, говорит о том, с какой легкостью можно определять и использовать дискриминационные союзы.Таким образом, на вопрос, заданный в этом другом потоке, можно ответить следующим образом, используя F #:

type FunctionType = 
| NoArgument of (unit -> unit)
| ArrayArgument of (obj[] -> unit)

let doNothing (arr:obj[]) = ()
let doSomething () = printfn "'doSomething' was called"

let mutable someFunction = ArrayArgument doNothing
someFunction <- NoArgument doSomething

//now call someFunction, regardless of what type of argument it's supposed to take
match someFunction with
| NoArgument f -> f()
| ArrayArgument f -> f [| |] // pass in empty array

На низком уровне здесь не происходит никакой магии CIL;просто NoArgument и ArrayArgument являются подклассами FunctionType, которые легко построить и деконструировать с помощью сопоставления с образцом.Ветви выражения сопоставления с образцом морально эквивалентны тесту типа, за которым следует доступ к свойствам, но компилятор гарантирует, что случаи имеют 100% охват и не перекрываются.Вы можете кодировать те же самые операции в C # без каких-либо проблем, но это было бы намного более многословно, и компилятор не помог бы вам с проверкой полноты и т. Д.

Кроме того, здесь нет ничего особенногок функциям;Дискриминационные объединения F # позволяют легко определять типы, которые имеют фиксированное количество именованных альтернатив, каждый из которых может иметь данные любого типа, который вы хотите.

1 голос
/ 21 сентября 2010

Я не совсем уверен, что правильно понял ваш вопрос ... Компилятор F # использует тип FSharpFunc для представления функций. Обычно в коде F # вы не имеете дело с этим типом напрямую, вместо этого используйте причудливое синтаксическое представление, но если вы предоставите какие-либо члены, которые возвращают или принимают функцию, и используете их из другого языка, строка C # - вы увидите это. Таким образом, вместо использования делегатов - F # использует свой специальный тип с конкретными или универсальными параметрами. Если ваш вопрос был о таких вещах, как , добавьте оператор "1004 *" что-то, чего я не знаю, что именно, но он имеет добавление ", тогда вам нужно использовать ключевое слово inline и компилятор выдаст тело функции на сайте вызова. Ответ @ kvb описывал именно этот случай.

...