はじめに

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 といったモナドにしてしまえばあとは普通に関数型にプログラムを作っていけばよい。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.