Foo to Bar:Haskell中的命名约定

2020-12-16 14:53:25

开发人员将大部分时间花在阅读,理解和探索使用现有解决方案的其他方式上。坦白说,在我们的职业中,几乎没有时间在现实生活中实际编写新库和创建新接口。因此,在最常见的活动中获得帮助非常重要。命名约定就是这样一种事情,如果达成共识并在全球范围内推广,则可以提高可读性并降低使用成本。

某些语言具有自己有意义的特殊命名约定。 Haskell就在其中。生态系统中每个地方都有很多命名模式(包括标准库),这些命名模式可以帮助您在不查看功能文档甚至类型的情况下识别其含义!此功能特别重要,因为命名是最难解决的开发问题之一,因此在此领域提供一些帮助和容易理解的规则可以改善每个人的生活。

在本文中,我们将一起探讨Haskell中的常见命名约定。它对创建者(库和API开发人员)和消费者(库用户)都非常有用,因为它建立了库API中接受的规范。

🦋如果您对其他有关如何编写Haskell代码的约定和最佳实践感兴趣,可以查看我们的样式指南。

让我们从Haskell的规范和标准中建立的常规和直接规范开始。 Haskell中的名称必须满足以下简单规则:

这些规则在规范中,因此由编译器检查。因此,如果尝试破坏命名规则,则在编译过程中会出错。

此外,函数遵循LowerCamelCase样式,类型遵循UpperCamelCase样式。这是用Haskell编写代码的事实上的标准,但是使用不同的样式不会导致编译器错误。此外,有些测试库使用snake_case自动发现测试。但是,您可以使用Haskell工具进行限制,例如HLint可以为您检查。

名称中有各种详细信息,这些提示将提示您该函数的功能。我们将逐步通过它们来学习认识所有人。

让我们从类型签名和定义中最常用的类型变量开始。一些变量表示特殊含义:它可以是与类型类有关的约定,也可以只是方便的较短用法。但是无论如何,在阅读和编写类型时,了解这些信息都是有用的:

(>> =):: Monad m => m a-> (a-> m b)-> m b(<>)::半群m => m-> m->米

我们在函数中命名参数和变量的方式也不是偶然的。它们包含一些提示,这些提示使阅读函数主体中使用的这些变量更加容易。函数中的变量使用以下已建立的常用名称:

Haskell函数中的后缀可以包含许多有关其用途的信息。有时您会同时看到多个不同的后缀,这些后缀结合了每个片段的特征,因此注意这一点很有帮助。

'在功能中使用符号,对于该功能,存在一个不带撇号的相应功能,例如foo和foo'。

最后的撇号表示它是类似功能的严格版本。这两个函数必须具有相同的类型,但其下的实现不同。他们的行为的唯一区别是带有'符号会更急切地评估中间结果。

foldMap ::(可折叠t,Monoid m)=> (a-> m)-> t a-> m foldMap' ::(可折叠t,Monoid m)=> (a-> m)-> t a->米

如您所见,这两个函数具有相同的类型。但是foldMap'当进行单向运算时,效率更高并且有助于避免空间泄漏。这两个论点都严格。

有一组符号用于指示函数在某些上下文中返回该值。该后缀-大写字母或单词-告诉我们该上下文应表示的类型类。

alter :: Ord k => (可能是->可能是-)-> k->映射k a->映射卡

alterF ::(函子f,Ord k)=> (也许a-> f(也许a))-> k->映射k a-> f(地图k a)

但是,有时后缀F具有其他含义。 F在格式化库中经常用作后缀,以指示函数是格式化程序还是漂亮打印机。

您会看到相同的命名如何具有不同的含义。重要的是,库必须明确建立其命名约定,并始终如一地使用它。

后缀A表示该函数适用于某些通用Applicative类型(具有Applicative实例的类型)。

后缀M是最常见的后缀。它通常意味着该函数可用于Monad或在Monadic上下文中使用。

注意:从历史上看,标准的Haskell库库没有Applicative函子,也没有Monads的超类。但是现在后缀M有时也与Applicative函数一起使用。

下划线作为功能的后缀,也具有特殊的含义。它为我们提供了一个线索,即该函数与不带_的函数完全一样,但是丢弃了结果(改为使用return()代替)。

您经常可以看到一系列函数,其名称末尾带有数字。这些组的初始部分相同,但数量不同。那里的数字代表每个函数采用的参数数量。

🔢通常不使用数字1,因为它是多余的。

liftA ::适用f => (a-> b)-^ 1-> f a-> f b-^ 1

liftA2 ::适用f => (a-> b-> c)-^ 1 ^ 2-> f a-> f b-> f c-^ 1 ^ 2 liftA3 ::适用f => (a-> b-> c-> d)-^ 1 ^ 2 ^ 3-> f a-> f b-> f c-> f d-^ 1 ^ 2 ^ 3 zipWith3 ::(a-> b-> c-> d)-> [a]-> [b]-> [c]-> [d] zipWith4 ::(a-> b-> c-> d-> e)-> [a]-> [b]-> [c]-> [d]-> [e] 数字1的特殊含义是要求容器中至少要包含一个参数: folder ::可折叠t => (a-> b-> b)-> b-> t a-> b foldr1 ::可折叠t => (a-> a-> a)-> t a-> 一种 但是,它与Foldable一起使用的事实并不理想。 有人建议为非空类型实现一个称为Foldable(或Semifoldable)的类型类。

后缀L和R(有时是l和r)代表功能应用的方向或遍历数据结构的顺序。

ℹ️在大多数情况下,这些兄弟功能具有相同的类型,但是在某些功能中,为了方便起见,某些自变量被反转。

folder ::可折叠t => (a-> b-> b)-> b-> t a-> b折叠::可折叠t => (b-> a-> b)-> b-> t a-> b扫描仪::(a-> b-> b)-> b-> [a]-> [b] scanl ::(b-> a-> b)-> b-> [a]-> [b] mapAccumR ::可遍历t => (a-> b->(a,c))-> -> t b-> (a,t c)mapAccumL ::可遍历t => (a-> b->(a,c))-> -> t b-> (a,t c)

某些可折叠或列表可使用的重载函数的后缀By具有不可重载的同级。

在某些库或应用程序的代码中,后缀P表示该函数是某种类型的解析器,例如使用optparse应用程序库时。该命名约定的用法如下所示:

数据Config = Config {configPort ::端口,configPath :: FilePath} portP ::解析器端口pathP ::解析器FilePath configP ::解析器Config configP =做configPort<-portP configPath<-pathP pure Config {..}

现在,我们将重点介绍函数前缀及其含义。与后缀相似,开发人员经常使用一些已建立的模式。

Haskell中的新类型是一种普遍的模式。 它是某种类型的包装。 因此,重要的是要提及与类型的关系或名称中的新类型这一事实。 新类型可以为其唯一字段命名。 最常见的命名约定之一是将此字段命名为以un为前缀的类型名称(unwrap的缩写): 当un后跟一个小写字母时,通常表示同一函数的反函数(撤消的缩写): folder ::可折叠t => (a-> b-> b)-> b-> t a-> b展开目录::(b->也许(a,b))-> b-> [一种] 在标准库库中,您可以找到许多使用前缀get的Monoidal新类型,它们具有相同的用途: 但是,如果新类型是Monad的包装,则使用前缀run代替: 新类型StateT s m a = StateT {runState :: s-> m(a,s)}

记录数据类型中的字段具有在Haskell生态系统中广泛使用的几种众所周知的命名约定,并且可能经常相同。

一种流行的命名规则是在每个字段前面加上完整类型名称,以避免名称与其他记录冲突:

有时,当类型的全名过长时,缩写会用作前缀:

前缀pretty用于纯函数,这些函数以更易于理解的方式显示值,与show不同,而show应该由Haskell解析。

数据GhcVersion = Ghc884 | Ghc8102派生库存(显示)prettyGhcVersion :: :: GhcVersion->文字prettyGhcVersion = \ case Ghc884-> " GHC 8.8.4" ghc8102-> " GHC 8.10.2"

当满足条件时,when *系列功能通​​常会执行某些操作。通常,第一个参数是条件,后跟需要执行的操作。此类函数通常会丢弃其中一个的结果并返回pure()。

当::适用f =>布尔-> f()-> f()-当M :: Monad m => m布尔-> m()-> m()when::: Applicative m =>也许-> (a-> m())-> m()whenNothingM :: Monad m => m(可能是a)-> m a-> m a whenLeft_ ::适用f => l r-> (l-> f())-> F ()

同样,除非有与检查相反的含义,否则还有一个前缀:when(not p)≡除非p。

前缀用于检查某些属性并返回Bool的谓词。该属性也可以是对构造函数的总和类型的检查,或者是更具体的检查:

isNothing ::也许-> Bool isLeft :: a b-> Bool isEven :: Int->布尔

我们已经看到了后缀M。但是,m也经常用作前缀。当您在该位置看到m时,它可能具有以下两种不同的含义。

当后面跟一个小写字母时,通常意味着该函数适用于某些Monadic类型(类似于后缀的含义)。

过滤器::((a-> Bool)-> [a]-> [a] mfilter :: MonadPlus m => (a-> Bool)-> m a-> m zip :: [a]-> [b]-> [(a,b)] mzip :: MonadZip m => m a-> m b-> m(a,b)

但是,当后面跟一个大写字母时,通常意味着这是值的Maybe版本。此命名约定通常与局部变量一起使用。

printPath ::也许是FilePath-> IO()printPath mPath =大小写mPath-> putStrLn"未指定路径" 正义之路-> putStrLn $"路径为:" ++路径 标准库使用后缀泛型来提供返回多态值或使用更多多态参数的函数。 因此,它们通常要慢得多,但是在某些情况下,它们是最佳选择。 Haskell允许定义自定义运算符,并且作为常规函数,它们还具有一些自己的命名约定。 某些已经存在的运算符周围的箭头通常表示从某种意义上说它是它的提升版本: <> 层可以表示相同概念的应用程序数量。 (<< $>>))::(Functor f,Functor g)=> (a-> b)-> f(g a)-> f(g b) (< $)::函子f => -> f b-> f a($>)::函子f => f a-> b-> b

有的经营者有!在它们中,这意味着它们是其类似物的严格版本:

($)::(a-> b)-> -> b($!)::(a-> b)-> -> b(< $>)::函子f => (a-> b)-> f a-> f b(< $!>):: Monad m => (a-> b)-> m a-> b

通过返回值或对其参数执行某些操作来处理每个构造函数的函数称为消除器。它与类型同名,并以小写字母开头。

bool :: a-^处理' False' -> a-^处理'正确' ->布尔->一种

也许:: b-^ Handle' Nothing' -> (a-> b)-^处理' ->也许-> b

要么::(a-> c)-^ Handle' Left' -> (b-> c)-^处理'正确' -> a b-> C

验证::(e-> c)-^处理'失败' -> (a-> c)-^处理'成功' ->验证-> C

有时,函数还具有前缀is或后缀Of / From(或两者都有),使它们读起来更像自然语言。

我们重点介绍了Haskell生态系统中一些最常见和已建立的命名约定。但是有时,不同的Haskell库或特定函数不遵循通用规则,并且库本身内部的命名规则不一致或不一致。

这意味着并不是每个库都使用命名约定,这是不幸的。习惯一些规则和常识,然后意识到它们在某些地方不起作用,并且您对某些事情应该如何工作的假设是不正确的,这非常令人困惑。这浪费了我们的时间,也拖慢了流程。作为一个社区,我们应该更加努力地建立和遵循最佳实践,因为根据2020年Haskell状态调查结果,这是Haskell开发人员最关注的难题之一。

在具有容器实现的程序包中,提取Map键的功能称为键,但提取值的功能称为elems,而不是逻辑上需要的值。

在容器中将字典转换为键值对列表的函数称为assoc,在无序容器中的函数称为toList。同时,toList也是Foldable的一种方法,在两种情况下的行为都与元素完全一样。

一般来说,不具有用于容器数据结构(映射,集合,序列等)的统一接口有时会引起痛苦。尽管生态系统尚未准备好使用背包功能(在Haskell已有4年历史了),但是container-backpack是解决此问题的一种方法。

* sql-simple系列库具有后缀_表示“无参数”而不是“此函数丢弃结果”的函数。

人们使用撇号'为它们的更新变量定义局部变量,因为很难想出一个新名称来更好地反映作用域中新var的含义。例如。您经常会看到这样的情况:let cur = f x; cur' = g cur; cur'' = h cur'。当变量彼此之间不接近时,这种方法使代码难以遵循,并且经常造成混乱,并且您首先想到的是使用了函数的某些严格版本。

Haskell具有称为“键入孔”的功能。此功能允许在表达式中使用以下划线开头的变量,并使编译器可以帮助您指定表达式的类型。但是,这与镜头命名规则相冲突:元组和棱镜以_开头的_1和_2镜头。

标准库库中的名称在某些方面也不一致。有一些模式,我们也在文章中描述过,但是也存在一些异常。例如,诸如Max和Const之类的新类型具有名为getMax和getConst的字段,但是Identity(也是一个新类型)具有名称runIdentity。这种不一致经常会令人很困惑,并且需要牢记相似结构值的不同命名约定。

各种功能中的m / M字母并不能真正说明单子类型应该去哪里。自己看看:

filterM ::适用m => (a-> m Bool)-> [a]-> m [a] mfilter :: MonadPlus m => (a-> Bool)-> m a->嘛

从某种意义上说,这两个函数都满足我们在谈论的命名约定。但是,没有逻辑上的解释说明为什么第一个正好使用列表,而另一个正好使用普通Monad。

我们相信,通过拥抱标准规则并彼此共享此知识,我们所有人都可以在这里做得更好。

进行编程时,通常对所接受的规则和最佳实践一无所知。 感觉到“在家”需要花费一些时间(以及适当的人员和资源以供学习)。 使用新语言时也是如此。 命名是代码可读性,可用性和可理解性的基本关键之一。 因此,共享这些知识同样重要。 为了使社区变得更强大,用户更加自信并团队合作,我们都需要遵循一些通用的命名标准。 我们希望本文中的观察结果可以成为一些更常见的规范和准则的第一步。 如果您喜欢我们的工作,则可以通过支持Ko-Fi或GitHub来帮助我们做更多的事情: 喜欢这篇文章吗? 考虑始终订阅我们的新闻通讯,以查看我们的新更新: