19
17

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.

『Don’t Be Scared Of Functional Programming』 JavaScriptで関数型言語に触れてみる。

Posted at

はじめに

これは、Don't Be Scared of Functional Programmingを読んだまとめです。
元記事の後半部分ではD3の例も載っていますが、D3の知識も入ってきてややこしいので紹介していません。
要望があれば付け足しますが、興味があれば実際に読んでみるといいと思います。
サンプルがしっかり書かれているので読みやすいです。

関数型プログラミングのさわりとしての話なので、ざっくりと書いている部分もありますが、大目に見て下さい。
コード等は、元記事の例を使用しています。

関数型プログラミングの基本コンセプト

  • Immutable
    要するに、『状態を変更できない』こと。変更を加えるのではなく、新しく作成する。
    例として、配列の中身を変更する処理があった場合に対象の配列を更新するのではなく、変更の加えられた新しい配列を作成します。

  • Stateless
    状態を保持しないこと。これは、関数の処理が他の何かに依存しないことです。
    外部の値から影響されず、引数として受け取った値に対しての処理しか行いません。

関数型プログラミングらしい実装のために

慣れていないと、どうしても関数型プログラミングっぽくない実装をしてしまいます。
よりベストプラクティスに沿うように、以下のルールを守りましょう。

  • 【ルール1】全ての関数は、最低でも1つの引き数を取ります。
  • 【ルール2】全ての関数が、何かしらの値、もしくは関数を返します。
  • 【ルール3】ループは使用禁止。

実際にやってみよう

こんな値を返すAPIがあったとします。

var data = [
  { 
    name: "Jamestown",
    population: 2047,
    temperatures: [-34, 67, 101, 87]
  },
  {
    name: "Awesome Town",
    population: 3568,
    temperatures: [-3, 4, 9, 12]
  }
  {
    name: "Funky Town",
    population: 1000000,
    temperatures: [75, 75, 75, 75, 75]
  }
];

人口(population)に対しての平均気温(temperature)をグラフに可視化したいとします。
グラフを描画するために、まずは上記のデータを以下の様な状態にしたいです。
xが平均気温、yが人口を表しています。

[
  [x, y],
  [x, y],
  ...
]

関数型プログラミングとか意識しないで実装すると例えばこんな感じ。
...んー、読みづらいかも。。

var coords = [],
    totalTemperature = 0,
    averageTemperature = 0;

for (var i=0; i < data.length; i++) {
  totalTemperature = 0;
  
  for (var j=0; j < data[i].temperatures.length; j++) {
    totalTemperature += data[i].temperatures[j];
  }

  averageTemperature = totalTemperature / data[i].temperatures.length;

  coords.push([averageTemperature, data[i].population]);
}

これを、関数型言語っぽく書き換えてみましょう。

まずは、配列の合計値を出す関数を作ります。
ここでは、上に書いた【ルール3】に則って、ループは使わず再帰処理を使って実装します。

// 引き数には、現在の合計値と数値のリストをとります。
function totalForArray(currentTotal, arr) {
  
  currentTotal += arr[0]; 

  // Array.shiftは使いません。【ルール2】配列を編集するのではなく、新しく作成します。
  var remainingList = arr.slice(1);

  // 再帰処理をします。現時点での合計値と残りの配列を渡します。
  // 配列が空の場合は、合計値を返します。
  if(remainingList.length > 0) {
    return totalForArray(currentTotal, remainingList); 
  } else {
    return currentTotal;
  }
}

この関数を使うと、気温の合計値は以下のように計算できます。

var totalTemp = totalForArray(0, temperatures);

足し算の部分も、関数にしてしまいましょう。

function addNumbers(a, b) {
  return a + b;
}

そうすると、さっきの関数はこうなります。

function totalForArray(currentTotal, arr) {
  currentTotal = addNumbers(currentTotal, arr[0]);

  var remainingArr = arr.slice(1);
  
  if(remainingArr.length > 0) {
    return totalForArray(currentTotal, remainingArr);
  }
  else {
    return currentTotal;
  }
}

...実はJavaScriptには便利な関数が元からありました。
reduce関数というものです。この関数で置き換えてみます。
ちなみに、reduceのような関数を高階関数と呼び、関数型のプログラム言語であれば大体サポートしています。

var totalTemp = temperatures.reduce(function(previousValue, currentValue){
  return previousValue + currentValue;
});

ここに、少し前に作成した足し算用の関数を使うとこうなります。
とてもすっきりしました。

var totalTemp = temperatures.reduce(addNumbers);

配列の合計値を求める、ってのは汎用的なので関数にしちゃいます。

function totalForArray(arr) {
  return arr.reduce(addNumbers);
}

var totalTemp = totalForArray(temperatures);


次に、求めた合計値から平均を出す関数を用意します。

function average(total, count) {
  return total / count;
}

今作成したaverage関数と、さっき作成した合計値を出す関数を組み合わせると...

function averageForArray(arr) {
  return average(totalForArray(arr), arr.length);
}

var averageTemp = averageForArray(temperatures);

次に、オブジェクトの配列から、単一のプロパティを抜き出す関数を作りましょう。
これには、JavaScriptが持っているmap関数というものを使います。

var allTemperatures = data.map(function(item) {
  return item.temperatures;
});

// -- 補足 ここから --
// 上のmap関数を使ってしているのは、
// data というオブジェクトの配列から、気温だけを抜き出してallTemperaturesという配列に入れる、ということです。
var data = [
  { 
    name: "Jamestown",
    population: 2047,
    temperatures: [-34, 67, 101, 87]
  },
  {
    name: "Awesome Town",
    population: 3568,
    temperatures: [-3, 4, 9, 12]
  }
  {
    name: "Funky Town",
    population: 1000000,
    temperatures: [75, 75, 75, 75, 75]
  }
];

var allTemperatures = [[-34, 67, 101, 87], [-3, 4, 9, 12], [75, 75, 75, 75, 75]];
// -- 補足 ここまで --

オブジェクトの配列から、プロパティを抜き出すのも汎用的なタスクなので、関数にしてしまいましょう。
注意したいのは、この関数は関数を返しています。実行は呼び出し元に任せています。

function getItem(propertyName) {
  return function(item) {
    return item[propertyName];
  }
}

ここまでと同じように、上のステップで用意した関数を実際に使ってみましょう。

var temperatures = data.map(getItem('temperature'));

さらに、『オブジェクトの配列から、プロパティを抜き出して配列にする』部分を関数として汎用的に使えるようにします。
ちなみにpluckは、『引き抜く』みたいな意味のようです。

function pluck(arr, propertyName) {
  return arr.map(getItem(propertyName));
} 

var allTemperatures = pluck(data, 'temperatures');

これで必要な情報を抜き出す事ができるようになりました。
気温は、全ての気温を持った配列の配列になっているので、平均気温の配列に整形します。
map関数とここまでに作成した平均の値を取り出す関数averageForArrayを使います。

var populations = pluck(data, 'population');

var allTemperatures = pluck(data, 'temperatures');
var averageTemps = allTemperatures.map(averageForArray);

// この時点で、以下の様な配列を取得できるようになりました。
// populations
[2047, 3568, 1000000]

// averageTemps
[55.25, 5.5, 75]

最後に、2つの配列を1つにまとめる関数を作成します。

function combineArrays(arr1, arr2, finalArr) {
  // 直接呼び出す時に渡し忘れた場合を想定して空の配列を用意しています。
  finalArr = finalArr || [];

  // 最初の要素を抜き出して出力用の配列に挿入します。
  finalArr.push([arr1[0], arr2[0]]);

  var remainingArr1 = arr1.slice(1),
      remainingArr2 = arr2.slice(1);

  // 結合する配列が空になった場合、処理を終了します。
  if(remainingArr1.length === 0 && remainingArr2.length === 0) {
    return finalArr;
  } else {
    return combineArrays(remainingArr1, remainingArr2, finalArr);
  }
};

var processed = combineArrays(averageTemps, populations);

// 最終的に取得できる値
// [
//  [ 55.25, 2047 ],
//  [ 5.5, 3568 ],
//  [ 75, 1000000 ]
// ]

無理して書く必要はないですが、1行で書くこともできます。

var processed = combineArrays(pluck(data, 'temperatures').map(averageForArray), pluck(data, 'population'));

まとめてみるとこうなります。

function addNumbers(a, b) {
  return a + b;
}

function totalForArray(arr) {
  return arr.reduce(addNumbers);
}

function average(total, count) {
  return total / count;
}

function averageForArray(arr) {
  return average(totalForArray(arr), arr.length);
}

function pluck(arr, propertyName) {
  return arr.map(getItem(propertyName));
} 

function combineArrays(arr1, arr2, finalArr) {
  finalArr = finalArr || [];

  finalArr.push([arr1[0], arr2[0]]);

  var remainingArr1 = arr1.slice(1),
      remainingArr2 = arr2.slice(1);

  if(remainingArr1.length === 0 && remainingArr2.length === 0) {
    return finalArr;
  } else {
    return combineArrays(remainingArr1, remainingArr2, finalArr);
  }
};

var populations = pluck(data, 'population');

var allTemperatures = pluck(data, 'temperatures');
var averageTemps = allTemperatures.map(averageForArray);

var processed = combineArrays(averageTemps, populations);

やってみて

関数に分けることで、処理の部分がとても簡潔に書けるようになりました。
これは普段コードを書くときにも必要な考え方ですね。
テストもしやすくなるので、後々の保守性やコードの再利用にもどんどん活かせそうな気がします。

(`・ω・´)ゞ

注意

再帰処理は、関数型言語にかぎらず有効な手段ですが、JavaScriptを含むいくつかの言語では呼び出し回数が増えると問題が出るようです。

参考:The maximum call stack size

また、ECMAScript6からtail recursionという手段もありますが、ここでは触れていません。

参考:What is tail recursion?

19
17
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
19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?