【问题标题】:Trouble understanding type inference when returning function from function in FSharp从 FSharp 中的函数返回函数时无法理解类型推断
【发布时间】:2016-10-07 07:22:06
【问题描述】:

我在我的 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 函数而不是通过getWarn

【问题讨论】:

  • 尝试像这样重写getWarn 函数:let getWarn (logger: log4net.ILog) format = Printf.ksprintf logger.Warn format。这会让编译器的类型推断找出你想要的吗?如果是这样,那么您遇到了某种问题,即它可以在经典函数声明中找出参数的类型,但当相同的参数在 lambda 表达式中时却不行。
  • 不,我害怕同样的结果
  • 尝试将管道转发到警告。
  • 当您将鼠标悬停在getWarn 函数的format 参数上时,您的IDE 会显示什么类型?是StringFormat<'T,'Result>吗?您看到的函数的 warngetWarn 版本有哪些类型?
  • @munn warn: StringFormat<'a,unit> -> 'a, getWarn: ILog -> StringFormat<'a,unit> -> 'a 如果我将 format 提供给 getWarn 函数,它会得到 'a 的类型

标签: f#


【解决方案1】:

这是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 时,你也会调用 getWarnGetLogger。在这个特定的示例中,这可能没问题,但如果您在生成返回函数之前做了一些重要的工作,那么每次调用都会重新完成这项工作。

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

let mapTuple f (a,b) = (f a, f b)
let makeList x = [x]

mapTuple makeList (1, "abc")

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

所以底线是,如果你只想生成一个warn 函数,只需给它一个参数就可以了。但是,如果您想一次生成两个(正如您在 cmets 中提到的那样),那么您就不走运了:它们最终必须属于同一类型,并且您不能在它们之后将它们称为泛型函数重新制作。

如果您想要一个函数返回一个(或多个)函数而不失去其通用性,则必须返回一个接口。接口上的成员函数可以是独立于接口本身的泛型的泛型:

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

【讨论】:

  • 谢谢!这正是我正在寻找的答案!现在我明白了编译器为什么会这样,并且基于这些知识我可以找到更好的方法来解决我的记录器问题。
  • 感谢您对我所见过的价值限制做出最好的解释!我觉得我现在真的明白了。有 200 个代表作为感谢! (在 24 小时内 - 系统不会让我奖励我刚刚发布的赏金,直到至少 24 小时过去。)
  • @rmunn,感谢您的高度赞扬,我认为这真的不值得:-)
  • 要添加更多参数的方法,例如abstract member Exnf: exn -&gt; StringFormat&lt;'h, unit&gt; -&gt; 'hmember x.Exnf exn format = Printf.ksprintf (fun title -&gt; session.LogException(title, exn)) format(这是我实现的差异日志库的 sn-p,因此请根据需要进行更改。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-03
  • 1970-01-01
  • 1970-01-01
  • 2021-06-16
相关资源
最近更新 更多