F#如何编译可以将多种不同参数类型带入IL的函数?

时间:2010-09-21 03:36:48

标签: f# compilation cil

我对F#几乎一无所知。我甚至不知道语法,所以我不能举例。

在注释线程中提到F#可以声明可以接受多种可能类型的参数的函数,例如字符串或整数。这类似于C#中的方法重载:

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

但是,在CIL中,您无法声明此表单的委托。每个代理必须具有一个特定的参数类型列表。因为F#中的函数是一等公民,所以看起来你应该能够传递这样的函数,并且将它编译成CIL的唯一方法就是使用委托。

那么F#如何将其编译成CIL?

4 个答案:

答案 0 :(得分:11)

这个问题有点含糊不清,所以我只是絮絮叨叨F#的真实情况。

在F#中,方法可以重载,就像C#一样。始终使用someObj.MethodNamesomeType.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-bound函数值不能重载。所以:

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特性,+运算符,例如,作为一个let-bound函数类型,可以“仅适用于int s“或”仅适用于float s“或诸如此类。某些ML风格(F#是OCaml的亲属)正是这样做的,例如仅+运算符适用于int,一个名为+.的单独运算符适用于float s。而在F#中,+是一个inline函数F#库适用于任何具有+运算符成员或任何原始数字类型的类型。内联还可以具有一些潜在的运行时性能优势,这对于某些数学/计算域也很有吸引力。 )

答案 1 :(得分:6)

当您编写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#中我们可以编写callFcall 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;
}

当然你可以在C#(duh!)中实现相同的Either类型,但它不是惯用的,这就是为什么它不是上一个问题的明显答案。

答案 2 :(得分:4)

假设我理解了这个问题,在F#中你可以定义表达式,这些表达式取决于具有特定签名的成员的可用性。例如

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

这定义了一个函数f,它取x类型的值^t和类型a的值^a,其中^t有一个方法{{ 1}}将Method带到^a(C#中的unit),并调用该方法。因为此函数定义为void,所以定义在使用点处内联,这是可以给出这种类型的唯一原因。因此,虽然您可以将inline作为第一类函数传递,但只有在静态知道类型f^t时才能这样做,以便可以静态地解析和插入方法调用到位(这就是为什么类型参数具有有趣的^a sigil而不是正常的^ sigil)。

以下是将'作为第一类函数传递的示例:

f

这会针对列表中的三个值运行方法type T() = member x.Method(i) = printfn "Method called with int: %i" i List.iter (f (new T())) [1; 2; 3] 。因为Method是内联的,所以这基本上等同于

f

修改

鉴于似乎导致这个问题的背景(C# - How can I “overload” a delegate?),我似乎根本没有解决你真正的问题。相反,Gabe似乎在谈论的是人们可以轻松地定义和使用受歧视的联盟。因此,使用F#:

可以像这样回答关于其他线程的问题
List.iter ((fun (x:T) a -> x.Method(a)) (new T())) [1; 2; 3]

在较低的水平,这里没有CIL魔法;只是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 NoArgumentArrayArgument的子类,它们易于构造并通过模式匹配进行解构。模式匹配表达式的分支在道德上等同于类型测试,后跟属性访问,但编译器确保案例具有100%覆盖率且不重叠。您可以在C#中对完全相同的操作进行编码而没有任何问题,但它会更加冗长,编译器也无法帮助您进行详尽的检查等。

此外,这里没有什么特别的功能; F#区分联合可以很容易地定义具有固定数量的命名选项的类型,每个类型都可以包含您想要的任何类型的数据。

答案 3 :(得分:1)

我不太确定正确理解你的问题...... F#编译器使用FSharpFunc类型来表示函数。通常在F#代码中,你不直接使用花哨的句法表示来处理这种类型,但如果你暴露任何返回或接受函数并使用其他语言的成员,那么行C# - 你会看到它。 因此,F#不使用委托 - 而是使用具有特定参数的特殊类型。 如果您的问题是关于添加某些东西 - 我不知道什么是完全但是它有添加运算符那么您需要使用 inline 关键字和编译器将在调用站点中发出函数体。 @kvb的回答正好描述了这个案例。