18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-17

何これ

私はここ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パーサのメソッド呼び出しはnodebackFromTaskTask モナドに変換する
    • Promise は promisedToTaskTask に変換する
    • エラーのときは NaN が返る
  • ソース

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);
18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?