在FSharp中从函数返回函数时难以理解类型推断

时间:2016-10-07 07:22:06

标签: f#

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

对于警告,我创建了以下功能:

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

我收到编译错误"The value is not a function and cannot be applied" 如果我删除第一个warn语句,它就可以了。

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

但为什么在我的第一个例子中并非如此,我直接使用warn函数而不是getWarn

1 个答案:

答案 0 :(得分:8)

这是value restriction

当你将warn声明为普通函数时,它只是一个通用函数,没有大惊小怪。但是当你声明它没有参数时,它变成了一个(它是一个函数类型的值,但仍然是一个值;有一个细微的差别),因此受到值限制,这在一个坚持认为,价值观不能是通用的。

尝试删除warn的所有用法(不仅仅是第一个)。您将得到编译器抱怨:Value restriction. The value 'warn' has been inferred to have generic type。请点击顶部的链接进行更多讨论。

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

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

let warn() = getWarn log4net.LogManager.GetLogger("bar")

另一种解决方法(实际上是相同的 - 见下文)是明确声明泛型参数,并添加类型注释:

let warn<'a> : StringFormat<'a, unit> -> 'a = log4net.LogManager.GetLogger("bar")

在这种情况下,类型注释是必需的,因为否则编译器不知道泛型参数如何与值类型相关。

这样,您就可以像使用真正的通用值一样使用它:

warn "This is a warning"
warn "The value of my string is %s" someString

但是有一个问题:这个技巧与添加单位参数(上面的解决方法,上面)非常相似。在幕后,编译器将此定义作为真正声明的泛型函数发出,并为其提供单个unit参数。这个(以及之前的解决方法)的含义是,每次使用warn时,都会调用它 - 即每次调用warn时,都会调用getWarn和{{1也是。在这个特定的例子中,这可能没问题,但是如果你在生成return函数之前做了一些重要的工作,那么每次调用都会重新完成这项工作。

这使我们对您的方法有了更深层次的问题:您试图在不失去其通用性的情况下传递函数。不能这样做。不能有函数值,从函数返回它或将它传递给函数,并且仍然保持通用。试试这个:

GetLogger

本能地,人们会期望这会产生并产生一个列表元组 - let mapTuple f (a,b) = (f a, f b) let makeList x = [x] mapTuple makeList (1, "abc") ,对吧?好吧,它不会像这样工作。当您声明( [1], ["abc"] )时,mapTuple f (a,b)参数必须是某种特定类型的 。它不能是开放的通用类型,因此即使它们属于不同类型,您也可以将其应用于fa。相反,推理以另一种方式工作:因为b属于一种类型,所以认为编译器,它应用于fa,然后b和{ {1}}必须属于同一类型。因此签名被推断为a

所以最重要的是,如果你只想制作一个b函数,只需给它一个参数就可以了。但是,如果你想同时生成两个(正如你在评论中提到的那样),那你就不幸了:它们必须最终属于同一类型,你不能把它们称为通用函数。重新制作。

如果你想拥有一个函数来返回一个函数(或多个函数)而不会失去它的通用性,你将不得不返回一个接口。接口上的成员函数可以是通用的,与接口本身的通用性无关:

mapTuple: ('a -> 'b) -> 'a*'a -> 'b*'b