脸谱网 推特 github LinkedIn链接 LinkedIn链接 LinkedIn链接
一张对称的照片,看起来是两栋完全相同的建筑。照片的视角是从两栋建筑的中间向上看天空。每栋建筑的窗户都有对面建筑的倒影。

为什么Haskell是我们构建生产软件系统的首选

哈斯克尔

Haskell是我们构建生产软件系统时使用的第一种编程语言。对于那些只对这门语言略知一二的人来说,这似乎是不寻常的。哈斯克尔以其高深的学习曲线而著称。它也经常被认为是一种实用性有限的研究语言。

虽然Haskell确实有很大的表面积,有许多概念和语法,来自大多数其他语言的程序员都会觉得不熟悉,但它在开发人员生产力、代码可维护性、软件可靠性和性能方面的组合是无与伦比的。在这篇文章中,我将介绍Haskell的一些定义特性,这些特性使它成为一种非常适合构建商业软件的优秀的工业级语言,以及为什么它通常是我们考虑用于新项目的第一个工具。

Haskell有一个强大的静态类型系统,可以防止错误并减少认知负荷

Haskell有一个非常强大的静态类型系统,它可以作为程序员的助手,在代码运行之前捕获并防止许多错误。许多程序员遇到Java或C++等静态类型语言时,会觉得编译器很麻烦。相比之下,Haskell的静态类型系统与编译时类型检查相结合,是一个无价的对编程伙伴,在开发过程中提供即时反馈。

与使用Python、JavaScript或PHP等语言编写相比,编写Haskell时需要维护的认知负载要小得多。许多关注点可以完全转移给编译器,而不需要程序员记住。例如,在编写Haskell时,无需提前提出以下问题:

  • 我需要检查此字段是否为空吗?
  • 如果请求有效负载中缺少字段怎么办?
  • 这个字符串已经解码为整数了吗?
  • 如果这个字符串不能解码为整数怎么办?
  • 这个操作符会隐式地将这个整数转换为字符串吗?
  • 这两个值可比吗?

这并不是说这些问题在Haskell中永远不需要回答;也就是说,当您需要解决其中一个问题时,编译器会抛出错误。例如,Haskell程序可能需要处理有时不存在的值,而不是将任何值设置为无效的,Haskell程序员必须使用也许 吧类型,它指示值可能不存在,并且编译器强制程序员显式处理没有什么价值;值不存在的情况。

Haskell的静态类型系统还带来了其他好处。Haskell代码在其函数之前使用类型签名,并描述每个参数和返回值的类型。例如,类似于Int->Int->布尔指示函数接受两个整数并返回布尔值。由于这些类型签名是由编译器检查和执行的,因此,这使得阅读Haskell代码的程序员在了解某段代码的功能时,可以只查看类型签名。例如,在寻找处理字符串、解码JSON或查询数据库的函数时,不会使用上面的类型签名。

类型签名甚至可以用于在整个Haskell代码库中搜索相关函数。使用胡格尔Haskell的API搜索,我们可以根据我们知道的需要的功能搜索类型签名。例如,如果我们需要转换国际浮动,我们可以搜索HoogleInt->浮点(搜索结果)这将为我们指明一个恰当的名称int2浮动功能。

Haskell还允许我们通过使用类型变量(由小写类型名表示)来创建多态类型签名。例如,签名a->b->a告诉我们,该函数接受两个任意类型的参数,并返回一个类型与第一个参数相同的值。假设我们想检查元素是否在列表中。我们正在寻找一个函数,它接受一个要搜索的项、一个项列表,并返回一个布尔值。我们不关心项目的类型,只要搜索项目和列表中的项目属于同一类型。所以我们可以搜索Hooglea->[a]->布尔(搜索结果),这将为我们指出元素功能。参数类型是Haskell中一个非常强大的功能,它支持编写可重用代码。

Haskell支持编写可组合、可测试且具有可预测副作用的代码

除了静态类型之外,Haskell还是一种纯函数式编程语言。这是Haskell的一个定义特性,也是该语言众所周知的特性,即使是那些只听说过Haskell-但从未使用过它的程序员也是如此。以纯函数风格编写有很多好处,并且有助于构建组织良好的代码基础。


从我们的专家软件工程师团队获得无限的技术指导

订阅技术指导,帮助您管理开发人员并构建更好的软件



开始于 600美元 每月
  • 您可以随时取消固定费率月度计划
  • 每个计划都包括无限制地接触我们的专家工程师
  • 通过共享渠道、现有通信工具或呼叫进行协作
  • 通常在工作日结束前作出响应
  • 尝试免费14天
浏览计划

“纯函数编程”中的“纯”一词意义重大。从这个意义上说,纯粹意味着我们编写的代码是纯粹的,或者没有副作用。另一个描述这一点的术语是参考透明度或属性,其中任何表达式(例如具有给定参数列表的函数调用)都可以替换为其返回值,而无需更改代码的功能。只有当这种纯函数没有副作用时,例如在主机系统上创建文件、运行数据库查询或发出HTTP请求,这才有可能实现。哈斯克尔的类型系统强加了这种纯度。

那么,纯粹意味着Haskell程序不会有副作用吗?当然不是,但这确实意味着影响被推到了我们系统的边缘。执行I/O操作(例如查询数据库或接收HTTP请求)的任何函数都必须具有捕获此操作的返回类型。这意味着我们在上一节中看到的类型签名(例如Int->浮点a->[a]->布尔)是指相应功能不会产生副作用的指标,因为浮动布尔只是原始返回类型。对于包含副作用的对比示例文件路径->IO字符串指示函数采用文件路径并执行返回字符串的I/O操作(这正是readFile(读取文件)函数)。

纯函数编程范式的另一个特征是高阶函数,即以函数作为参数的函数。最常用的高阶函数之一是功能性维修计划,它将函数应用于容器(例如列表)中的每个值。例如,我们可以应用名为广场,它获取一个整数并将该整数乘以自身返回到整数列表中,以将其转换为平方整数列表:

方形:: 国际 -> 国际
方形x=x*x

功能性维修计划正方形[1,2,,4,5]--返回[1,4,9,16,25]

以这种风格编写的代码往往既可组合又可测试。上面的例子很简单,但有许多高阶函数的应用。例如,我们可以编写如下函数renderPost(渲染后)它获取帖子数据的记录,并返回以HTML呈现的帖子版本。如果我们有帖子列表,我们可以运行fmap renderPost发布列表生成渲染帖子的列表。我们的renderPost(渲染后)函数既可以用于单个案例,也可以用于多个案例,而无需任何更改,因为使用功能性维修计划更改应用它的方式。我们还可以为renderPost(渲染后)功能和组合功能性维修计划在我们的测试中验证帖子列表的行为时。

Haskell有助于快速开发、无忧重构和出色的可维护性

通过将上述静态类型和Haskell的纯功能风格结合起来,用Haskells开发软件的速度往往非常快。我们使用的常见开发工作流之一是依赖于一个名为吉希德是一个简单的命令行工具,它依赖Haskell REPL自动监视代码的更改和增量重新编译。这使我们能够在将更改保存到文件后立即查看代码中的任何编译器错误。我们通常只在一个窗格中使用文本编辑器打开终端吉希德在Haskell中开发应用程序时。

虽然手动验证代码的结果最终是必要的,例如通过刷新浏览器中的页面或使用工具验证JSON端点,但很多这类操作可以推迟到编程会话结束。程序员在用Python或PHP之类的语言编写web服务时会遇到许多运行时错误,这些错误会被立即捕获并显示为编译器错误吉希德这与在更改某些代码后切换到浏览器窗口并刷新页面的需要相差甚远;开发工作流是每个开发过web应用程序的人都非常熟悉的。

除了开发期间的严格反馈循环外,Haskell代码很容易重构和修改。像用任何其他语言编写的真实世界代码一样,用Haskell编写的此类代码并不是只写的。它最终将需要维护、更新和扩展,通常由非代码原始作者的开发人员进行。借助编译时检查,Haskell中的许多代码重构变得很容易;常见的重构工作流是在一个位置进行所需的更改,然后一次修复一个编译器错误,直到程序再次编译。这比动态类型语言中的等效更改要容易得多,动态类型语言没有为程序员提供这样的帮助。

动态类型语言的支持者经常会争辩说,自动化测试取代了编译时类型检查的需要,也有助于防止错误。然而,测试没有类型约束那么强大。为了使测试有效,它们必须:

  1. 实际上是编写的,但许多现实世界的代码库都有有限的测试。
  2. 做出正确的断言。
  3. 全面(测试各种输入)并提供良好的覆盖率(测试大部分代码库)。
  4. 易于快速运行和完成,否则它们将不会成为开发工作流的一部分。
  5. 根据他们测试的代码进行更新和维护。

Haskell的类型系统没有上述问题。类型系统是语言中的一个固定装置,编译器总是验证类型是否正确。类型系统本质上是全面的,提供了Haskell代码的每一部分的完整覆盖,并且在底层代码更改时不需要对其进行任何更改。所有这些并不是说类型系统可以取代每一种类型的测试。但它所做的是提供比测试更全面的保证,并存在于每个代码库中,即使不存在测试。

Haskell程序具有卓越的性能,可实现更快的应用程序和更低的硬件成本

GHC是最常用的Haskell编译器,它生成的可执行文件速度极快,尤其是与常用于应用程序开发的其他语言(如PHP或Python)相比。这一改进的性能使应用程序响应速度更快,硬件成本更低。

当其他语言被描述为速度慢时,经常会听到其他语言的支持者不屑一顾,因为与雇佣程序员的成本相比,硬件成本相对较小。这可能是真的,但我们发现Haskell与用于web开发的其他语言之间的差异是惊人的。

在我们过去从事的一个项目中,我们开始在Haskell web服务中实现新的API端点,而不是现有的PHP。在Haskell中构建特性和添加端点大约一年后,PHP和HaskellWeb服务在请求计数和类型方面处理的平均工作负载相似,并执行由同一SQL数据库支持的类似CRUD操作。基础设施托管在AWS上,每个web服务使用的基础设施的细分如下。

Web服务语言 EC2实例类型 中央处理器 皇家音乐学院 每个实例的月成本 使用的实例数 每月总成本
菲律宾比索 c5.x大 4个专用CPU内核 8 GB $122 2 $244
哈斯克尔 t3.纳米 2个Flex CPU内核(使用率限制为20%) 0.5 GB $3.75 4 $15

在这个应用程序中,每个Haskell和PHP web服务在查询同一数据库的同时,处理的请求数量、工作负载和一天中的流量峰值都相似。PHP和Haskell web服务都使用Nginx作为反向代理。最后,运行Haskell基础设施的成本大约是PHP基础设施的1/16(或6%)。检查我们的AWS使用指标,Haskell机器上的CPU从未达到5%。Haskell端点的响应时间始终小于等于100ms,略优于PHP端点。

最终,我们有两个web服务,一个用Haskell编写,另一个用PHP编写,性能类似,但前者的成本为200美元/年,后者的成本为3000美元/年。值得注意的是,该应用程序的用户基数相对较小,每月活动用户(MAU)不足25000。这种成本差异将随着用户群规模、MAU数量和基础设施的增加而扩大。

当然有可能批评这种比较,我也不认为它是科学的。但我很清楚,根据我们过去运行生产工作负载的经验,Haskell至少比PHP强一个数量级(与许多其他类似语言相比,PHP 7.0+的性能非常好)。在其他网络语言上运行Haskell所带来的成本降低绝不是微不足道的。

Haskell非常适合域建模和防止域逻辑错误

除了简单的编译时类型检查之外,Haskell类型系统的另一个好处是,它可以通过在应用程序中使用自定义数据类型来建模问题域。这允许程序员创建由类型系统执行的业务逻辑规则的描述。Haskell具有所谓的代数数据类型(ADT),由记录(产品类型)和标记的联合(总和类型)组成。记录类似于字典或JSON对象,通常在许多语言中可用。然而,标记的并集在许多语言中都不可用,但它们在领域建模中具有很大的灵活性。

ADT的威力最好通过一个例子来说明。假设我们正在创建一个必须跟踪客户发票的发票系统。每张发票都必须包含发票所针对的行项目列表,并且具有指示订单是否已付款或已取消的发票状态。我们用于建模的类型可能如下所示:

类型 美元 = 国际

数据 客户发票 = 客户发票
{发票编号:: 国际
,到期金额: 美元
,税款: 美元
,计费项目:[字符串]
,状态:: 发票状态
,创建时间:: UTC时间
,到期日期: 
}

数据 发票状态
    = 发布
    | 支付
    | 取消

像这样在类型系统中建模域规则(例如,发票的状态为发布,支付,或取消)导致这些规则在编译时强制执行,如前面关于静态类型的部分所述。这是一组比在类方法中编码类似规则强得多的保证,就像在没有和类型的面向对象语言中一样。对于上述类型,无法定义客户发票例如,那没有到期金额。也不可能定义发票状态这不是上述三个值之一。

上述类型的一个应用程序可能是基于发票状态创建通知消息的函数。此函数需要客户发票作为参数,并返回表示通知内容的字符串。

创建客户通知:: 客户发票 -> 字符串
创建客户通知发票=
    案例状态发票属于
        发布 ->
            “发票编号” ++ 显示(invoiceNumber发票)++ “到期时间” ++ 显示(dueDate发票)

        支付 ->
            “已成功支付发票号” ++ 显示(发票编号发票)

        取消 ->
            “发票编号” ++ 显示(invoiceNumber发票)++ “已取消”

上述函数使用模式匹配(语言中的另一个特性)来处理所有可能的发票状态值。这个案例语句允许我们处理地位字段。

类型系统可以防止我们在更改域规则时出错。假设在这个应用程序上线一段时间后,我们从用户那里得到反馈,我们需要能够退款。为了促进这一点,我们将更新我们的发票状态类型以包含已退款值构造函数:

数据 发票状态
    = 发布
    | 支付
    | 取消
    | 已退款

如果这是我们唯一更改的代码,那么在编译时,我们会得到以下错误:

CustomerInvoice.hs:(15,5)-(20,35):错误:[-Wincomplete-patterns,-Werror=incomplete-patterns]模式匹配不全面在一种情况下,备选方案:模式不匹配:退款|15 |案例状态发票|     ^^^^^^^^^^^^^^^^^^^^^^...

哎哟!看起来我们忘记更新创建客户通知函数来处理此新状态值。编译器抛出错误并告诉我们案例语句不处理已退款值作为其模式匹配的一部分。

通过在类型中对域进行建模,编译器帮助我们确保所有域逻辑都可以处理域*中的每个可能值。这可以保护我们在使用动态类型语言编写时免受未处理值这一常见错误的影响。在这种情况下,自动测试不能替代类型,因为引入新的可能值通常需要更新测试来断言新值是否可以处理,这并不能帮助我们避免这个问题&忘记更新业务逻辑的测试和忘记更新业务逻辑学一样容易。

Haskell拥有大量成熟、高质量的库

Haskell社区发布了大量高质量的生产级包,其中许多包已经保存了十年或更长时间。Haskell社区对每个功能类别中哪些包是好的选项达成了普遍共识(例如解码/编码JSON、解析XML、解码CSV、使用SQL数据库、HTML模板、websockets、使用Redis等)。在某些类别中,有一个单一的最佳选项,即事实上的标准。在其他类别中,根据开发人员愿意做出的设计决策或权衡,有几个可比的选项可供选择。

Haskell在其包存储库中有超过21000个包可用,黑客攻击以及在构建工具可以依赖的各种地方(如GitHub)发布的更多软件包。然而,与许多其他语言存储库中可用的软件包数量相比,这个数字相形见绌。截至本文发表之日,Ruby已经164000颗宝石出版。有PyPI上的282000个Python包.结束了npm上有130万个JavaScript包截至2020年4月。

这种差异导致了我听到的关于在生产中使用Haskell的保留意见之一:可用的Haskel包没有其他语言的那么多。我对此的回应是,在构建生产系统时,给定语言的可用包总数基本上是不相关的。

在构建生产系统时,决定使用哪个软件包决不是基于可用软件包的总数,而是基于哪个软件包信誉良好、用途广泛,以及其他因素,例如良好的文档以及给定的软件包是否仍在维护。简单地说,重要的是质量而不是数量,为此,Haskell社区在策划我前面描述的真实世界用例所需的包方面做得很好。

Haskell使编写并发程序变得容易

纯函数式语言的一个特点是,默认情况下,Haskell中的值是不可变的。这并不是说价值观永远不会改变,而是状态不会就地改变。例如,当函数将元素附加到列表中时,将返回一个新列表,旧列表使用的内存将由垃圾收集器释放。这种不变性的好处是简化了并发编程。在具有可变值的语言中,多个线程访问相同的值可能会导致竞争条件和死锁等问题。

由于Haskell中的值是不可变的,因此即使程序在多个线程上运行并访问共享内存,也不会出现此类问题。这也导致了围绕并发编程的更简单的心理模型。并发代码通常可以用与单线程代码相同的风格编写,函数在新线程上运行底层工作负载,只需包装单线程实现。

并发是Haskell程序员工具箱中的一个有用工具。在我们过去从事的项目中,我们已经完成了所有工作,从实现的websocket服务器开始,这些服务器作为服务于HTTP API的同一可执行文件的一部分运行,创建一个多线程工作机系统,其所需的开销远低于管理以有限并发支持的语言编写的工作机所需的单个Linux进程。

Haskell支持特定于领域的语言,从而提高表达能力并减少样板

Haskell的类型系统和语言特性使其成为编写编译器的常见选择。其中一个分支是Haskell库有时会使用特定领域的语言(DSL)以提高其可用性。与通用语言相反,DSL是一种小型语言,旨在非常适合表达特定应用程序或问题域的规则。

SQL是最广为人知、使用最广泛的DSL之一,它是用于查询存储在关系数据库系统中的数据的语言。与大多数语言不同,SQL是声明性的,而不是命令性的。这意味着SQL程序倾向于描述什么其执行结果应该是怎样应该实现这一结果。任何熟悉SQL的开发人员都可以想象,编写代码以检索以命令样式存储在表中的一系列行中的数据是多么麻烦。

Haskell中促进DSL的功能之一称为Template Haskell。这通常被库作者用来允许库的消费者使用一种表达性语法,以避免大量的样板。其中一个例子是持久库,最流行的SQL库之一。Persistent公开了一个DSL,该DSL使用所谓的PersistentEntitySyntax,允许库用户定义其数据库架构。下面是此语法的一个示例。


名称文本
年龄国际 也许 吧
博客帖子
标题文本
作者ID人员ID
发布日期UTC时间
博客帖子标签
标签文本
博客帖子Id博客帖子Id

上面的代码不是Haskell,如果您从未使用过Haskell's Persistent库,那么很可能您从未见过这种语法。然而,它的作用显而易见——它定义了三个表(,博客帖子、和博客帖子标签)以及其中的列。这段代码被Haskell程序所使用,并取代了编写大约150行Haskel代码来定义所有数据类型和访问器函数以处理这三个表中的数据的需要。

以上只是外部DSL的一个示例,它是使用自己语法的DSL。其他公开DSL的库包括用于Web服务器路由定义和HTML模板的库。一些库作者选择创建用Haskell语法编写的嵌入式特定域语言(eDSL)。这会产生一系列专门用于特定域的类型和函数。皮包骨是一个广泛使用的库的示例,该库公开了用于编写类型安全SQL查询的eDSL。

哈斯克尔有一个很大的社区,里面挤满了聪明友好的人

使用编程语言最重要的方面之一是社区。Haskell的社区规模很大,包括来自许多不同技术背景的各种各样的人。这包括编程语言研究人员,其中一些人自1990年Haskell成立以来一直在研究Haskells,其他编程语言的创造者,其编译器是用Haskelle编写的,自学成才的Haskell-爱好者,商业上使用Haskell的专业Haskell-grammers(我们Foxhound Systems属于这一类),以及许多渴望学习的学生。

Haskell社区非常欢迎初学者。虽然由于语言的深度和广度,它的学习曲线比许多其他语言的学习曲线更陡峭,但很容易提出问题,并找到任何真诚希望帮助他人学习该语言的人的帮助。

我们喜欢与Haskell社区进行交流的一些形式包括:

  • 这个Haskell subreddit公司它拥有60000多名读者,是reddit上最大的编程语言社区之一。
  • 这个函数编程松弛,它有许多专用于Haskell的频道(包括#哈斯克尔,#哈斯克尔·比金纳,#哈斯克尔工作、和#哈斯克尔掺杂).
  • Haskell邮件列表,例如哈斯克尔·卡夫,内容丰富,从图书馆公告到语言问答,再到志愿者机会
  • 这个#哈斯克尔Freenode IRC网络上的频道通常有1000多人连接,是Slack频道的一个很好的替代方案。
  • 这个哈斯克尔周报这是一份每周新闻稿,重点介绍了上周的博客帖子和其他公告。
  • 虽然不是传统社区哈斯克尔StackOverflow上的标记共有46000多个相关问题。通常能找到对特定主题或语言相关问题有很好概述的优秀答案。

这不是一份详尽的清单,没有必要通过每个论坛参与。但是,当有人寻求帮助或学习该语言时,使用上述任何论坛都是值得的。

结论

有很多原因可以解释为什么Haskell是我们构建生产软件系统的首选编程语言。重述一下这篇文章中的整个列表:

  • Haskell有一个强大的静态类型系统,可以防止错误并减少认知负荷。
  • Haskell支持编写可组合、可测试且具有可预测副作用的代码。
  • Haskell有助于快速开发、无忧重构和出色的可维护性。
  • Haskell程序具有卓越的性能,可以实现更快的应用程序和更低的硬件成本。
  • Haskell非常适合域建模和防止域逻辑中的错误。
  • Haskell拥有大量成熟、高质量的库。
  • Haskell使编写并发程序变得容易。
  • Haskell支持特定于域的语言,这有助于提高表达能力并减少样板文件。
  • 哈斯克尔有一个很大的社区,里面挤满了聪明友好的人。

正是这些原因的总和使Haskell成为一个令人信服的选择。Haskell能够实现快速开发、无忧重构、易于维护、提供出色的性能,并拥有成熟的生态系统。这些方面使它成为构建生产应用程序的最佳选择。


Christian Charukiewicz是Foxhound Systems的合作伙伴和首席软件工程师。在Foxhound Systems,我们使用Haskell创建快速可靠的定制软件。想找人帮助您构建新产品或将Haskell介绍给您自己的开发团队吗?联系我们info@foxhound.systems.