有很多关于JavaScript函数编程理论的好文章.有些甚至包含代码示例,显示命令式/面向对象编程和声明式/函数式编程之间的区别.但我发现,通过简单的JavaScript代码示例,没有一个能说明如何在web应用程序中处理副作用.现实世界中的任何应用程序都无法完全避免副作用(数据库调用、登录到控制台、保存到文件、绘制到屏幕等),我很难弄清楚在实践中是如何做到的.

有一些博客文章和S/O答案(比如这个:How to perform side-effects in pure functional programming?)涉及到在现实世界中处理副作用的主题,但它们通常并不简单,不包含代码示例或其他语言(Haskell、Scala等)中的代码示例.我还没有找到Node/JavaScript的.

所以考虑到以下使用MongoDB数据库的非常简单的示例Node/Express应用程序,必须实现哪些代码更改,以使这段代码完全反映当前的JavaScript函数式编程最佳实践.尤其是在处理数据库调用的路由/函数方面.我希望你的回答能帮助我和其他人更好地理解函数式编程的"避免副作用"概念在现实世界JavaScript中的实际apply.

/*app.js*/

const express = require('express')
const app = express()
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var greetingSchema = mongoose.Schema({
    greeting: String
});

var Greeting = mongoose.model('Greeting', greetingSchema);

app.get('/', function (req, res) {
  Greeting.find({greeting: 'Hello World!'}, function (err, greeting){
    res.send(greeting);
  });  
});

app.post('/', function (req, res) {
  Greeting.create({greeting: 'Wasssssssssssuuuuppppp'}, function (err, greeting){
  res.send(greeting);
  });      
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})

推荐答案

你将无法完全避免副作用,但你可以尽可能地最大限度地消除它们.

例如,Express框架本身就是必需的.运行像res.send()这样的函数完全是为了它们的副作用(大多数时候你甚至不关心它的返回值).

你能做的(除了对所有声明使用const、使用Immutable.js个数据 struct 、Ramda、将所有函数写成const fun = arg => expression;而不是const fun = (arg) => { statement; statement; };等)是对Express通常的工作方式进行一些抽象.

例如,您可以创建以req为参数的函数,并返回一个对象,该对象包含响应状态、头和要作为主体进行管道传输的流.从某种意义上说,这些函数可以是纯函数,它们的返回值仅取决于它们的参数(请求对象),但您仍然需要一些包装器来使用Express固有的命令式API实际发送响应.这可能不是小事,但可以做到.

作为一个例子,考虑将主体作为对象发送为JSON的函数:

const wrap = f => (req, res) => {
  const { status = 200, headers = {}, body = {} } = f(req);
  res.status(status).set(headers).json(body);
};

它可以用来创建如下路由处理程序:

app.get('/sum/:x/:y', wrap(req => ({
  headers: { 'Foo': 'Bar' },
  body: { result: +req.params.x + +req.params.y },
})));

使用一个函数返回一个没有副作用的表达式.

完整示例:

const app = require('express')();

const wrap = f => (req, res) => {
  const { status = 200, headers = {}, body = {} } = f(req);
  res.status(status).set(headers).json(body);
};

app.get('/sum/:x/:y', wrap(req => ({
  headers: { 'Foo': 'Bar' },
  body: { result: +req.params.x + +req.params.y },
})));

app.listen(4444);

测试响应:

$ curl localhost:4444/sum/2/4 -v
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 4444 (#0)
> GET /sum/2/4 HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:4444
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Foo: Bar
< Content-Type: application/json; charset=utf-8
< Content-Length: 12
< ETag: W/"c-Up02vIPchuYz06aaEYNjufz5tpQ"
< Date: Wed, 19 Jul 2017 15:14:37 GMT
< Connection: keep-alive
< 
* Connection #0 to host localhost left intact
{"result":6}

当然,这只是一个基本的 idea .您可以让wrap()函数接受异步操作函数返回值的promise ,但这样一来,它就不那么没有副作用了:

const wrap = f => async (req, res) => {
  const { status = 200, headers = {}, body = {} } = await f(req);
  res.status(status).set(headers).json(body);
};

还有一个处理者:

const delay = (t, v) => new Promise(resolve => setTimeout(() => resolve(v), t));

app.get('/sum/:x/:y', wrap(req =>
  delay(1000, +req.params.x + +req.params.y).then(result => ({
    headers: { 'Foo': 'Bar' },
    body: { result },
  }))));

我在处理程序中使用了.then()而不是async/await,以使其看起来更具功能性,但它可以写成:

app.get('/sum/:x/:y', wrap(async req => ({
  headers: { 'Foo': 'Bar' },
  body: { result: await delay(1000, +req.params.x + +req.params.y) },
})));

如果作为wrap参数的函数是一个生成器,而不是像基于生成器的协同程序通常那样只产生解析的promise ,它将产生解析的promise 或流的卡盘,并通过一些包装来区分这两者,那么它可以变得更具普适性.这只是一个基本概念,但可以进一步扩展.

Mongodb相关问答推荐

$mod只支持数字类型,不支持MongoDb中的array和int

使用MongoDB 4将根文档替换为数组

Mongo Aggregate管道-另一个集合中的多个查找

如何将交易列表变成 token 数量的对象?

使用 $addFields 将字段添加到 $lookup 结果中的每个项目

根据状态过滤 mongoDb 中的结果

MongoDB 查询以包含多个字段的最常见值的计数

MongoDB聚合多条件

更新 MongoDB 中嵌套实体数组中的属性

使用 Spring Boot >= 2.0.1.RELEASE 将 ZonedDateTime 保存到 MongoDB 时出现 CodecConfigurationException

mongodb上不区分大小写的查询

如何使用 -number 后缀对字符串进行 MongoDB 查询排序?

NodeJS中的密码重置

如何在mongodb中删除数组的第n个元素

无法从 MongoDb C# 中的 BsonType ObjectId 反序列化字符串

MongoDB - 聚合 - 获取数组中的唯一项

Mongodb KeyFile 太开放权限

Mongodb错误:The positional operator did not find the match needed from the query

更新时提示Field name duplication not allowed with modifiers

如何从mongoose中的对象 ID 获取创建日期?