对函数式编程感兴趣? 作为服务的功能(FaaS)如何? 在本教程中,您将通过在Clojure中编写OpenWhisk操作来学习将这两者结合在一起。 这样的动作比用JavaScript编写的动作更清晰,更简洁。 同样,函数式编程是FaaS的更好范例,因为它鼓励编程而不依赖副作用。
这是三个教程系列中的第一篇,这些教程通过开发库存控制系统来说明Clojure和OpenWhisk。 在第1部分中,您将学习如何使用Clojure通过Node.js运行时和ClojureScript包来编写OpenWhisk操作。 第2部分将教您如何使用OpenWhisk序列将动作组合为有用的块,这些块可以完成应用程序的工作。 第3部分将向您展示如何与外部数据库交互,以及如何使用日志记录在OpenWhisk应用程序中调试自己的Clojure。
构建应用程序所需的条件
- OpenWhisk和JavaScript的基本知识(Clojure是可选的,本文介绍了您在需要时需要的内容)
- 一个Bluemix帐户( 在此处注册 )。
为什么这样
实际上,这实际上是两个独立的问题:
- 为什么要用Clojure编写OpenWhisk操作,而不是使用本机JavaScript?
- 如果要用Clojure编写,为什么要使用OpenWhisk?
让我们依次看一看...
为什么要用Clojure编写OpenWhisk操作,而不是使用本机JavaScript?
Clojure是Lisp的方言,并提供该语言的所有编程优势(例如不变性和宏)。 这需要一些时间来习惯,但是一旦完成,您就可以编写清晰,简洁的代码。 例如,此行用了450多个单词来在本文后面进行解释,任何了解Clojure的人都可以一眼理解它:
"getAvailable" {"data" (into {} (filter #(> (nth % 1) 0) dbase))}
如果要用Clojure编写,为什么要使用OpenWhisk?
FaaS平台(例如OpenWhisk)使构建高度模块化的系统变得很容易,该系统仅通过定义明确的接口进行通信。 这使得开发模块化的应用程序变得容易,而没有副作用。 同样,FaaS需要更少的资源,因此比拥有不断运行的应用程序便宜。
开发工具链
IBM并未正式推荐Clojure,并且OpenWhisk没有Clojure运行时。 我们将在OpenWhisk上运行Clojure的方式是使用ClojureScript包,该包将Clojure代码编译为JavaScript。 然后可以由Node.js运行时执行JavaScript。
使用Node.js运行时对动作进行编码的最常见方法是将所有内容放入一个文件中,并且具有一个主要函数来接收参数并返回结果。 这种方法很简单,但是您的代码仅限于使用OpenWhisk已经拥有的任何npm库。
另外,您可以使用package.json文件编写一个更完整的Node.js程序,将其放入zip文件中,然后上传。 这使您可以使用其他库,例如clojurescript-nodejs
。 有关更多详细信息,请阅读Raymond Camden 撰写的 “ 在OpenWhisk中创建压缩的动作 ”。
Windows应用商店包含一个Linux子系统,您可以直接从Windows运行它。 就个人而言,我更喜欢在Linux上安装工具链-这样,我就可以直接从Windows笔记本电脑上进行安装。 以下命令是在该环境中发出的。
- 安装npm(这可能是一个耗时的过程,因为它需要很多其他软件包):
sudo apt-get update sudo apt-get install npm
- 创建具有以下内容的package.json文件(可在GitHub上找到):
注意:当前软件包版本是0.0.8。 通过在package.json文件中指定版本,可以确保如果将来发布的版本不向后兼容,应用程序也不会损坏。{ "name": "openwhisk-clojure", "version": "1.0.0", "main": "main.js", "dependencies": { "clojurescript-nodejs": "0.0.8" } }
- 创建一个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;
- 创建一个action.cljs文件(在GitHub上可用 ):
(ns action.core) (defn cljsMain [params] {:a 2 :b 3 :params params} )
- 运行以下命令以安装依赖项:
npm install
- 安装zip程序。
sudo apt-get install zip
- 压缩操作所需的文件。
zip -r action.zip package.json main.js action.cljs node_modules
- 下载适用于Linux的wsk可执行文件 (此链接适用于64位版本)。 将其放在路径中的目录中,例如
clojurescrip/usr/local/bin
。sudo mv wsk /usr/local/bin
- 获取您的身份验证密钥,然后运行
wsk
命令登录。wsk property set --apihost openwhisk.ng.bluemix.net --auth <your key here>
- 上载操作(在这种情况下,将其命名为
test
)。wsk action create test action.zip --kind nodejs:6
- 转到Bluemix OpenWhisk UI,单击左侧栏上的Develop ,然后运行操作
test
。 响应应类似于以下屏幕截图:
注意:如果您在日志中查找该操作,它将表明您使用的是未声明的变量。 您可以放心地忽略该警告消息。
它是如何工作的?
我之前已经写过关于如何集成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) {
我们通过声明自己是action.core
命名空间的一部分来开始创建Clojure代码。
var clojure = "(ns action.core)\n ";
将参数导入Clojure有点复杂。 当更简单的解决方案失败时,我转向了这一解决方案,它将参数编码为JavaScript Object Notation(JSON)字符串。 JSON可以作为JavaScript表达式进行评估,可以使用ClojsreScript中的语法(js* <JavaScript expression>)
对其进行评估。 但是,JavaScript表达式是一个字符串, Clojure中的字符串用双引号(“)括起来,双引号(”)与JSON.stringify
使用的相同。因此,下一行确保参数字符串中的双引号被转义。请注意当参数值包含双引号时,这种简单的解决方案将失败;我计划在本系列的第三篇文章中展示一种更好的解决方案。
var paramsString = JSON.stringify(params);
paramsString = paramsString.replace(/"/g, '\\"');
此行添加了实际调用Clojure中的动作的代码。 在Clojure(及其祖先Lisp)中,函数调用不表示为常用function(param1, param2, ...)
,而是表示为(function param1 param2 ...)
。 从最里面的括号到最外面的括号,此代码首先获取参数字符串并将其解释为JavaScript表达式。 然后,它将使用该值调用函数cljsMain
。 的输出cljsMain
,一个Clojure的哈希表,然后被转换到使用JavaScript哈希表clj->js
。
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}
)
库存控制系统
本文的示例应用程序是一个库存控制系统。 它有两个前端-一个是可以减少库存的销售点,另一个是可以让经理购买替换物品或更正库存编号的重新订购系统。
“数据库”操作
要抽象数据库,请创建一个处理所有数据库交互的操作。 根据参数,此操作需要执行以下操作之一:
- getAvailable-获取可用物品(您有库存的物品)的列表,以及每个物品有多少。
- getAll-获取所有物品的列表,包括缺货的物品,以便重新排序。
- processPurchase —获取项目清单以及每个项目的购买数量,然后从库存中扣除。
- processReorder-获取重新排序的项目和金额的列表,并将其添加到库存中。
- processCorrection-获取物料清单和正确的数量(在实际盘点库存之后)。 此数量可能大于或小于数据库中当前的数量。
目前,该数据库将是一个哈希表,其中项目名称为键,而库存量为值。 请注意,每次重新启动操作过程时,都将重置此值。
- 在新目录(例如,…/ inventory / dbase_mockup)中,创建与测试操作相同的三个文件: package.json , main.js和action.cljs 。 前两个具有与测试操作相同的内容。 您可以在GitHub中找到第三个action.cljs。
- 运行以下命令以安装依赖项:
npm install
- 压缩动作:
zip -r action.zip package.json main.js action.cljs node_modules
- 上传动作:
wsk action create inventory_dbase action.zip --kind nodejs:6
- 使用测试输入运行操作以查看会发生什么:
输入项 预期结果 { "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哈希表(与cljs->js
使用的cljs->js
相反)。
cljParams (js->clj params)
其他两个符号action
和data
获得特定参数的值。 在哈希表中获取值的一种方法是函数(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(从C,C ++和Java继承它们)的switch...case
语句相同。
(case action
"getAll"
操作是最简单的,只需在参数"data"
下返回数据库即可。
"getAll" {"data" dbase}
下一步操作将寻找可用的物品,即您有库存的物品。 实现此功能的表达式并不特别复杂,但是它使用了几种特定于函数式编程的技术。
在命令式编程中,您告诉计算机该怎么做。 在函数式编程中,您告诉计算机您想要什么,然后让计算机找出如何执行此操作。 在这种情况下,您希望计算机为您提供项目数大于0的所有项目。
为此,您可以使用filter
功能。 此函数接收一个函数和一个列表,并仅返回参数函数为其返回真值的那些项(大多数值为真)。 当给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或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))}
其他三个操作将修改数据库。 但是,我希望他们返回新数据库。 为此,请使用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中编写单个动作。 如果您正在编写单页应用程序,那可能就足够了。 但是,要在OpenWhisk上的Clojure中编写整个应用程序,需要执行其他操作,这些操作会将作为操作正常输出的JSON转换为HTML,并将带有新信息的HTTP POST请求转换为JSON。 这是本系列下一个教程的主题。
翻译自: https://www.ibm.com/developerworks/cloud/library/cl-clojure-openwhisk1/index.html