简单的Node / Express应用程序,函数式编程方式(如何处理JavaScript中的副作用?) [英] Simple Node/Express app, the functional programming way (How to handle side-effects in JavaScript?)
问题描述
有博客文章和S / O答案(就像这样: 因此...给出以下非常简单的使用MongoDB数据库的Node / Express应用程序示例,必须实施哪些代码更改这段代码完全反映了当前JavaScript函数式编程的最佳实践。特别是当涉及处理数据库调用的路由/函数时。我希望你的回答能够帮助我和其他人更好地理解实际JavaScript中的避免副作用功能编程概念的实际应用。 您将无法完全避免副作用,但您可以尽一切努力最大限度地抽象他们在可能的情况下离开。 例如,Express框架固有地势在必行。你的副作用完全可以像 您可以使用 const .io / immutable-js /rel =noreferrer> Immutable.js 数据结构, Ramda ,将所有函数写成
/ * 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);
$ b $ app.get('/',function(req,res){
Greeting.find({greeting:'Hello World!'},function(err,greeting){
res.send(greeting);
});
});
$ b $ app.post('/',function(req,res){
Greeting.create({greeting:'Wasssssssssssuuuppppp'},function(err,greeting){
res.send(greeting);
});
});
$ b app.listen(3000,function(){
console.log('Example app listening on port 3000!')
})
res.send()
那样运行(你甚至不关心它的大部分时间返回值)。
const fun = arg =>表达式;
而不是 const fun =(arg)=> {statement; statement;};
req
作为参数,并返回一个包含响应状态,标题和要作为正文管道的流的对象。这些函数可能是纯函数,因为它们的返回值仅取决于它们的参数(请求对象),但您仍然需要一些包装来实际发送使用Express的固有命令API的响应。它可能不是微不足道的,但它可以做到。
作为一个例子,考虑这个函数将body作为一个对象发送为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);
};
$ b $ 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
*在DNS缓存中找不到主机名
*尝试127.0.0.1 ...
*已连接到本地主机(127.0.0.1)端口4444(#0)
> GET / sum / 2/4 HTTP / 1.1
> User-Agent:curl / 7.35.0
>主机:localhost:4444
>接受:* / *
>
< HTTP / 1.1 200 OK
< X-Powered-By:Express
< Foo:Bar
< Content-Type:application / json; charset = utf-8
<内容长度:12
< ETag:W /c-Up02vIPchuYz06aaEYNjufz5tpQ
<日期:2017年7月19日星期三15:14:37 GMT
<连接:保持活动
<
*连接#0到主机localhost保持完好
{result:6}
当然,这只是一个基本的想法。你可以使 wrap()
函数接受承诺函数作为异步操作函数的返回值,但是可以证明它不会如此副作用:
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));
$ b 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
在处理程序中使它看起来更加实用,但它可以写成:
$ p $
app.get('/ sum /:x /:y',wrap(async req =>({
headers :{'Foo':'Bar'},
body:{result:await delay(1000,+ req.params.x + req.params.y)},
})));
如果函数是的参数, wrap
将是一个生成器,它不仅仅产生解决的承诺(就像基于生成器的协程通常所做的那样),它会产生承诺解析或者流式传输,用一些包装来区分这两者。这只是一个基本的想法,但可以进一步扩展。
There are many good articles about the theory of functional programming in JavaScript. Some even contain code examples showing the difference between imperative/object-oriented programming and declarative/functional programming. But I have found none that show, with simple JavaScript code examples, how to handle side-effects in a web app. No real world application can entirely avoid side-effects (database calls, logging to console, saving to a file, drawing to the screen etc.) and I have a hard time figuring out how it is done in practice.
There are blog articles and S/O answers (like this one: How to perform side-effects in pure functional programming?) that touch on the subject of handling side-effects in the real world but they are usually far from simple, don't include code example or include code example in other languages (Haskell, Scala, etc.). I haven't found one for Node/JavaScript.
So... given the following very simple example Node/Express app with MongoDB database, what code changes must be implemented so that this piece of code fully reflect current JavaScript functional programming best practices. Especially when it comes to the routes/functions handling database calls. I'm hoping your answers will help me, and others, better understand the practical application of the 'avoiding side-effects' concept of Functional Programming in real-world JavaScript.
/*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!')
})
You will not be able to avoid side effects entirely but you can make some effort to maximally abstract them away where possible.
For example the Express framework is inherently imperative. You run functions like res.send()
entirely for their side effects (you don't even care about its return value most of the time).
What you could do (in addition to using const
for all your declarations, using Immutable.js data structures, Ramda, writing all functions as const fun = arg => expression;
instead of const fun = (arg) => { statement; statement; };
etc.) would be to make a little abstraction on how Express usually works.
For example you could create functions that take req
as parameter and return an object that contains response status, headers and a stream to be piped as body. Those functions could be pure functions in a sense that their return value depend only on their argument (the request object) but you would still need some wrapper to actually send the response using the inherently imperative API of Express. It may not be trivial but it can be done.
As an example consider this function that takes body as an object to send as json:
const wrap = f => (req, res) => {
const { status = 200, headers = {}, body = {} } = f(req);
res.status(status).set(headers).json(body);
};
It could be used to create route handlers like this:
app.get('/sum/:x/:y', wrap(req => ({
headers: { 'Foo': 'Bar' },
body: { result: +req.params.x + +req.params.y },
})));
using a function that returns a single expression with no side effects.
Complete example:
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);
Testing the response:
$ 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}
Of course this is just a basic idea. You could make the wrap()
function accept promises for the return value of the functions for async oprations, but then it will arguably not be so side-effect free:
const wrap = f => async (req, res) => {
const { status = 200, headers = {}, body = {} } = await f(req);
res.status(status).set(headers).json(body);
};
and a handler:
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 },
}))));
I used .then()
instead of async
/await
in the handler itself to make it look more functional, but it can be written as:
app.get('/sum/:x/:y', wrap(async req => ({
headers: { 'Foo': 'Bar' },
body: { result: await delay(1000, +req.params.x + +req.params.y) },
})));
It could be made even more universal if the function that is an argument to wrap
would be a generator that instead of yielding only promises to resolve (like the generator-based coroutines usually do) it would yield either promises to resolve or chucks to stream, with some wrapping to distinguish the two. This is just a basic idea but it can be extended much further.
这篇关于简单的Node / Express应用程序,函数式编程方式(如何处理JavaScript中的副作用?)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!