副作用を完全に回避することはできませんが、可能な場合は副作用を最大限に抽象化するように努力することができます。
たとえば、Expressフレームワークは本質的に必須です。 res.send()
のような関数を実行します 完全にそれらの副作用のためです(ほとんどの場合、その戻り値についてさえ気にしません)。
何ができるか(const
の使用に加えて) すべての宣言に対して、Immutable.js
を使用します データ構造、 Ramda
、すべての関数をconst fun = arg => expression;
として記述します。 const fun = (arg) => { statement; statement; };
など)は、Expressが通常どのように機能するかを少し抽象化することです。
たとえば、req
を取る関数を作成できます パラメータとして、応答ステータス、ヘッダー、および本文としてパイプされるストリームを含むオブジェクトを返します。これらの関数は、戻り値が引数(要求オブジェクト)のみに依存するという意味で純粋関数である可能性がありますが、Expressの本質的に必須のAPIを使用して実際に応答を送信するには、ラッパーが必要です。些細なことではないかもしれませんが、それは可能です。
例として、jsonとして送信するオブジェクトとしてbodyを受け取るこの関数を考えてみましょう:
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}
もちろん、これは単なる基本的な考え方です。 wrap()
を作成できます function acceptは、非同期操作の関数の戻り値の約束を受け入れますが、その場合、おそらくそれほど副作用がないわけではありません:
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
の引数である関数があれば、さらにユニバーサルにすることができます。 (ジェネレーターベースのコルーチンが通常行うように)解決する約束だけを生成する代わりに、解決する約束またはストリームするチャックのいずれかを生成し、2つを区別するためにいくつかのラッピングを行うジェネレーターになります。これは単なる基本的な考え方ですが、さらに拡張することができます。