内容


使用 Clojure 编写 OpenWhisk 操作,第 1 部分

使用 Lisp 方言为 OpenWhisk 编写简明的代码

通过开发一个库存控制系统来了解该如何做

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 Clojure 编写 OpenWhisk 操作,第 1 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 Clojure 编写 OpenWhisk 操作,第 1 部分

敬请期待该系列的后续内容。

对函数式编程感兴趣?那么对函数即服务 (FaaS) 是否也很感兴趣?在本教程中,将通过用 Clojure 编写 OpenWhisk 操作来学习如何将这二者结合使用。这些操作比使用 JavaScript 编写的操作更加简明。另外,函数式编程是 FaaS 的一种更好模式,因为它鼓励编写没有意外结果的程序。

本教程是由 3 篇教程组成的教程系列的第一篇,将通过开发一个库存控制系统来演示 Clojure 和 OpenWhisk。在第 1 部分中,将学习如何通过 Node.js 运行时和 ClojureScript 包,使用 Clojure 来编写 OpenWhisk 操作。第 2 部分将介绍如何使用 OpenWhisk 序列,将操作组合到完成应用程序工作的有用的代码块中。第 3 部分将介绍如何连接外部数据库,以及如何使用日志来调试 OpenWhisk 应用程序中的 Clojure 代码。

构建您的应用程序需要做的准备工作

  • OpenWhisk 和 JavaScript 的基本知识(Clojure 是可选的,在您需要它时,本文会介绍您需要掌握的知识)
  • 一个 IBM Cloud 帐户(在此处注册

为什么这么做?

实际上,这是两个独立的问题:

  1. 为什么要用 Clojure 编写 OpenWhisk 操作,而不选择使用原生 JavaScript?
  2. 如果将用 Clojure 编写代码,为什么还要使用 OpenWhisk?

让我们依次看看每个问题……

为什么要用 Clojure 编写 OpenWhisk 操作,而不选择使用原生 JavaScript?

Clojure 是一种 Lisp 方言,它提供了该语言的所有编程优势(比如不可变性和宏命令)。熟悉它需要花一些时间,但是一旦您熟悉了它,就可以编写出简明的代码。例如,本文后面用了超过 450 个单词来解释下面这行代码,而任何人只要理解 Clojure,都能对它一目了然:

      "getAvailable" {"data" (into {} (filter #(> (nth % 1) 0) dbase))}

如果您打算用 Clojure 编写代码,为什么还要使用 OpenWhisk?

诸如 OpenWhisk 之类的 FaaS 平台,使得构建仅通过明确定义的接口进行通信的高度模块化的系统变得很容易。这使得开发模块化应用程序变得很容易,不会产生任何意外结果。另外,FaaS 需要的资源更少,因此比拥有一直运行的应用程序更经济。

开发工具链

IBM 没有正式推荐 Clojure,而且 OpenWhisk 没有 Clojure 运行时。我们将通过使用 ClojureScript 包在 OpenWhisk 上运行 Clojure,该包将 Clojure 代码编译为 JavaScript。然后,可通过 Node.js 运行时执行 JavaScript。

使用 Node.js 运行时编写操作的最常见方式是将所有内容都放在一个文件中,通过一个 main 函数接收参数并返回结果。此方法很简单,但您的代码仅能使用 OpenWhisk 已拥有的 npm 库。

或者,您可以编写一个包含 package.json 文件的更加完整的 Node.js 程序,将它压缩为 zip 文件,然后上传它。这使您能使用其他库,比如 clojurescript-nodejs。要获得更多细节,请阅读 Raymond Camden 编写的“在 OpenWhisk 中创建压缩操作”。

Windows 应用商店包含一个可直接从 Windows 运行的 Linux 子系统。就本人而言,我更喜欢将工具链安装在 Linux 上—这样,我就可以直接从我的 Windows 笔记本电脑上运行它。下面的命令是在该环境中发出的。

  1. 安装 npm(这可能是一个很耗时的过程,因为它需要其他大量的包):
    sudo apt-get update
    sudo apt-get install npm
  2. 创建一个包含此内容的 package.json 文件(可在 GitHub 上获得):
    {
      "name": "openwhisk-clojure",
      "version": "1.0.0",
      "main": "main.js",
      "dependencies": {
        "clojurescript-nodejs": "0.0.8"
      }
    }
    备注:目前的包版本为 0.0.8。通过在 package.json 文件中指定版本,可以确保如果在未来发布了一个不向后兼容的版本,不会破坏应用程序。
  3. 创建一个 main.js 文件(可在 GitHub 上获得):
    // Get a Clojure environment
    var cljs = require('clojurescript-nodejs');
    
    
    // Evaluate the action code
    cljs.evalfile(__dirname + "/action.cljs");
    
    
    // The main function, the one called when the action is invoked
    var main = function(params) {
    
      var clojure = "(ns action.core)\n ";
    
      var paramsString = JSON.stringify(params);
      paramsString = paramsString.replace(/"/g, '\\"');
    
      clojure += '(clj->js (cljsMain (js* "' + paramsString + '")))';
    
      var retVal = cljs.eval(clojure);
      return retVal;
    };
    
    exports.main = main;
  4. 创建一个 action.cljs 文件(可在 GitHub 上获得):
    (ns action.core)
    
    
    (defn cljsMain [params]
      {:a 2 :b 3 :params params}
    )
  5. 运行此命令来安装依赖项:
    npm install
  6. 安装压缩程序。
    sudo apt-get install zip
  7. 压缩该操作所需的文件。
    zip -r action.zip package.json main.js action.cljs node_modules
  8. 下载适用于 Linux 的 wsk 可执行程序(此链接适用于 64 位版本)。将它放在该路径中的一个目录中,例如 clojurescrip/usr/local/bin
    sudo mv wsk /usr/local/bin
  9. 获取您的验证密钥并运行 wsk 命令来登录。
    wsk property set --apihost openwhisk.ng.bluemix.net --auth <your key here>
  10. 上传该操作(在本例中,将它命名为 test)。
    wsk action create test action.zip --kind nodejs:6
  11. 转到 Bluemix OpenWhisk 用户界面,单击左边侧栏上的 Develop,然后运行操作 test。响应应类似于下面的屏幕截图: Bluemix OpenWhisk 测试响应
    Bluemix OpenWhisk 测试响应

备注:如果查看该操作的日志,它会告诉您,您使用了一个未声明的变量。可以安全地忽略这条警告消息。

它的工作原理是什么?

我之前写过一篇关于如何集成 Clojure 和 Node.js 的文章,所以这里只简单解释一下。如果想了解更多细节,可以随时查阅该文章。

查看桩代码 main.js,该代码首先创建一个 ClojureScript 环境(转换为 JavaScript 而不是 Java 的 Clojure),然后计算 action.cljs 文件。

// Get a Clojure environment
var cljs = require('clojurescript-nodejs');

// Evaluate the action code
cljs.evalfile(__dirname + "/action.cljs");

这种方法很简单,而且尽管它要求每次重新启动操作时都编译 Clojure,但这并没有听起来那么糟。执行一次初始化代码(不在 main 中或由 main 中的代码调用的代码),然后由 OpenWhisk 缓存结果。因此,仅在长时间未调用操作时才会重新编译 Clojure。

接下来是 main 函数。该函数是使用一个 JavaScript 哈希表中的参数来调用的。

// The main function, the one called when the action is invoked
var main = function(params) {

要开始创建 Clojure 代码,首先要将名称空间声明为 action.core

var clojure = "(ns action.core)\n ";

将参数导入 Clojure 中有点复杂。当比较简单的解决方案失效时,我采用了将参数编码为 JavaScript Object Notation (JSON) 字符串的方法。可以将 JSON 作为一个 JavaScript 表达式进行计算,可以使用语法 (js* <JavaScript expression>) 在 ClojureScript 中计算它。但是,JavaScript 表达式是一个字符串,而且 Clojure 中的字符串包含在双引号 (") 中JSON.stringify 也使用了双引号字符。因此,下一行将确保参数字符串中的双引号已转义。请注意,在参数值包含双引号时,这个简化的解决方案将会失效;我计划在本系列的第 3 篇文章中介绍一个更好的解决方案。

var paramsString = JSON.stringify(params);
paramsString = paramsString.replace(/"/g, '\\"');

这一行将添加用 Clojure 实际调用该操作的代码。在 Clojure(及其早期产品 Lisp)中,函数调用没有表达为常用的 function(param1, param2, ...),而是表达为 (function param1 param2 ...)。从最里层的括号到最外层,此代码首先接受参数字符串,并将它解释为 JavaScript 表达式。然后,它使用该值调用函数 cljsMain。接下来使用 clj->jscljsMain 的输出(一个 Clojure 哈希表)转换为一个 JavaScript 哈希表。

clojure += '(clj->js (cljsMain (js* "' + paramsString + '")))';

最后,调用 Clojure 代码并返回值:

  var retVal = cljs.eval(clojure);
  return retVal;
};

这一行导出 main 函数,以便可供运行时使用。

exports.main = main;

action.cljs 中的操作本身甚至更加简单。第一行声明名称空间 action.core。Clojure 源自 Java 虚拟机语言,该名称空间拥有 Java 中的类名称的一些功能。

(ns action.core)

此代码定义了 cljsMain 函数。一般而言,Clojure 函数是使用 (defn <function name> [<parameters>] <expression>) 定义的。 该表达式通常是一个函数调用,但不是必须如此。 在这里,它是一个文字表达式。Clojure 中的哈希表包含在花括号 ({}) 中。语法为 {<key1> <value1> <key2> <value2> ...}。 本例中的键为关键字,也就是以冒号 (:) 开头的单词,在 Clojure 中,这意味着它们不能是用于任何其他用途的符号。这个哈希表中的值包含两个数字和传递给操作的参数。

(defn cljsMain [params]
  {:a 2 :b 3 :params params}
)

库存控制系统

本文的样本应用程序是一个库存控制系统。它有两个前端 —一个是减少库存的销售点,另一个是让经理购买替换商品或更正库存数量的再订购系统。

“Database”操作

要抽象数据库,可以创建一个操作来处理所有数据库交互。基于参数,此操作需要执行以下操作之一:

  • getAvailable— 获取可用商品(有货的商品)列表,以及每种商品的拥有数量。
  • getAll— 获取要再订购的所有商品的列表,包括缺货商品。
  • processPurchase— 获取一个商品列表和其中每种商品的购买量,从库存中扣除它们。
  • processReorder— 获取再订购商品的列表和数量,将它们添加到库存中。
  • processCorrection— 获取一个商品列表和正确的数量(在亲自统计库存后)。该数量可能比数据库中的当前数量多或少。

就现在而言,该数据库将是一个哈希表,键为商品名称,值为库存数量。请注意,每次重新启动该操作,都会重置此值。

  1. 在新的目录(例如 …/inventory/dbase_mockup)中创建为 test 操作创建的相同 3 个文件:package.jsonmain.jsaction.cljs。前两个文件包含的内容与 test 操作中的同名文件相同。可以在 GitHub 中找到第 3 个 action.cljs 文件。
  2. 运行此命令来安装依赖项:
    npm install
  3. 压缩该操作:
    zip -r action.zip package.json main.js action.cljs node_modules
  4. 上传该操作:
    wsk action create inventory_dbase action.zip --kind nodejs:6
  5. 使用测试输入来运行该操作,查看产生的结果:
    输入预期结果
    {
        "action": "getAll"
    }
    {
      "data": {
        "T-shirt L": 50,
        "T-shirt XS": 0,
        "T-shirt M": 0,
        "T-shirt S": 12,
        "T-shirt XL": 10
      }
    }
    {
        "action": "getAvailable"
    }
    {
      "data": {
        "T-shirt L": 50,
        "T-shirt S": 12,
        "T-shirt XL": 10
      }
    }
    {
        "action": "processCorrection",
        "data": {"T-shirt L": 10, "Hat": 15}
    }
    {
      "data": {
        "T-shirt L": 10,
        "Hat": 15,
        "T-shirt XS": 0,
        "T-shirt M": 0,
        "T-shirt S": 12,
        "T-shirt XL": 10
      }
    {
        "action": "processPurchase",
        "data": {
            "T-shirt L": 5,
            "T-shirt S": 2 
        }
    }
    {
      "data": {
        "T-shirt L": 5,
        "Hat": 15,
        "T-shirt XS": 0,
        "T-shirt M": 0,
        "T-shirt S": 10,
        "T-shirt XL": 10
      }
    }
    {
        "action": "processReorder",
        "data": {
            "T-shirt L": 20,
            "T-shirt M": 30
        }
    }
    {
      "data": {
        "T-shirt L": 25,
        "Hat": 15,
        "T-shirt XS": 0,
        "T-shirt M": 30,
        "T-shirt S": 10,
        "T-shirt XL": 10
      }
    }

它的工作原理是什么?

本节将介绍一些 Clojure 概念。推荐打开一个浏览器选项卡访问 Clojure 命令行(称为 REPL,表示“读取、计算和输出循环”)来进行阅读,边学边做。

action.cljs 的第一行定义了名称空间:

(ns action.core)

接下来,我们使用 def 命令将 dbase 定义为一个哈希表。该语法类似于 JavaScript 中的语法,但有多处重要区别:

  • 键和值之间没有冒号 (:)。
  • 可以使用逗号 (,) 作为不同的键值对之间的分隔符 ({"a" 1, "b" 2, "c" 3})。但是,也可以省略分隔符,不更改表达式的值(所以 {"a" 1 "b" 2} 等同于 {"a" 1, "b" 2})。
  • 键不需要是字符串(这里看不到);它可以是任何合法值:
    (def dbase {
      "T-shirt XL" 10
      "T-shirt L" 50
      "T-shirt M" 0
      "T-shirt S" 12
      "T-shirt XS" 0
      }
    )

然后,使用 defn 定义函数 cljsMain。它接受一个参数,也就是一个包含参数的哈希表。由于 main.js 的编写方式,这是一个 JavaScript 哈希表,而不是 Clojure 哈希表。

(defn cljsMain [params] (

下一行使用了 let 函数。此函数获取一个矢量 —实质上是一个包含在方括号 ([]) 中的列表— 和一个表达式。该矢量中首先是标识符,随后是在 let 表达式执行期间分配给标识符的值。通过使用 let,可以采用与命令式编程类似的格式来编程。矢量内的代码可以使用 JavaScript 编写为:

	var cljParams = js→clj(params);
	var action = get(cljParams, "action");
	var data = get(cljParams, "data");

Clojure 代码中的开头一行是:

let [

我上面已经提到过,params 中的值是一个 JavaScript 哈希表。js->cljs 函数将它转换为 Clojure 哈希表(main.js 中使用了 cljs->js 的反向转换)。

cljParams (js->clj params)

其他两个符号 actiondata 获取特定参数的值。获取哈希表中的值的一种方法是使用函数 (get <hash table> <value>)。不是所有操作都有 data 参数,但这没有关系 — 在这些情况下,我们会获得 nil,而不是错误条件。

要查看实际效果,可在 REPL 网站上运行以下代码: (get {:a 1 :b 2 :c 3} :b)。请记住,以冒号开头的单词无法用作符号的关键字,所以不需要将它们作为字符串对待。

      action (get cljParams "action")
      data (get cljParams "data")
    ]

函数 case 的运作方式与 JavaScript 中的 switch...case 语句相同(继承自 C、C++ 和 Java 中的相同语句)。

    (case action

"getAll" 操作是最简单的操作,它返回参数 "data" 下的数据库。

      "getAll" {"data" dbase}

下一个操作将查找可用商品,也就是有货的商品。实现此操作的表达式不是特别复杂,但它使用了函数式编程中独有的多种技术。

在命令式编程中,您应该告诉计算机做什么。在函数式编程中,您应该告诉计算机您想要什么,让计算机决定该如何做。在本例中,您希望计算机提供数量大于 0 的所有商品。

为此,您使用了 filter 函数。此函数接收一个函数和一个列表,仅返回该函数返回的参数值为 true(大部分值都为 true)的商品。为 filter 提供一个哈希表时,它表现得就像它是一个有序键值对列表,每个对包含一个键和它的值。

#(<function>) 格式定义了一个函数(没有为它命名,所以它是一个匿名函数)。在该函数定义中,您使用百分比形式(%%1)引用该函数的唯一参数,如果有多个参数,则引用第一个参数。可以使用 %2%3 等引用其他参数。要从一个列表或矢量获取值,可以使用 nth 函数。此函数从 0 开始计数,所以列表中的第一个值为 (nth [<list>] 0),第二个值为 (nth [<list>] 1),依此类推。

可在 REPL 网站上运行 (nth [:a :b :c :d] 2),了解 nth 的工作原理。要查看匿名函数的实际运行效果,请运行 (#(+ 3 %) 3)。该匿名函数对它获得的值加 3,所以结果为 3 + 3 或 6。

函数 #(> (nth % 1) 0) 查找参数中的第二个值,检查它是否大于 0。由于 filter 处理哈希表的方式,该值始终是商品数量。出于本文的目的,应该仅关心该值为正值的情况。

此刻,结果是一个矢量列表,每个矢量包含两个值:产品名和库存数量。但是,期望输出是一个哈希表。要将这种格式的值添加到哈希表中,请使用 into 函数。此函数的第一个参数是您向其中添加值的初始哈希表,在本例中是一个空哈希表。

要在 REPL 网站上跟随操作,请运行 (filter #(= (nth % 1) 1) {:a 1 :b 0 :c 1 :d 2}) 来查看该列表。然后,运行 (into {} (filter #(= (nth % 1) 1) {:a 1 :b 0 :c 1 :d 2})) 来查看哈希表格式的该列表。

  "getAvailable" {"data" (into {} (filter #(> (nth % 1) 0) dbase))}

其他 3 个操作将修改该数据库。但是,我希望它们返回新的数据库。为此,可以使用 do 函数。此函数获取一些表达式,计算它们,然后返回最后一个结果。这支持存在意外结果的表达式,比如为 dbase 符号赋予一种新含义。

处理关联很容易。因为更正的值会替换现有值,所以可以使用 into 函数。它的行为与您预期的一样,在键相同时替换值。

      "processCorrection" (do
          (def dbase (into dbase data))
          {"data" dbase}
      )

处理购买和再订购更加困难,因为它同时依赖于 dbase 中的旧值和 data 中的新值。幸运的是,Clojure 提供了一个名为 merge-with 的函数,该函数接收一个函数和两个哈希表。如果一个键仅存在于一个哈希表中,则使用该值。如果一个键同时存在于两个哈希表中,它会运行该函数并使用该值。

要在 REPL 网站上跟随操作,请运行 (merge-with #(- %1 %2) {:a 1 :b 2 :c 3} {:b 3 :c 2 :d 4})

      "processPurchase" (do
          (def dbase (merge-with #(- %1 %2) dbase data))
          {"data" dbase}
      )
      "processReorder" (do
          (def dbase (merge-with #(+ %1 %2) dbase data))
          {"data" dbase}
      )

在所有值和表达式对后,可以放置一个默认值。在本例中,该默认值是一条错误消息。

      {"error" "Unknown action"}
    )
  )
)

结束语

在本教程中,学习了如何用 Clojure 编写一个操作,即一个模拟数据库。如果正在编写单页应用程序,这么做可能就足够了。但是,要使用 Clojure 在 OpenWhisk 上编写一个完整的应用程序,需要执行其他操作,将一个操作正常输出的 JSON 转换为 HTML,并将包含新信息的 HTTP POST 请求转换为 JSON。这是本系列中下一篇教程的主题。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing
ArticleID=1051946
ArticleTitle=使用 Clojure 编写 OpenWhisk 操作,第 1 部分: 使用 Lisp 方言为 OpenWhisk 编写简明的代码
publish-date=11132017