文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

这是为 JavaScript 和 Node 开发者准备的 PureScript 实践。

如果你关注了我的 Twitter,你应该知道我喜欢的是函数式编程风格,富有表现力的语言。

我将在近几天写写有关函数式编程和静态类型系统,以及为什么我认为这才是用户界面敏捷开发的未来。而现在,我要向你们介绍我最喜欢的函数式编程语言——PureScript。核心团队最近发布了新版本——0.9.1,现在正是构建稳健的生态系统的好时候。

第 1 段(可获 2 积分)

我假设这是目前 Node/JS 生态系统中的一个知识。我希望透过实现一个针对行业目标或类似的产品的方式来介绍这次的内容。我认为 PureScript 对于工程师团队和企业构建真实产品是有用的。

为什么不是 Elm? 见本文脚注!

什么是 PureScript?

PureScript 是一个语法和特性都非常像 Haskell 的语言,但是有一些值得注意的差别。它向下编译成具有良好可读性和高性能的 JavaScript 代码,还对外提供了丰富的功能接口(Foreign Function Interface)与其它的 JavaScript 库进行交互。编译器是由 Haskell 构建的,目前正用 Node 来构建编译器,也有 C 和 Erlang 构建的试验性的后台系统。它与 Browserify 和 Webpack 等工具兼容,所以可以在浏览器中运行。还有一个优势是不需要笨重的运行时实现。

第 2 段(可获 2 积分)

它的编程风格最大限度的使用了函数式编程的特性,严格限制状态和副作用,相比规范更优先考虑兼容性。你可以使用完整的类型系统和测试工具来保障代码的正确性,严格的控制有状态的代码。每个副作用都是由单一功能函数返回的状态序列,通过 Aff 类型可以让异步操作,如 Ajax 请求更加简洁明了。

配置初始环境!

在我本地的 PureScript 开发环境使用 npm script 以及 Babel 来支持 FFI 模块的编译。在开始之前,我们需要一个简单的项目脚手架,它包含了 `package.json`,`.babelrc` 和其它 Node/JS 需要的初始化文件。

第 3 段(可获 2 积分)

配置 npm

在开始之前,你需要安装 PureScript,构建工具 pulp, 一个格式化错误信息的工具—— purescript-psa 和 开发工具 pscid

$ npm install --save-dev purescript pulp purescript-psa pscid

这会将相关工具安装到你本地工程的 node_modules 目录中,可以通过 npm scripts 来使用这些工具。现在,让我们开始设置这些脚本(npm scripts)吧。打开 package.json 文件, 如果不存在 scripts,就添加这个字段。

"scripts": {
    "build": "pulp build"
},

第一个脚本很简单。它仅仅是调用了本地的 pulp 命令。我们在每个工程项目中分别管理这些依赖,所以并不需要全局上的 pulp 命令行工具。

第 4 段(可获 2 积分)

接着,我们需要运行测试用例。在 script 区块中添加如下代码:

    "test": "pulp test"

也很简单,不过有个小问题。通常会有许多用例是不通过测试的,这并不是坏事 —— 但 npm 在每一个用例失败后会显示许多的调试信息,这些对你来说是没有用的。在实际场景中,IDE 和 CI 系统可能会使用 npm test 命令,但在开发时对你是有帮助的,比如下面提到的 npm run dev 命令。

我是测试驱动的开发工程师,所以习惯在文件发生变更以后立即运行测试用例。这实际上是 pscid 工具提供的功能之一。设置如下命令就可以使用 pscid 功能:

第 5 段(可获 2 积分)
    "dev": "pscid --test --port 1701"

然后我使用 Babel 来解决 FFI 模块,配置如下:

    "build:babel": "babel src -s -d lib"

如果我确实使用了 FFI 模块,那我会在之前的 build 命令中添加 build:babel 子命令。

目前 PureScript 社区使用的包管理工具是 Bower。也许不久的将来会有变化,但现在我们通过 postinstall 命令来调用 bower 安装开发时的依赖的模块:

    "postinstall": "bower install"

目前我与其它 PureScript 开发者有异议的地方是,我认为单元测试是第一等开发工具,所以我在一个新的环境中会优先学习如何运行测试用例。我目前最喜欢的工具是 Bodil Stokke 提供的 purescript-test-unit。

第 6 段(可获 2 积分)
$ bower install --save-dev purescript-test-unit

设置编辑器

接下来,该配置你的编辑器。我使用的编辑器是 Atom,它对 PureScript 提供 IDE 级别的支持。如果你希望使用其它的编辑器,请参考 wiki 页面的 编辑器和工具支持

对于 Atom 来说,你需要安装以下的包:

$ apm install language-purescript ide-purescript

要开启 ide-purescript, 我们需要先通过 npm install -g purescript 在全局上安装 PureScript。我尝试配置项目工程中指定的版本号,但至今没有成功。

第 7 段(可获 2 积分)

为了统一你的 IDE 和 CLI 的编译过程,在 package.json 文件中创建一个新的 “scripts” 入口

    "build:json": "pulp build --include lib:test --no-psa --json-errors",

--include lib:test 在我进行测试的时候能在编辑器中显示提示信息。--json-errors 让 pulp 输出 json 格式的数据而不是格式化的文本。 --no-psa 在 json 模式下会 purescript-psa 是无效的,它会忽略掉这些信息。

最后,在 Atom 打开 ide-purescript 配置页面,设置 "Build command" 为 npm run build:json。确保 "Build on save" 和 "Use fast rebuild" 是勾选的就可以了。

第 8 段(可获 2 积分)

单元测试

是时候创建你的第一个 PureScript 文件了!文件名为 test/Main.purs。没错,不是 src/Main.purs —— 我们今天要创建的第一个 PureScript 文件是 test/Main.purs,因为这是一个测试驱动的开发工程师写的教程。 🤓

这个文件会包含你的第一个模块,Test.Main。当你执行 pulp test 时,会自动执行 main 函数。

包的导入导出

首先,我们定义自己的模块,导入一些依赖:

module Test.Main where

import Prelude  
import Test.Unit (suite, test)  
import Test.Unit.Assert (equal)  
import Test.Unit.Main (runTest)  
第 9 段(可获 2 积分)

默认情况下,你的module将会导入所有定义的。但可以使用 括号来限制:

module Test.Main (main) where  

下一步就是导入Prelude,它是一个 基本构建块的标准库。在它后面我没有使用括号,意味着会自动导入Prelude中所有定义。

接下来导入Test.Unit, 但我只导入了我使用到的函数。我喜欢显式的导入所需的,但我为了方便对Prelude我没有这样做。

第 10 段(可获 2 积分)

函数式应用

现在,让我们写一个  Hello World 示例:

main = runTest do  
  suite "Hello" do
    test "World!" do
      equal (1 + 1) 2

好吧,import语法还行,但上面的语法就让一个典型的前端人员抓狂了 😳 这是什么鬼啊? 没有分号, 没有大括号,没有参数的函数?

我将在后面谈到这是什么意思,但现在你需要知道PureScript是空格敏感的语言。万岁!在使用Python捣鼓Django的日子里我就曾搞错过。 do 块表示新的函数调用序列。 这是PureScript的控制单体结构的语法糖,因为测试运行是有状态的且跟踪测试是否通过。

第 11 段(可获 2 积分)

如果你想添加第二个测试,就不必缩进:

main = runTest do  
  suite "Hello" do
    test "World" do
      equal (1 + 1) 2
    test "Far out!" do
      equal (2 + 2) 4

测试本身实际上是一个函数调用:

      equal (1 + 1) 2

如果你使用JavaScript来转换上述代码,转换后大概是这样:

      equal(1 + 1, 2)

这个表达式的第一个单词是函数的名称: equal。 它是被定义在上面引入的Test.Unit.Assert 模块中的。函数与参数是由空格分隔的,参数之间也是用空格分隔的。因此在本例中,第一个参数是:(1 + 1)。这个参数被应用到函数中, 这里发生了一些神奇的事。

第 12 段(可获 2 积分)

PureScript自动扩展每一个函数,也就是每次应用一个函数的参数,它返回一个新的函数来接收下一个参数。如果没有下一个参数,它就执行该函数并返回结果。如果把它转换为JavaScript,大概就像这样:

const equal = result => expected => {  
  // ... here lies the implementation of the equal function
}

或者,转换为ES5的语法:

function equal(result) {  
  return function(expected) {
    // ... here lies the implementation of the equal function
  }
}
第 13 段(可获 2 积分)

现在还有最后一个问题。这个加号表示什么? 这是一个 用户定义 的操作符,在 Prelude 中创建。PureScript 为你提供了为一个函数定义一个操作符的能力。这里的 + 操作符就是 add 函数的一个别名。

好了,保存你的文件。编译是否有问题呢? 在 Atom 中,你很可能看到一个黄色高亮的警告,提示你需要在 main 函数添加类型声明。类型的问题我们现在还不需要太担心。 PureScript 有一个丰富的类型系统,提供了大量可用的选项(添加注释通常是最佳实践)。如果你发现其它的错误,先确保你已经执行过 npm install,并进行到了下一步。在CLI 中通过 purescript-psa 格式化的信息会更容易阅读,我们的测试用例会将格式化的结果显示出来。

第 14 段(可获 2 积分)

下一步,在命令行中执行 pulp test 来运行你的测试代码。 输出的内容看起来应该是这样的:

purescript-test-unit

有吸引力的社区

如果你的测试代码运行的不顺畅,而你也没有找出问题的原因, PureScript 有一个非常优秀而活跃的社区!我经常在 Freenode(我推荐免费的 IRCCloud) 的 #purescript 主题下看到 主要的开发者在发表观点。GitterSlack 的聊天室里一样很活跃。我经常在三个地方逛来着。

构建自己的模块

现在,开始创建一个属于自己的模块吧。让我们做点实在的,比如一个 API。让我们安装 purescript-affjax,一个易用的 AJAX 库。

第 15 段(可获 2 积分)
$ bower install --save purescript-affjax

创建 src/Main.purs 文件:

module Main where

import Prelude  
import Control.Monad.Aff.Console (log)  
import Control.Monad.Aff (launchAff)  
import Network.HTTP.Affjax (get)

main = launchAff do  
  res <- get "http://jsonplaceholder.typicode.com/todos"
  log $ "GET /api response: " <> res.response

src/Main.purs 模块中的 main 函数就是在你执行 pulp run 命令时调用的函数。

json output

解析一个 AJAX 调用

让我们把 main 函数拆开来看看到底发生了什么。首先,你要调用 launchAff。我们可以在这里查看源码,可以看到这个函数获取 Affyielding 的内容,然后返回 Eff 的取消器(Canceler)。意思就是—— launchAff 将计算转化为同步的 Eff 并执行它。这里不需要指定成功的回调函数,do 代码块的返回值会被忽略,而执行完成以后,取消器函数将成为新的值返回。如果你需要执行一个异步效果,调用这个函数就好了。你不需要处理 throw 出来的错误,你也不需要关心 Aff 返回的结果。这很符合我们需要的情况,我们仅仅是要记录结果而已。(这里好像有点问题。。)

第 16 段(可获 2 积分)

这样我们写了一个 do 区块,在这个例子中我们创建了一个 Aff 函数体,编译器会检测到你在这个区块中使用了 get,推断出这是 Aff 类型。

do 区块序列中的第一个申明调用了 get 函数,你可以在这里看到相关源码。箭头左侧是 do 函数体的一部分,指定 get 函数的返回值赋值给本地变量 res。

第二个申明使用 log 函数,将响应的内容打印到控制台中。 <> 符号是 Prelude 中 append 函数的别名。它被作为连接符,接受左边的值作为 append 函数的第一个参数,它右边的值作为第二个参数,等同于如下代码:

第 17 段(可获 2 积分)
append "GET /api response: " res.response  

在 JavaScript 语法中类似这样:

append('GET /api response: ', res.response)  

如果你想用常规的方法,而不是自定义符号的话,只需要将其包在引号中即可:

"GET /api response: " `append` res.response

那 log 后面的$ 符号又是什么意思呢?这是让你避免写 括号 的语法糖。如果去掉 $ 符号,效果如下:

log ("GET /api response: " <> res.response)  

$ 符号本质上意味着, “优先计算右边的表达式,然后将结果传到左边”。

第 18 段(可获 2 积分)

理论的时间

好了,让我们先把今天的练习放一下,补充一些后续场景的详细内容。先说说类型系统吧!

那么,什么是类型系统呢?

类型是一种在代码中标注具体的结构,用于编译器对相关的代码进行静态解析的方式。听起来类型系统允许开发者严格的控制运行时错误,或在某些情况下避免这些错误!通过深入的静态分析可以有效的提高开发效率。它们在重构中能帮助我们减小改动带来的编译器报错,使一些强大的技术得以实现,比如模式匹配和类型约束。也许比这些更重要,这些优秀的特性就是由丰富的类型系统提供的。如果你投入时间去学习这些概念,通常情况下,通过它们的类型就能了解许多信息。

第 19 段(可获 2 积分)

PureScript 中的类型系统使得基于范畴理论,注重组合(compositional programming)的抽象编程成为可能。范畴理论的行为约束是受数学定律支配的。这些依循定律的约束让我们可以推断出关键信息。类似函数的设计模式等概念,这些策略强调兼容性更多于标准化,支持强大的互动性。

这是一个深入的主题,需要加以研究和实验才能完全搞懂,我不打算装作我已经完成了学习的旅程。我推荐大家用 PureScript 开始试验和深入的学习,就能慢慢的理解了。函数式编程经常被称为“由直觉构建”,因为内在的理解比起表面的理解要强大太多了。

第 20 段(可获 2 积分)

副作用呢?

要了解副作用,你必须首先理解纯度。 一个纯函数将每个可能的输入参数映射到相同的返回值,无论函数何时被调用,并对函数外的任何东西都没有显著影响。 你可以把它作为无状态的函数,没有“之前”和“之后”的概念。没有外部变量像“this”,“window”、“console”,全局变量、或一个外部闭包的变量可以在该函数内使用,因为那将意味着函数依赖或影响其作用域外的变量,而且时间也将是一个影响因素。

第 21 段(可获 2 积分)

如果你需要时间成为一个因素呢? 如果你需要某个状态,或者某件事情发生在一个特定的顺序呢? 在PureScript中有许多通过副作用处理状态的技术,例如单体和应用函数。 “do”函数的神秘背后的真理是,PureScript使用它来表示单体变换,它发生在序列中,因此需要副作用是有状态的。 这可能是PureScript内部的副作用,也可能是在运行时环境中的外部副作用。 如果要读取或影响本机环境(JavaScript运行时)中的某些内容,则使用“Eff”类型。 与这种类型的交互通过monadic接口来处理,所以你可以使用'do'表示法。

第 22 段(可获 2 积分)

所有这一切中的不变性因素呢? 在JavaScript中,非基本参数是通过引用传递的。 这意味着,变异参数导致函数外的值的变化,使其不纯。您需要用“Eff”来处理副作用。 您可以使用monads之类的东西来表示完全封装在函数中的值的突变。

在哪里可以找到更多的信息?

那么,这是今天! 我们已经涵盖了很多内容,如果我做得够好,那么比起你的答案来你可能有更多的问题。 我们鼓励你使用ML编程风格开始一段旅程,即使你不选择PureScript,你也可以看到该方法的优点。

第 23 段(可获 2 积分)

如果你想知道更多,可以从一些好的地方开始:

祝你好运!

脚注:Elm怎么样?

你可能想知道为什么我现在没有写Elm。 Elm现在在React社区反响强烈,这有充分的理由! 它向我们许多从未想过还能以这种方式的前端工程师引入了强烈的,富于表现的输入和ML风格语法的乐趣。 我个人对Elm感到非常兴奋,希望看到它的增长,但它现在不是我最喜欢的语言,因为它非常专注于作为一个非常适合新开发者的UI语言。 这绝对是Elm的焦点,但我正在寻找一些更通用的东西。 具体来说,我更喜欢PureScript,因为它的通过类型类的自组织多态性,Eff效应系统,行多态性,以及它对JavaScript的低级外部函数接口。

第 24 段(可获 2 积分)

文章评论