如果您练习类型化编程,即程序的许多语义内容可以静态反映在类型系统中,您会发现在许多(但不是全部)情况下,命名参数对于可读性来说不是必需的。
List
考虑OCaml 标准库中的以下示例。通过知道它们对列表进行操作,并且使用函数的名称(希望清楚:我们都支持良好的名称选择),您会发现您不需要解释参数的作用。
val append : α list -> α list
val flatten : α list list -> α list
val exists: (α -> α bool) -> α list -> bool
val map: (α -> β) -> α list -> β list
val combine : α list -> β list -> (α * β) list
请注意,最后一个示例很有趣,因为不清楚代码将做什么。实际上会有几种解释。combine [1;2] [3;4]
返回[(1,3); (2,4)]
,而不是,例如,[(1,3); (1,4); (2,3); (2,4)]
。也不清楚当两个列表长度不同时会发生什么(故障模式不清楚)。
不完全的函数,可能引发异常或不终止的函数,通常需要更多关于失败案例是什么以及它们将如何表现的文档。这是支持我们所谓的纯编程的一个强有力的论据,其中函数的所有行为都以返回值(无异常、可观察状态突变或非终止)的形式表示,因此可以通过以下方式静态捕获类型系统。
当然,这只适用于足够参数化的函数。他们有一种类型,可以非常清楚地说明他们的工作。并非所有函数都如此,例如考虑模块的blit
函数String
(我相信你最喜欢的语言也有这样的例子):
val blit : string -> int -> string -> int -> int -> unit
嗯?
出于这个原因,编程语言添加了对命名参数的支持。例如,在 OCaml 中,我们有允许命名参数的“标签”。相同的函数在StringLabels
模块中导出为:
val blit : src:string -> src_pos:int -> dst:string -> dst_pos:int -> len:int -> unit
这样更好。是的,命名参数在某些情况下很有用。
但是请注意,命名参数可用于隐藏糟糕的 API 设计(也许上面的示例也针对这种批评)。考虑:
val add : float -> float -> float -> float -> float -> float -> float * float * float
晦涩难懂吧?但是之后:
type coord = {x:float; y:float; z:float}
val add : coord -> coord -> coord
那好多了,我不需要任何参数标签(可以说记录标签提供命名,但实际上我可以float * float * float
在这里同样使用;值记录可以包含命名(和可选?)参数的事实也是一个有趣的评论)。
David M. Barbour提出了这样的论点,即命名参数是语言设计的“拐杖”,用于篡改 API 设计者的懒惰,并且没有它们会鼓励更好的设计。我不确定我是否同意在所有情况下都可以有利地避免命名参数,但他的观点肯定与我帖子开头的类型宣传一致。通过提高抽象级别(通过更多的多态/参数类型或更好的问题域抽象),您会发现您减少了对参数命名的需求。