JavaScript で関数型プログラミング実践

# 何これ
私はここ1,2 か月集中的に JavaScript (Node) + Ramda + Folktale で関数型プログラミングを勉強していて、そろそろ実践的なコードが書けそうだなと思ったので披露をするよ。**「関数型プログラミングって何よ？」**という質問に対しては、本稿では「以下のスタイルでコードを作成すること」とするよ。

- `pipe`, `compose` を使った関数合成で処理を構築するスタイル
    - 可読性を失わない範囲でポイントフリーを指向するスタイル
    - `map`, `reduce` 等を使って制御構造を削減するスタイル
- モナドを使うスタイル
    - 関数を純粋に構成するスタイル
    - `lift`, `sequence`, `traverse`, `chain/flatMap`, `map` を活用するスタイル

質問あればコメントしてね。

# 作るもの

## 仕様


以下のように資産クラスが与えられた際に、実行時の資産総額を出力するプログラム。VISA と JT と テンセントの株を持っていて、銀行預金として、みずほとりそな、SBIの外貨預金をしている例を示す。

```js
const asset = {
  stocks: [
    { label: 'V', number: 100, comment: 'VISA' },
    { label: '2914:TYO', number: 500, comment: 'JT' },
    { label: '0700:HKG', number: 100, comment: 'Tencent' },
  ],
  cache: [
    { value: 500000, currency: 'JPY', comment: 'mizuho' },
    { value: 300000, currency: 'JPY', comment: 'resona' },
    { value: 10000, currency: 'USD', comment: 'SBI' },
  ],
};
```

## 実行例
この人は4.8 万ドル持っているみたい。お金もちだね。

```
$ node fin.js
48683.32551606166
```

# 実装

## 使用ライブラリ

- HTTP リクエスト: axios
- XML パーサ: xml2js
- Task モナド: Folktale
- 便利な関数: Ramda

モナドは、Monet.js と Folktale がお勧め。便利関数群は Ramda と Lodash があるけど、Ramda の方が機能が多いような気がする（Lodash で `traverse` とか `lift` ってできないような）

## WebAPI

- Google finance: 全世界の取引所の株価を提供
- ECB: 為替データを提供

Google Finance のAPI `getprices` は非公式API であるが、現時点で世界の取引所の株価を取得できる現実的なAPI はここしかない。

為替データは EU の中央銀行 ECB が公式に継続的にデータを配信しているので利用させてもらう。XML形式なので JavaScript との相性は悪い。

## コード

- 概要：以下3つを`Task`モナドでくるみ、集計関数を`lift` して適用して結果を得る
    - 為替データの取得: `getExchangeTable`
    - 株データの取得: `getStock`
    - 現金データの取得: `getCache`

- ひとこと
    - callback型のXMLパーサのメソッド呼び出しは`nodebackFromTask` で `Task` モナドに変換する
    - Promise は `promisedToTask` で `Task` に変換する
    - エラーのときは `NaN` が返る

- ソース

```js
const axios = require('axios');
const xml2js = require('xml2js');
const R = require('ramda');
const nodebackFromTask = require('folktale/conversions/nodeback-to-task');
const promisedToTask = require('folktale/conversions/promised-to-task');
const Task = require('folktale/concurrency/task');

const xmlToJSON = nodebackFromTask((new xml2js.Parser()).parseString);

// FX stuff, getExchangeTable() and related.
const createRawExchangeTable = R.pipe(
  R.path(['gesmes:Envelope', 'Cube', 0, 'Cube', 0, 'Cube']),
  R.map(R.prop('$')),
  R.reduce((acc, x) => { acc[x.currency] = parseFloat(x.rate); return acc; }, {}),
);
const createExchangeTable = table => ({
  JPY: table.JPY / table.USD,
  HKD: table.HKD / table.USD,
  EUR: table.USD,
  USD: 1.0,
});
const getExchangeTable = R.pipe(
  () => 'http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml',
  promisedToTask(axios.get), // -> Task
  R.map(R.prop('data')),
  R.chain(xmlToJSON), // flatten Task of Task
  R.map(createRawExchangeTable),
  R.map(createExchangeTable),
);

// Stock stuff, getStocks() and related.
const attachInfo = (entry) => {
  const [ticker, exchange] = entry.label.split(':');
  const url = `https://finance.google.com/finance/getprices?q=${ticker}&i=300&p=5d&f=o`;
  /* eslint no-nested-ternary: 'off' */
  const currency = exchange === 'HKG' ? 'HKD' : exchange === 'TYO' ? 'JPY' : 'USD';
  return ({
    ticker,
    exchange,
    url: exchange ? url.concat(`&x=${exchange}`) : url,
    currency,
    ...entry,
  });
};
const getPrice = R.pipe(
  R.prop('url'),
  promisedToTask(axios.get),
  R.map(R.pipe(
    R.prop('data'),
    R.split(/(?:\n)/g),
    R.takeLast(2),
    R.take(1),
    parseFloat,
  )),
);
const attachPrice = (entry) => {
  const price = getPrice(entry);
  return R.chain(p => Task.of({ price: p, ...entry }))(price);
};
const getStock = R.pipe(
  R.prop('stocks'),
  R.map(attachInfo),
  R.map(attachPrice), // ask Google howmuch. returns Array of Task
  R.sequence(Task.of), // to a single Task (of Array)
);

// cache stuff
const getCache = obj => Task.of(obj.cache);

// Reporting Stuff
/* eslint no-mixed-operators: 'off' */
const calcTotalUSD = (xTable, stocks, caches) => (
  stocks.reduce((acc, x) => acc + x.number * x.price / xTable[x.currency], 0) +
    caches.reduce((acc, x) => acc + x.value / xTable[x.currency], 0)
);

// Main
const asset = {
  stocks: [
    { label: 'V', number: 100, comment: 'VISA' },
    { label: '2914:TYO', number: 500, comment: 'JT' },
    { label: '0700:HKG', number: 100, comment: 'Tencent' },
  ],
  cache: [
    { value: 500000, currency: 'JPY', comment: 'mizuho' },
    { value: 300000, currency: 'JPY', comment: 'resona' },
    { value: 10000, currency: 'USD', comment: 'SBI' },
  ],
};

R.lift(calcTotalUSD)(getExchangeTable(), getStock(asset), getCache(asset))
  .run().promise().then(console.log);
```