何これ
私はここ1,2 か月集中的に JavaScript (Node) + Ramda + Folktale で関数型プログラミングを勉強していて、そろそろ実践的なコードが書けそうだなと思ったので披露をするよ。**「関数型プログラミングって何よ?」**という質問に対しては、本稿では「以下のスタイルでコードを作成すること」とするよ。
-
pipe
,compose
を使った関数合成で処理を構築するスタイル- 可読性を失わない範囲でポイントフリーを指向するスタイル
-
map
,reduce
等を使って制御構造を削減するスタイル
- モナドを使うスタイル
- 関数を純粋に構成するスタイル
-
lift
,sequence
,traverse
,chain/flatMap
,map
を活用するスタイル
質問あればコメントしてね。
作るもの
仕様
以下のように資産クラスが与えられた際に、実行時の資産総額を出力するプログラム。VISA と JT と テンセントの株を持っていて、銀行預金として、みずほとりそな、SBIの外貨預金をしている例を示す。
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
が返る
- callback型のXMLパーサのメソッド呼び出しは
-
ソース
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);