榆树爱好者的 Haskell

2021-08-06 09:00:27

许多年前,NRI 采用 Elm 作为前端语言。我们从一次性的概念验证开始,随着工程团队越来越多地接受 Elm 比 JavaScript 更好的开发人员体验,我们的前端开发越来越多地发生在 Elm 中。今天,我们几乎所有的前端都是用 Elm 编写的。同时,在后端,我们使用 Ruby on Rails。 Rails 为我们提供了良好的服务,并支持了我们网站的惊人增长,无论是在它支持的功能方面,还是在使用它的学生和教师数量方面。但是我们开始错过一些让我们在 Elm 中如此高效的工具:像用于建模数据的自定义类型、类型检查器及其有用的错误消息或易于编写(快速)测试的工具。几年前,我们开始将 Haskell 视为一种替代后端语言,它可以为我们的后端带来一些我们在前端编写 Elm 时所体验到的好处。今天,我们后端代码的一些关键部分是用 Haskell 编写的。多年来,我们开发了 Haskell 的写作风格,它可以被描述为非常像 Elm(它也在不断变化!)。 Elm 是一种小型语言,具有丰富的错误信息、丰富的文档和强大的社区。这些共同使 Elm 成为最适合学习的编程语言之一。 ElmBridge 活动的参与者将在 5 小时内从对语言一无所知到使用 Elm 编写真正的应用程序。我们在 NoRedInk 有大量 Elm 代码,它支持一些非常棘手的 UI 工作。 Elm 可以很好地扩展到不断增长且日益复杂的代码库。编译器保持快速,我们不会对更改代码的能力失去信心。您可以在此处详细了解我们的 Elm 故事。 Haskell 有很多我们喜欢 Elm 的语言特性:自定义类型来帮助我们对数据建模。纯函数和显式副作用。编写没有运行时异常的代码(主要是)。在易学性方面,Haskell 做出了与 Elm 不同的权衡。该语言要大得多,尤其是在包含许多可以启用的可选语言功能时。是否要在代码中使用这些功能完全取决于您,但是如果您想使用 Haskell 的包、文档和操作方法,则需要了解其中的许多功能。 Haskell 的编译器错误通常不如 Elm 的有用。最后,我们已经阅读了许多 Haskell 书籍和博客文章,但没有发现任何东西可以让我们从不了解 Haskell 到在其中编写一个真正的应用程序,该应用程序几乎与 Elm 指南一样小而有效。

我们在 Elm 中习惯的许多细节也可以在 Haskell 中获得。但是 Haskell 有许多附加功能,我们使用的每一个功能都添加到 Elm 程序员需要学习的内容列表中。因此,我们采取了 Haskell 社区中许多人在我们之前走过的道路:将自己限制在语言的一个子集上。有多种编写 Haskell 的风格,每种风格都有自己的权衡。示例包括 Protolude、RIO、镜头生态系统等等。我们的方法不同之处在于受到 Elm 的强烈启发。那么我们受 Elm 启发的 Haskell 写作风格是什么样的呢?为了使我们的 Haskell 代码更像 Elm,我们最早的努力是将 Elm 标准库移植到 Haskell。我们已将此端口开源为名为 nri-prelude 的库。它包含与 Elm 模块对应的 Haskell 模块,用于处理字符串、列表、字典等。 nri-prelude 还包括一个 elm-test 的端口。它提供了编写单元测试和基本属性测试所需的一切。最后,它包含一个 GHC 插件,使 Haskell 的默认 Prelude(基本上是它的标准库)表现得像 Elm 的默认值。例如,它添加了一些模块(如 List)的隐式限定导入,类似于 Elm 所做的。 Elm 坚持支持前端应用程序的单一架构,恰如其分地称为 Elm 架构。它的一个优点是它强制分离应用程序逻辑(所有这些条件和循环)和效果(比如与数据库交谈或获取当前时间)。我们喜欢使用 Elm 架构编写前端应用程序,但看不到将其 1:1 应用于后端开发的方法。在 F# 社区中,他们将 Elm 架构用于某些后端功能(请参阅:何时使用 Elmish Bridge),但它并不普遍适用。我们仍然希望鼓励应用程序逻辑和效果之间的分离,因为在我们的后端代码中已经看到了失去这种区别的一些影响。阅读我们的另一篇文章+☄️河豚,请扩大网站!如果你想阅读更多关于这个。在许多选项中,我们目前使用句柄模式来管理效果。对于每种类型的效果,我们创建了一个 Handler 类型(我们以打字错误的方式添加了额外的 r 并且它一直存在。抱歉)。我们在我们的库中使用这种模式与外部系统对话:nri-postgresql、nri-http、nri-redis 和 nri-kafka。

如果没有 Elm 架构,我们在很大程度上依赖于通过有状态任务类型链接排列。这感觉类似于命令式编码:首先,执行 A,然后执行 B,然后执行 C。希望在我们稍后的 Haskell 之旅中,我们会发现一个很好的架构来简化我们的后端代码。 Haskell 与 Elm 和 Rails 不同的一个方面是它不是特别固执己见。 Haskell 生态系统通常提供多种不同的方式来做一件特定的事情。因此,无论是编写 http 服务器、日志记录还是与数据库交谈,我们第一次做这些事情时都需要决定如何做。有时 Haskell 生态系统提供了一个适合我们 Elm 价值观的选项,比如句柄模式,所以我们选择了它。有时一个库有不同的值,然后选择不使用它也很容易。一个例子是镜头/棱镜生态系统,它允许编写超级简洁的代码,但几乎是一种必须首先学习的语言。最困难的决定是一种方法可以保护我们免于以某种方式犯错(我们喜欢),但需要熟悉更多语言功能才能使用(我们宁愿避免)。为了帮助我们做出更好的决定,我们经常尝试两种方式。也就是说,我们愿意构建一个带有或不带有复杂语言特性的软件,以确保复杂性的成本值得该特性给我们带来的好处。我们采取的另一种方法是在本地做出决策。一个团队可能会评估一个新功能,然后演示它,并在他们充分意识到该功能值得之后与其他团队分享。请记住:Haskell 的超能力是易于重构。与我们的 ruby​​ 代码不同,在我们的 Haskell 代码库中进行重大重写通常需要数小时或数天(而不是数周或数月)的努力。同时采用两种不同的模式成本相对较低!我们的方法在某些方面类似于 Elm 而在其他方面则不同的一个例子是我们如何与数据库对话。为此,我们使用了称为 quasiquoting 的 GHC 功能,它允许我们将 SQL 查询字符串直接嵌入到我们的 Haskell 代码中,如下所示:

{-# LANGUAGE QuasiQuotes #-}module Animals (listAll) whereimport Postgres (query, sql)listAll :: Postgres.Handler -> Task Text (List (Text, Text))listAll postgres = query postgres [sql|SELECT 物种,属FROM Animal|] 一个名为 postgresql-typed 的库可以针对真实的 Postgres 数据库测试这些查询,如果查询不适合数据,则会在编译时向我们显示错误。如果我们在查询中引用的表或列在数据库中不存在,则可能会发生这种编译时错误。通过这种方式,我们使用静态检查来消除一整类潜在的应用程序/数据库兼容性问题!缺点是编写这样的代码需要每个使用它的人学习一些关于准引号的知识,以及不同类型查询的期望返回类型。也就是说,使用某种查询库也有一个学习曲线,而且查询库往往非常大,可以支持所有不同类型的查询。另一个我们在选择网络服务器库时牺牲了额外的安全性和语言复杂性的例子。我们在这里使用了servant,这是一个允许您使用类型表达REST API 的库,如下所示: import Servantdata Routes route = Routes { listTodos :: route :- "todos" :> Get '\[JSON\] [Todo], updateTodo :: route :- "todos" :> Capture "id" Int :> ReqBody '[JSON] Todo :> Put '[JSON] NoContent, deleteTodo :: route :- "todos" :> Capture "id" Int :> Delete '[JSON] NoContent } deriving (Generic) Servant 是一个大型库,它使用了大量类型级编程技术,这在 Elm 中非常罕见,因此了解类型魔术的工作原理需要很高的学习成本.在没有深入理解的情况下使用它是相当简单的。使用 Servant 获得的好处超过了扩展复杂性的成本。基于上例中的类型,servant 生态系统可以生成其他语言(如 Elm 或 Ruby)的函数。使用这些功能意味着我们可以通过后端到前端或服务到服务的通信来节省时间。如果某些 Haskell 类型以向后不兼容的方式更改,我们将生成新的 Elm 代码,这可能会在 Elm 端引入编译器错误。

所以现在我们使用servant!重要的是要注意,我们想要的是编译时服务器/客户端兼容性检查,这就是我们吞下 Servant 复杂性的原因。如果我们可以在没有上面演示的类型级编程的情况下获得同样的好处,我们更愿意这样做。希望在未来,另一个库能够从更像 Elm 的 API 中提供同样的好处。我们正在生产中运行上面讨论的库。我们最常用的 Haskell 应用程序每分钟接收数十万个请求,并且几乎不会产生任何错误。代码可以在 NoRedInk/haskell-libraries 找到。库已发布到 hackage 和 stackage。我们很想知道您的想法!