LoginSignup
7
8

More than 5 years have passed since last update.

関数型のよさがわかる実践例

Last updated at Posted at 2018-04-11

はじめに

手続き型と比べて関数型のよさを布教する人が多いけど、100までの偶数の和を足す例で説明されても正直腑に落ちないわけで。もう少し複雑というか、現実に即した例で説明したほうがいいんじゃない?

お題

  • 仕様

    • EUの中央銀行 ECB が提供する為替データをもとに、
    • USD/JPY, EUR/USD, HKD/USD の値を出力するコンソールプログラムを作る
  • 制約

  • 出力例

$ 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;
};
7
8
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
7
8