为什么不注释功能参数?


28

为了使这个问题更容易回答,让我们假设程序员头脑中模棱两可的代价要比几次额外的击键要昂贵得多。

鉴于此,我为什么不让我的队友注释其功能参数就逃脱呢?以下面的代码为例,这可能是更复杂的代码:

let foo x y = x + y

现在,对工具提示的快速检查将向您显示F#已确定将x和y表示为整数。如果这就是您的意图,那么一切都很好。但是不知道那不是你的意图。如果您已创建此代码以将两个字符串连接在一起怎么办?或者,如果我认为您可能打算添加双打呢?或者,如果我只是不想将鼠标悬停在每个函数参数上来确定其类型,该怎么办?

现在以这个为例:

let foo x y = "result: " + x + y

F#现在假设您可能打算连接字符串,因此x和y被定义为字符串。但是,作为维护您的代码的可怜的schmuck,我可能会看这件事,并想知道您是否打算将x和y(整数)加在一起,然后将结果附加到用于UI的字符串中。

当然,对于这样简单的示例,可以放手,但是为什么不执行显式类型注释的策略呢?

let foo (x:string) (y:string) = "result: " + x + y

模棱两可有什么危害?当然,程序员可能会为尝试做的事情选择错误的类型,但至少我知道他们的意图是,这不仅仅是疏忽。

这是一个严重的问题...我对F#还是很陌生,正在为我的公司开拓创新。我采用的标准可能会成为将来所有F#编码的基础,并且嵌入无止尽的复制粘贴中,我相信它将在未来的几年中渗透到整个文化中。

那么...关于F#的类型推断是否有什么特别之处,使其成为一项有价值的功能,仅在必要时进行注释?还是专家F#-er习惯于为非平凡的应用程序注释其参数?


带注释的版本不太通用。出于同样的原因,您最好在签名中使用IEnumerable <>,而不要在SortedSet <>中使用。
帕特里克

2
@Patrick-不,不是,因为F#正在推断字符串类型。我没有更改函数的签名,只是将其明确。
JDB

1
@Patrick-即使是真的,你能解释为什么这是一件坏事吗?如果那是程序员想要的,则签名应该不太一般。也许更一般的签名实际上是在引起问题,但是我不确定在没有大量研究的情况下程序员打算如何具体。
JDB

1
我认为可以公平地说,在函数式语言中,您倾向于通用性,并在更具体的情况下进行注释。例如,当您想要更好的性能时,可以使用类型提示获得它。在任何地方使用带注释的函数将要求您为每种特定类型编写重载,这在一般情况下可能不做保证。
罗伯特·哈维

Answers:


30

我不使用F#,但是在Haskell中,它是注释(至少)顶级定义(有时是本地定义)的一种好形式,即使该语言具有广泛的类型推断能力。这有几个原因:

  • 阅读
    当您想知道如何使用函数时,使类型签名可用非常有用。您可以简单地阅读它,而不必尝试自己推断或依靠工具来为您做。

  • 重构
    当您想改变一个函数时,拥有一个明确的签名可以确保您的转换保留了原始代码的意图。在类型推断的语言中,您可能会发现高度多态的代码将进行类型检查,但不会达到您的预期目的。类型签名是在界面上具体化类型信息的“屏障”。

  • 性能
    在Haskell中,函数的推断类型可能会重载(通过类型类),这可能意味着运行时调度。对于数字类型,默认类型是任意精度整数。如果您不需要这些功能的全部通用性,则可以通过将功能专用于所需的特定类型来提高性能。

对于局部定义,let-bound变量和lambda的形式参数,我发现类型签名通常在代码中花费的成本高于它们所添加的值。因此,在代码审查中,我建议您坚持使用顶级定义的签名,而仅在其他地方要求明智的注释。


3
这听起来像是一个非常合理且均衡的响应。
JDB

1
我同意具有明确定义的类型的定义可以在重构时提供帮助,但是我发现单元测试也可以解决此问题,并且因为我认为单元测试应与函数式编程一起使用,以确保函数在设计之前就可以正常工作了。函数与另一个函数一起使用,这使我可以将类型排除在定义之外,并确信如果进行重大更改,它将被捕获。示例
Guy Coder 2013年

4
在Haskell中,对函数进行注释被认为是正确的,但是我相信惯用的F#和OCaml倾向于省略注释,除非有必要消除歧义。这并不是说有害(尽管F#中的注释语法比Haskell中的丑陋)。
KChaloux 2013年

4
@KChaloux在OCaml中,目前已经在接口(输入注释.mli)文件(如果你写一个,你都强烈建议键入注释往往是从定义省略,因为他们会是冗余的界面。
吉尔“所以-别再邪恶了

1
@Gilles @KChaloux确实与F#签名(.fsi)文件相同,但有一个警告:F#不会将签名文件用于类型推断,因此,如果实现中的某些内容含糊不清,您仍然必须再次对其进行注释。books.google.ca/...
亚瓦尔·阿明

1

乔恩给出了一个合理的答案,在此我不再赘述。但是,我将向您展示可能满足您需求的选项,在此过程中,您将看到除是/否之外的其他答案。

最近,我一直在使用解析器组合器进行解析。如果您知道解析,那么您通常会在第一阶段使用词法分析器,而在第二阶段使用语法分析器。词法分析器将文本转换为标记,解析器将标记转换为AST。现在将F#作为一种功能语言并将组合器设计为要组合的,解析器组合器被设计为在词法分析器和解析器中使用相同的功能,但是如果您为解析器组合器函数设置类型,则只能使用它们来lex或解析,而不是两者兼而有之。

例如:

/// Parser that requires a specific item.

// a (tok : 'a) : ('a list -> 'a * 'a list)                     // generic
// a (tok : string) : (string list -> string * string list)     // string
// a (tok : token)  : (token list  -> token  * token list)      // token

要么

/// Parses iterated left-associated binary operator.


// leftbin (prs : 'a -> 'b * 'c) (sep : 'c -> 'd * 'a) (cons : 'd -> 'b -> 'b -> 'b) (err : string) : ('a -> 'b * 'c)                                                                                    // generic
// leftbin (prs : string list -> string * string list) (sep : string list -> string * string list) (cons : string -> string -> string -> string) (err : string) : (string list -> string * string list)  // string
// leftbin (prs : token list  -> token  * token list)  (sep : token list  -> token  * token list)  (cons : token  -> token  -> token  -> token)  (err : string) : (token list  -> token  * token list)   // token

由于该代码具有版权,因此我不会在此处包含它,但可以在Github上获得。不要在这里复制它。

为了使这些功能正常工作,必须将它们与通用参数一起使用,但是我包括一些注释,这些注释根据函数的使用显示推断出的类型。这使得易于理解要维护的功能,同时又保留了通用功能。


1
为什么不将通用版本写入函数?那行不通吗?
svick

@svick我不明白你的问题。您的意思是我将类型排除在函数定义之外,但是可以将泛型添加到函数定义中,因为泛型不会改变函数的含义吗?如果是这样,原因更多是出于个人喜好。当我第一次使用F#时,我确实添加了它们。当我开始与F#的更高级用户一起工作时,他们希望将它们保留为注释,因为在没有签名的情况下修改代码更加容易。...
Guy Coder 2013年

从长远来看,一旦代码开始工作,我就将它们显示为对所有人有用的注释。当我开始编写函数时,我会输入类型。函数正常工作后,将类型签名移至注释并删除尽可能多的类型。使用F#有时,您需要保留类型以帮助进行推理。一段时间以来,我一直在尝试使用let和=创建注释,以便您可以取消注释该行以进行测试,然后在完成后再次对其进行注释,但是它们看起来很傻,并且添加let和=并不难。
Guy Coder 2013年

如果这些评论回答了您的要求,请告诉我,以便我将其转移到答案中。
Guy Coder 2013年

2
因此,在修改代码时,您不会修改注释,导致文档过时了吗?这听起来对我来说不是优势。而且我真的看不到凌乱的注释比凌乱的代码更好。在我看来,这种方法结合了两种选择的缺点。
svick

-8

错误的数量与程序中的字符数量成正比!

通常将其表示为与代码行成比例的错误数量。但是用更少的行使用相同数量的代码将产生相同数量的错误。

因此,虽然很明确,但键入的任何其他参数都可能导致错误。


5
“错误的数量与程序中的字符数量成正比!”因此我们都应该切换到APL吗?过于简洁是一个问题,就像过于冗长一样。
svick

5
为了使这个问题更容易回答,我们假设程序员头脑中的歧义代价要比其他几次击键要昂贵得多。
JDB 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.