はじめに
JavaScriptで関数型プログラミングをするために、二つのライブラリが必要。
- ramda:
compose
,map
などのツールセットを提供する - folktale: Maybe, Task などのモナドを提供する
本稿では、ramda, folktale を利用して実践的な JavaScriptアプリを作るための基礎を固める。ramda, folktale ともに fantasy-land spec を意識して実装されているため、相互運用が可能。
以下の章では、断りなく以下の import を利用する。
const R = require('ramda');
const Result = require('folktale/result');
const Task = require('folktale/concurrency/task');
#01. 例外が送出される同期関数
例として JSON.parse
を考えよう。例外を送出する関数は impure であるため、ほぼ全てのライブラリ関数の呼び出しは impure である。Result.try
を使って例外を捕捉して、Result
にする。Result
はfolktale のクラスで、 一般的には Either と呼ばれるモナドである。
Result.try(() => JSON.parse('{"one": 1}')); // Result.Ok({ value: { one: 1 } })
Result.try
は実行する関数に引数を渡せないし、即時に評価されてしまうので、以下のようにラップしてあげるのが良い。
const myparse = val => Result.try(() => JSON.parse(val));
myparse('{"one": 1}');
JSON.parse
は例外を出す以外は pure なのでこの処方でOKである。一方、例外以外の副作用がある場合、すなわち IO モナドを利用するような場合は、Task
を使うことになる。次の章で扱おう。
#02. 副作用のある同期関数
例として val => JSON.parse(val + Date.now())
を考えよう。引数に String を渡すと例外が返るし、Date.now()は副作用を持つので、例としては十分実践的だろう。
こういう場合、Task
モナドにする。Task は IO モナドおよび Promise の上位互換である。run()
がトリガとなって実行が始まる。
const impure = val => JSON.parse(val + Date.now());
const mydate = val => Task.task(r => r.resolve(Result.try(() => impure(val))));
mydate(0); // Task will be Result.OK Integer
mydate('a'); // Task will be Result.Error Error
上記の処方は、TaskにResultが埋まっているので扱いがめんどくさかったり、Result.Error の値が resolve されてしまうなど不満があるかもしれない。そんな時はこんな感じに、Result を Task にconvert してもよい。(こういったユーティリティ関数があってもいいのだが、見つけられていない)
const mydate2 = val => Task.task((r) => {
const result = Result.try(() => impure(val));
result.matchWith({
Ok: ({ value }) => { r.resolve(value); },
Error: ({ value }) => { r.reject(value); },
});
});
mydate2(0).run() // -> resolved Task Integer
mydate2('a').run() // -> rejected Task Error
以上をまとめる。同期的でimpureな関数 impure
を使いたいときは、以下のようにする。
const Task = require('folktale/concurrency/task');
const Result = require('folktale/result');
const syncFuncInvoker = func => val => Task.task((r) => {
const result = Result.try(() => func(val));
result.matchWith({
Ok: ({ value }) => { r.resolve(value); },
Error: ({ value }) => { r.reject(value); },
});
});
const impure = val => JSON.parse(val + Date.now());
const mydate = syncFuncInvoker(impure);
mydate(0).run()
mydate('a').run()
応用。複数のTaskモナドの結果を一つのTaskにまとめる(Sequenceの利用)。
const R = require('ramda');
const Task = require('folktale/concurrency/task');
const Result = require('folktale/result');
const syncFuncInvoker = func => val => Task.task((r) => {
const result = Result.try(() => func(val));
result.matchWith({
Ok: ({ value }) => { r.resolve(value); },
Error: ({ value }) => { r.reject(value); },
});
});
let state = {
mizuho: { balance: 1000 },
ufj: { balance: 500 },
};
// impure
const getBalanceFrom = bank => state[bank].balance;
const mizuho = syncFuncInvoker(getBalanceFrom)('mizuho');
const ufj = syncFuncInvoker(getBalanceFrom)('ufj');
const resona = syncFuncInvoker(getBalanceFrom)('resona');
R.sequence(Task.of, [mizuho, ufj]).map(R.sum).run().promise()
.then(console.log); //-> 1500
R.sequence(Task.of, [mizuho, ufj, resona]).map(R.sum).run().promise()
.then(console.log)
.catch(({ message }) => console.log(message));
// -> Cannot read property 'balance' of undefined
#03. コールバック型関数の呼び出し
コールバック型の関数は非同期に実行され、呼び出し元には何も返さず、コールバックの中で色々やるので impure な関数である。前章同様、Task
にしてあげればよい。folktale では、Node型のコールバック型関数、つまりコールバックの第一引数がerrであるような関数をTask
に変換するfromNodeBack
という関数があるのでそれを使う。
const nodebackFromTask = require('folktale/conversions/nodeback-to-task');
const sample = (str, cb) => cb(null, `${str} received`);
const genTask = nodebackFromTask(sample);
genTask('hello').run().promise().then(console.log);
setTimeout
などNode型でないコールバックの場合は自分で作ればいい。そんなに難しくはない。
const genTask2 = (val) => Task.task((r) => {
setTimeout(() => { r.resolve(`${val} received`); } ,1000);
});
genTask2('world').run().promise().then(console.log);
#04. Promise型関数の呼び出し
Task
は完全にPromiseを包含する。Promise型の関数呼び出しをTask
に変更するユーティリティがあるのでそれを使えばよい。
const promisedToTask = require('folktale/conversions/promised-to-task');
promisedToTask(() => Promise.resolve(10))().run().promise()
.then(console.log);
そのあとは?
Result や Task といったモナドにしてしまえばあとは普通に関数型にプログラムを作っていけばよい。