F# 从FSharp中的函数返回函数时理解类型推断有困难

F# 从FSharp中的函数返回函数时理解类型推断有困难,f#,F#,我在我的F#应用程序中使用log4net,希望通过创建一些函数使日志记录更加地道 对于警告,我创建了以下函数: let log = log4net.LogManager.GetLogger("foo") let warn format = Printf.ksprintf log.Warn format 这很有效,我可以做到: warn "This is a warning" // and warn "The value of my string is %s" someString 然而,我想

我在我的F#应用程序中使用log4net,希望通过创建一些函数使日志记录更加地道

对于警告,我创建了以下函数:

let log = log4net.LogManager.GetLogger("foo")
let warn format = Printf.ksprintf log.Warn format
这很有效,我可以做到:

warn "This is a warning"
// and
warn "The value of my string is %s" someString
然而,我想让它更通用,并使之能够传入我想要的记录器。因此,我将该函数包装在另一个可以将ILog作为参数的函数中:

let getWarn (logger: log4net.ILog) =
    fun format -> Printf.ksprintf logger.Warn format
我也试过:

let getWarn (logger: log4net.ILog) format =
    Printf.ksprintf logger.Warn format
当我现在这样使用它时:

let warn = getWarn log4net.LogManager.GetLogger("bar")
warn "This is a warning"
但是,当我这样做时,这是可行的:

warn "The value of my string is %s" someString
我收到一个编译器错误,它说
“该值不是函数,无法应用”
如果我删除第一个warn语句,它就会工作

因此,我猜编译器根据我使用
warn
的第一条语句推断类型,因此我需要始终以相同的方式使用它

但是为什么在我的第一个示例中不是这样,我直接使用
warn
函数,而不是通过
getWarn

就是这样

当您将
warn
声明为普通函数时,它只是一个泛型函数,没有任何麻烦。但当您在没有参数的情况下声明它时,它会变成一个值(它是函数类型的值,但仍然是一个值;存在细微的差异),因此会受到值限制,简言之,值不能是泛型的

尝试删除
warn
的所有用法(而不仅仅是第一个用法)。编译器会对此抱怨:
值限制。已推断值“warn”具有泛型类型
。请按照顶部的链接进行更多讨论

当然,这有点太强了,因此F#稍微放宽了这一规则:如果在其声明附近使用该值,编译器将根据用法修复泛型参数。这就是为什么它适用于第一种用法,而不适用于第二种用法。你正确地理解了那部分

一种解决方法是添加显式参数,从而使
warn
成为“声明的函数”,而不是“函数类型的值”。试试这个:

let warn() = getWarn log4net.LogManager.GetLogger("bar")
let mapTuple f (a,b) = (f a, f b)
let makeList x = [x]

mapTuple makeList (1, "abc")
另一种解决方法(实际上是相同的-请参见下文)是显式声明泛型参数,并添加类型注释:

let warn<'a> : StringFormat<'a, unit> -> 'a = log4net.LogManager.GetLogger("bar")
但这里有一个陷阱:这个技巧实际上与添加一个单位参数(前面的解决方法,如上)是一样的。在幕后,编译器将把这个定义作为一个真正声明的泛型函数发出,并给它一个
单元
参数。这意味着(以及之前的解决方法)每次使用
warn
,它都会被调用,即每次调用
warn
,您也会调用
getWarn
GetLogger
。在这个特定的示例中,这可能是可以的,但是如果您在生成返回函数之前做了一些重要的工作,那么每次调用都会重新完成这些工作

这就给我们指出了您的方法的一个更深层次的问题:您试图在传递函数的同时不丢失它们的泛型性。不能那样做。不能有一个函数值,不能从函数返回它或将它传递给函数,但仍然使它保持泛型。试试这个:

let warn() = getWarn log4net.LogManager.GetLogger("bar")
let mapTuple f (a,b) = (f a, f b)
let makeList x = [x]

mapTuple makeList (1, "abc")
本能地,人们会期望这样做会产生一组列表-
([1],“abc”])
,对吗?嗯,它不是这样工作的。当您声明
mapTuple f(a,b)
时,该
f
参数必须是某种特定类型。它不能是开放的泛型类型,因此您可以将其应用于
a
b
,即使它们属于不同的类型。相反,推理是以另一种方式工作的:因为
f
是一种类型,编译器认为,并且它同时应用于
a
b
,那么
a
b
必须是同一种类型。因此签名被推断为
mapTuple:('a->'b)->'a*'a->'b*'b

因此,底线是,如果您只想生成一个
warn
函数,只需给它一个参数,就可以了。但是,如果您想同时生成两个(正如您在评论中提到的那样),那就太不走运了:它们必须是同一类型的,并且在生成后不能将它们作为泛型函数调用

如果您想让一个函数返回一个(或多个)函数而不丢失其泛型,则必须返回一个接口。接口上的成员函数可以是泛型的,与接口本身的泛型无关:

type loggers =
    abstract member warn: Printf.StringFormat<'a, unit> -> 'a
    abstract member err: Printf.StringFormat<'a, unit> -> 'a

let getLoggers (log: log4net.ILog) = 
  { new loggers with
    member this.warn format = Printf.ksprintf log.Warn format
    member this.err format = Printf.ksprintf log.Error format }

let logs = getLoggers (log4net.LogManager.GetLogger("Log"))

logs.warn "This is a warning"
logs.warn "Something is wrong: %s" someString
类型记录器=

抽象成员warn:Printf.stringformat尝试像这样重写
getWarn
函数:
让getWarn(logger:log4net.ILog)format=Printf.ksprintf logger.warn format
。这能让编译器的类型推断出你要做什么吗?如果是这样的话,那么您遇到了某种问题,它可以在经典函数声明中找出参数的类型,但在lambda表达式中不能找到相同的参数。不,同样的结果,我也很震惊尝试将管道转发到warn。当您将鼠标悬停在
getWarn
函数的
format
参数上时,IDE会显示什么类型?它是
StringFormat
?对于函数的
warn
getWarn
版本,您看到了哪些类型?@munn
warn:StringFormat StringFormat谢谢!这正是我想要的答案!现在我明白了为什么编译器会这样,基于这些知识,我可以找到更好的方法来解决我的日志问题。感谢您对我所看到的值限制给出了最好的解释!我觉得我现在真的明白了。有200个代表作为感谢!(24小时后——系统不会让我奖励我刚刚发布的赏金,直到至少24小时过去。)@rmunn,谢谢你的高度赞扬,我认为这真的不值得:-)添加更多参数的方法,例如执行
抽象