はじめに
手続き型と比べて関数型のよさを布教する人が多いけど、100までの偶数の和を足す例で説明されても正直腑に落ちないわけで。もう少し複雑というか、現実に即した例で説明したほうがいいんじゃない?
お題
-
仕様
- EUの中央銀行 ECB が提供する為替データをもとに、
- USD/JPY, EUR/USD, HKD/USD の値を出力するコンソールプログラムを作る
-
制約
- JavaScript で実装
- 取得データは http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml
- XMLパーサには xml2js を使う
- HTTPリクエストには axios を使う
- linter に怒られないコード
出力例
$ node forex.js
{ JPYUSD: 107.01399563142141,
HKDUSD: 7.849688536526171,
EURUSD: 1.2361 }
回答例1:そこそこ頑張った
自分で実装してから、回答例を見てほしい。
一つ目の回答例は全体で23行、一時変数はたったの 2個。あなたのコードはどうだっただろう。コメントで教えてくれてもいいのよ。
const { promisify } = require('util');
const axios = require('axios');
const xml2js = require('xml2js');
const R = require('ramda');
const xmlToJSON = promisify((new xml2js.Parser()).parseString);
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 => ({
JPYUSD: table.JPY / table.USD,
HKDUSD: table.HKD / table.USD,
EURUSD: table.USD,
});
const getRate = async () => {
const response = await axios.get('http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml');
const jsonData = await xmlToJSON(response.data);
return R.pipe(createRawExchangeTable, createExchangeTable)(jsonData);
};
getRate().then(console.log);
#つーか、xml2js
の吐くJSONが汚すぎる、、、
回答例2: モナドを使うとすごくいい
Promise はモナドじゃないのでポイントフリーにできない。Promise の代わりに、folktale の Task
モナドにすると、回答例は全体で24行、一時変数は 0個になる。しかもインデントの深さは最大でも1。
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 xmlToJSON = nodebackFromTask((new xml2js.Parser()).parseString);
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 => ({
JPYUSD: table.JPY / table.USD,
HKDUSD: table.HKD / table.USD,
EURUSD: table.USD,
});
const getRate = R.compose(
R.map(createExchangeTable),
R.map(createRawExchangeTable),
R.chain(xmlToJSON),
R.map(R.prop('data')),
promisedToTask(() => axios.get('http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml')),
);
getRate().run().promise().then(console.log);
回答例3: 普通に手続き的に
普通に書くと、28行で一時変数は5個。
const { promisify } = require('util');
const axios = require('axios');
const xml2js = require('xml2js');
const xmlToJSON = promisify((new xml2js.Parser()).parseString);
const createRawExchangeTable = (obj) => {
const dataPart = obj['gesmes:Envelope'].Cube[0].Cube[0].Cube;
const table = {};
for (x of dataPart) {
table[x.$.currency] = parseFloat(x.$.rate)
}
return table;
};
const createExchangeTable = (jsonData) => {
const table = createRawExchangeTable(jsonData);
return {
JPYUSD: table.JPY / table.USD,
HKDUSD: table.HKD / table.USD,
EURUSD: table.USD,
};
};
const getRate = async () => {
const response = await axios.get('http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml');
const jsonData = await xmlToJSON(response.data);
return createExchangeTable(jsonData);
};
getRate().then(console.log);
考察
- 関数型にすると、一時変数の数は大幅に削減できる
- データではなく、操作にフォーカスできるのは強み。
- 関数型にすると、for文 はなくせる
- 代わりに reduce, map になるので読みやすくなるかは微妙。
- バグを減らせるか?はあまり期待できないかも。
- モナドまで手を出すと、関数型はがぜん読みやすくなる
- ポイントフリー(完全に composable)につくると読みやすい
- そうでない場合、手続き的な部分と関数型てきな部分が混在するので、人によっては読みにくいとか、手続き型とそんなに変わりがないと思ってしまうかも。
今度ポエムがバズったら、この記事を紹介してみよう。
おまけ:for 文は読みにくい?
全く同等のことを for と reduce で書いた例が以下(さっきの例から抜粋)。どっちがよみやすい?まあ、どっちもどっちじゃない?
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 createRawExchangeTable2 = (obj) => {
const dataPart = obj['gesmes:Envelope'].Cube[0].Cube[0].Cube;
const table = {};
for (x of dataPart) {
table[x.$.currency] = parseFloat(x.$.rate)
}
return table;
};