使用 Clojure 编写 OpenWhisk 操作,第 3 部分: 改进您的 OpenWhisk Clojure 应用程序

栏目: 编程语言 · Clojure · 发布时间: 6年前

内容简介:使用 Clojure 编写 OpenWhisk 操作,第 3 部分通过开发一个库存控制系统来了解该如何做在前两篇教程中,您学习了如何使用 Clojure(一种基于 Lisp 的函数式编程语言)编写一个基本的 OpenWhisk 应用程序,从而为 OpenWhisk 应用程序创建操作。本教程是本系列的最后一部分,将展示如何改进任何这类应用程序。首先,您将学习如何支持包含双引号的参数。然后,我将展示如何使用永久型数据库 (Cloudant) 代替变量来存储信息。

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

改进您的 OpenWhisk Clojure 应用程序

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

使用 Clojure 编写 OpenWhisk 操作,第 3 部分: 改进您的 OpenWhisk Clojure 应用程序

Ori Pomerantz

2017 年 12 月 19 日发布

系列内容:

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

https://www.ibm.com/developerworks/cn/library/?series_title_by=**auto**

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

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

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

在前两篇教程中,您学习了如何使用 Clojure(一种基于 Lisp 的函数式编程语言)编写一个基本的 OpenWhisk 应用程序,从而为 OpenWhisk 应用程序创建操作。本教程是本系列的最后一部分,将展示如何改进任何这类应用程序。首先,您将学习如何支持包含双引号的参数。然后,我将展示如何使用永久型数据库 (Cloudant) 代替变量来存储信息。

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

本教程以“使用 Clojure 编写 OpenWhisk 操作”系列的前两篇教程( 第 1 部分:使用 Lisp 方言为 OpenWhisk 编写简明的代码第 2 部分:将 Clojure OpenWhisk 操作连接为有用的序列 )中的信息为基础,所以建议您先阅读这两篇教程。此外,您将需要:

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

运行应用程序 获取代码

函数式编程没有副作用,将它们与业务逻辑分离。这样做可以得到更加模块化、更容易测试、更容易调试的应用程序。

包含引号的参数

在第 1 部分中,我介绍了 main.js JavaScript:

// 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;

该脚本使用了一个高度简化的解决方案向 Clojure 提供参数:

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

此解决方案可生成类似这样的字符串: {\"num\": 5, \"str\": \"whatever\"} 。双反斜杠 ( \\ ) 被转换为单反斜杠(单反斜杠是转义字符)。得到的 Clojure 代码为 (js* " {\"num\": 5, \"str\": \"whatever\"}") 。因为 js* 将它获得的字符串参数计算为 JavaScript,所以这会让我们重新获得原始参数 {"num": 5, "str": "whatever"} 。问题在于,如果一个字符串参数已经包含一个双引号 ( " ),按处理引号的方式处理它,就会得到一个类似 {"num": 5, "str": "what"ever"} 的表达式和语法错误。从理论上讲,要解决此问题,可以使用 js/<var name> 语法访问一个包含这些参数的变量,但出于某种原因,这在 OpenWhisk 中行不通。

在第 3 部分中,我将介绍 fixHash 函数,它迭代各个参数(包括任何嵌套数据结构)并查找字符串。在一个字符串中,它将所有双引号替换为 \\x22 。第一个反斜杠将第二个反斜杠转义,所以真实值为 \x22 。此值最终被转换为 ASCII 字符 0x22(十进制数 34,也就是双引号),但此过程到后面阶段才会执行,所以 replace 方法不会修改这些字符。

// Fix a hash table so it won't have internal double quotes
var fixHash = function(hash) {
	if (typeof hash === "object") {
		if (hash === null) return null;
		if (hash instanceof Array) {
			for (var i=0; i<hash.length; i++)
				hash[i] = fixHash(hash[i]);
			return hash;
		}
		
		var keys = Object.keys(hash) 
		for (var i=0; i<keys.length; i++)
			hash[keys[i]] = fixHash(hash[keys[i]]);
		return hash;
	}

	if (typeof hash === "string") {
		return hash.replace(/"/g, '\\x22');
	}

	return hash;
		
};

保存到数据库

一个几分钟不用就会重置为初始值的库存管理系统不是很有用。因此,下一步是设置一个对象存储实例来存储数据库值( inventory_dbase 操作中的 dbase 变量):

  1. 在 IBM Cloud 控制台中,单击 Menu 图标并转到 Services > Data & Analytics
  2. 单击 Create Data & Analytics service ,并选择 Cloudant NoSQL DB
  3. 将服务命名为“OpenWhisk-Inventory-Storage”并单击 Create
  4. 创建该服务后,打开它并单击 Service credentials > New credential
  5. 将新凭证命名为“Inventory-App”并单击 Add
  6. 单击 View credentials 并将该凭证复制到一个文本文件中。
  7. 选择 Manage > Launch
  8. 单击数据库图标和 Create Database
  9. 将该数据库命名为“openwhisk_inventory”。
  10. 单击 All Documents 行中的加号图标并选择 New Doc
  11. dbase.json 的内容复制到文本区并单击 Create Document

可通过两种方式将 Cloudant 数据库与应用程序相结合。可以将 Cloudant 操作添加到序列中,或者修改 inventory_dbase 操作。我选择了第二种解决方案,因为它允许我更改单个操作(因为所有数据库工作都集中在这里)。

  1. 用于 Cloudant 的 npm 库 添加到 package.json 中的依赖项中,并更新 inventory_dbase 操作的 action.cljs 文件:
    (ns action.core)
    
    (def cloudant-fun (js/require "cloudant"))
    
    (def cloudant (cloudant-fun "url goes here"))
    
    (def mydb (.use (aget cloudant "db") "openwhisk_inventory"))
    
    
    
    ; Process an action with its parameters and the existing database
    ; return the result of the action
    (defn processDB [action dbase data]
      (case action
        "getAll" {"data" dbase}
    
        "getAvailable" {"data" (into {} (filter #(> (nth % 1) 0) dbase))}
    
        "processCorrection" (do
          (def dbaseNew (into dbase data))
          {"data" dbaseNew}
        )
    
        "processPurchase" (do
          (def dbaseNew (merge-with #(- %1 %2) dbase data))
          {"data" dbaseNew}
        )
    
        "processReorder" (do
          (def dbaseNew (merge-with #(+ (- %1 0) (- %2 0)) dbase data))
          {"data" dbaseNew}
        )
    
        {"error" "Unknown action"}
      )   ;  end of case
    )   ; end of processDB
    
    
    
    (defn cljsMain [params] (
        let [
          cljParams (js->clj params)
          action (get cljParams "action")
          data (get cljParams "data")
          updateNeeded (or (= action "processReorder")
                           (= action "processPurchase")
                           (= action "processCorrection"))
        ]
    
        ; Because promise-resolve is here, it can reference
        ; action
        (defn promise-resolve [resolve param] (let
          [
            dbaseJS (aget param "dbase")
            dbaseOld (js->clj dbaseJS)
            result (processDB action dbaseOld data)
            rev (aget param "_rev")
          ]
            (if updateNeeded
              (.insert mydb (clj->js {"dbase" (get result "data"),
                                      "_id" "dbase",
                                      "_rev" rev})
                #(do (prn result) (prn (get result "data")) (resolve (clj->js result)))
              )
              (resolve (clj->js result))
            )
          )   ; end of let
        )   ; end of defn promise-resolve
    
    
        (defn promise-func [resolve reject]
          (.get mydb "dbase" #(promise-resolve resolve %2))
        )
    
        (js/Promise. promise-func)
      )   ; end of let
    )    ; end of cljsMain

让我们看看 action.cljs 的一些更重要的部分。

您需要获取数据库。在 JavaScript 中,可以采用以下方式进行编码:

var cloudant_fun = require("cloudant ");
var cloudant = cloudant_fun(<<<URL>>>);
var mydb = cloudant.db.use("openwhisk_inventory ");

相同代码的 Clojure 版本是类似的,但有一些区别。 首先, require 是一个 JavaScript 函数。要访问它,需要使用 js 名称空间来限定它(上面第 12 步的清单中的第 3 行):

(def cloudant-fun (js/require "cloudant"))

下一行(第 5 行)非常标准。URL 是来自数据库凭证的 URL 参数:

(def cloudant (cloudant-fun <<URL GOES HERE>>))

要从一个 JavaScript 对象获得一个成员,可以使用 aget 。要使用一个对象的方法,可以使用 (.<method> <object> <other parameters>) 。 参见第 7 行:

(def mydb (.use (aget cloudant "db") "openwhisk_inventory"))

读取和写入数据库都是异步操作。 这意味着不能简单地运行它们并将结果返回给调用方(OpenWhisk 系统)。而是需要返回一个 Promise 对象 。这个构造函数接受一个参数 — 一个函数,需要调用它来启动进程,我们需要该进程的结果。利用 Clojure 调用 JavaScript 对象构造函数的语法为 (js/<object name>. <parameters>) 。参见第 75 行:

(js/Promise. promise-func)

提供给 Promise 对象的构造函数的函数为 promise-func 。它接受两个参数。一个是在成功时调用的函数(一个参数,即该操作的结果)。另一个是在失败时调用的函数(也是一个参数,即 error 对象)。在本例中,该函数获取 dbase 文档,然后使用 success 函数和该文档来调用 promise-resolve 。该匿名函数的第一个参数 ( #(promise-resolve resolve %2) ) 是 error(如果有)。因为这是一个演示程序,为了简便起见,我们忽略了错误。参见第 71-73 行:

(defn promise-func [resolve reject]
      (.get mydb "dbase" #(promise-resolve resolve %2))
    )

promise-funcpromise-resolve 都是在 cljsMain 内定义的。因为 promise-resolve 需要 action 参数的值。通过在 cljsMain 内定义这些函数,您可以使用该局部变量,而无需在整个调用链中拖着这些函数。参见第 52-55 行:

(defn promise-resolve [resolve param] (let
      [
        dbaseJS (aget param "dbase")
        dbaseOld (js->clj dbaseJS)

获取数据或修改它的函数是 processDB 。此函数封装了第 1 部分中解释的功能。参见第 56 行:

result (processDB action dbaseOld data)

由于 Cloudant 用来确保状态一致性的算法,所以必须使用修订 ( _rev ) 来更新数据库。 读取一个文档时,您会获得当前的修订 ( _rev )。写入更新的版本时,必须向 Cloudant 提供您要更新的修订。在此期间,如果另一个进程更新了该文档,版本将会不匹配,更新将会失败。参见第 57 行:

rev (aget param "_rev")

如果数据已修改,则更新 Cloudant。参见第 59 行:

(if updateNeeded

为数据库提供新数据、文档名称和您更新的修订。参见第 60-62 行:

(.insert mydb (clj->js {"dbase" (get result "data"),
                                  "_id" "dbase",
                                  "_rev" rev})

完成更新后(我们假设更新已成功;这是一个教学样本,而不是生产代码),运行下面的函数。请注意添加调试打印输出的机制。使用 do 对多个表达式进行求值,执行任意次数的 prn 函数调用来输出您需要的信息,最后放入您实际想要的表达式。

在本例中,您调用了通过 Promise 对象获得的 resolve 函数。因为此函数是用 JavaScript 编写的,它需要接收一个 JavaScript 对象而不是 Clojure 对象,所以您使用了 clj->js 。参见第 63-64 行:

#(do (prn result) (prn (get result "data")) (resolve (clj->js result)))
          )

如果不需要更新 Cloudant,可以仅运行 resolve 函数。参见第 65-66 行:

(resolve (clj->js result))
        )

编译 Clojure 代码

目前,只要 Node.js 应用程序重新启动,我们就会将 Clojure 代码发送到 OpenWhisk,并在那里编译它。另一个选择是在本地将 Clojure 一次性编译为 JavaScript,并发送已编译好的版本。如果您感兴趣的话,可以在 这里 了解它的工作原理。但是,此方法不会显著提高性能。

在下面的屏幕截图中可以看到,尽管使用了编译后的 ClojureScript 代码,第一次调用该操作所花的时间仍比后续调用长得多。

使用 Clojure 编写 OpenWhisk 操作,第 3 部分: 改进您的 OpenWhisk Clojure 应用程序

使用 Clojure 编写 OpenWhisk 操作,第 3 部分: 改进您的 OpenWhisk Clojure 应用程序

缓慢的原因在于,实际编译对资源的使用不是很密集。创建 Clojure 环境是 Clojure 使用过程中的资源密集部分,而且即使 Clojure 代码本身已编译为 JavaScript,也需要这么做。编译器生成的 JavaScript 使用了一些大型的库。

结束语

这个介绍 Clojure 中的 OpenWhisk 操作的教程系列到此就结束了。希望我向您展示了使用函数式编程来实现函数即服务 (FaaS) 的一些优势。到最后,几乎每个应用程序都需要在某个时刻使用副作用,但函数式编程没有副作用,将它们与业务逻辑分离。这样做可以得到更加模块化、更容易测试、更容易调试的应用程序。通过将应用程序逻辑分离为操作和序列,很容易让应用程序的大型结构变得更清晰,并编写单元测试,将它们作为 REST 调用来运行。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Rationality for Mortals

Rationality for Mortals

Gerd Gigerenzer / Oxford University Press, USA / 2008-05-02 / USD 65.00

Gerd Gigerenzer's influential work examines the rationality of individuals not from the perspective of logic or probability, but from the point of view of adaptation to the real world of human behavio......一起来看看 《Rationality for Mortals》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具