LoginSignup
8
9

More than 3 years have passed since last update.

関数型プログラミングJavaScriptについて

Last updated at Posted at 2020-01-24

※こちらは以下に紹介する本の要約を主とした勉強会用資料です。

読んだ本

JavaScript関数型プログラミング 複雑性を抑える発想と実践法を学ぶ impress top gearシリーズ | LuisAtencio, 株式会社イディオマコムニカ 加藤大雄 | 工学 | Kindleストア | Amazon

著者のGitリポジトリ: GitHub - luijar/functional-programming-js: Code Samples Functional Programming in JavaScript, Manning 2016

関数型で書くとこうなる

以下の処理を命令型(所謂「普通の」書き方)、関数型の両方で書いてみる。

以下のような名前のリストに対して

const names = [
  'alonzo church',
  'Haskell_curry',
  'stephen_kleene',
  'john von Neumann',
  'Stephen Kleene',
  null
];

以下のような操作を行う。

①nullチェックを実施し
②アンダーバーを取り除き
③姓名の先頭を大文字にし
④重複を除去して
⑤出力する

命令型の場合

const result = [];

for (let i = 0; i < names.length; i++) {
  const n = names[i];
  if (n !== undefined && n !== null) {  // ①
    ns = name.replace(/_/, '').split(' ');  // ②
    for (let j = 0; j < ns.length; j++) {  // ③
      let p = ns[j];
      p = p.charAt(0).toUpperCase() + p.slice(1);
      ns[j] = p;
    }
    if (result.indexOf(ns.join(' ')) < 0) {  // ④
      result.push(ns.join(' '));
    }
  }
}
console.log(result);  // ⑤

関数型(with Lodash)の場合

const _ = require('lodash');
const isValid = (val) => !_.isUndefined(val) && !_.isNull(val);

const result = _.chain(names)
  .filter(isValid)  // ①
  .map((s) => s.replace(/_/, ' '))  // ②
  .map(_.startCase)  // ③
  .uniq()  // ④
  .value();

console.log(result);  // ⑤

(極端な例だが)関数型で書くと、簡潔かつ見通し良いコードになっている。

関数型プログラミングとは

一連の処理を関数として分割・抽象化し、それらの組み合わせで記述するプログラミング手法
※命令型プログラミング…状態(データ)を管理しながら、上から順に命令を実行するプログラミング手法

特徴

①純粋関数の組み合わせで処理を記述する

  • 純粋関数…参照透過性が保たれている関数=副作用のない関数
    • 提供される入力値(引数)にのみ依存する
    • 関数の外部で変更される可能性のあるデータに一切依存しない
    • 関数自身のスコープ外にある変数を一切変更しない
      ex. グローバル変数、DOM操作など
  • 参照透過性…同じ入力に対して実行結果が常に変わらないこと
  • 副作用(side effects)…変数の再代入や入出力などによってプログラムの状態が変化すること

※不純な関数の例

var counter; // グローバル変数が外部から変更される可能性がある
function increment() {
  return counter++;
}

純粋であるためには

function increment(counter) {
  return counter++;
}

と書く必要がある。

②処理を抽象化する

上述の名前のリストを出力する例では、nullチェックを以下のように定数化していた。

const isValid = (val) => !_.isUndefined(val) && !_.isNull(val);

isValidは「引数をnullかどうか判定する」関数であり、
リストを出力する処理以外にも利用できる一般化(抽象化)されたロジックである。
このように、関数型プログラミングでは処理を可能な限り汎用的なロジックに分割することを目指す。

③管理する状態(データ)を極力減らす

以下は、ループ処理におけるカウンタと配列インデックスを管理しないよう実装した例。

命令型

const array = [0, 1, 2, 3, 4, 5];
for (let i = 0; i < array.length; i++) {
  array[i] = Math.pow(array[i], 2);
}
console.log(array); // [0, 1, 4, 9, 16, 25];

関数型

const array = [0, 1, 2, 3, 4, 5].map((num) => {
  return Math.pow(num, 2);
});
console.log(array); // [0, 1, 4, 9, 16, 25];

利点

  • 予期せぬ挙動(副作用)を減少できる
  • コードのモジュール性、再利用性を高められる
  • プログラムが理解しやすい粒度で関数として分割され、コード全体の見通しがよくなる
  • 各関数に参照透過性が保たれるため、ユニットテストが容易になる

欠点

  • 全体のコード量が命令型よりも多くなる傾向にある
  • 命令型よりも処理が遅く、メモリ消費量が多くなる場合がある
    ex. カリー化(後述)された関数を大量に使用した場合

JavaScriptにおける関数型プログラミング

純粋関数型言語(HaskellやLISPなど)では、参照透過性が常に保たれるという意味において、全ての式や関数の評価時に副作用を生まない。
一方JavaScriptは、変数の値やオブジェクトの状態を書き換えるプログラミングスタイル / 使用用途から、すべてのコードを関数型で書くことは不可能に近い。
このため関数型でJavaScriptを書くということは、純粋な処理と不純な処理を分離し、状態の変更を最小限に抑える ことを目的とする。

コーディングTips

①返り値があり、単一の機能を持つ関数に処理を分解する

②外部の変数を変更しない関数を書く

③Array.map()/Array.filter()/Array.reduce()などを使う

以下は、上述のリスト操作をLodashなしで書いた例。

export default class NameList {
  constructor() {
    this.names = [
      'alonzo church',
      'Haskell_curry',
      'Stephen_Kleene',
      'john von Neumann',
      'Stephen Kleene'
    ];

    console.log(this.formatNames());
  }

  // thisなど変数に変更を与える処理を局所化する

  formatNames() {
    NameList.removeDuplicate(
    NameList.removeUnderscore(
    NameList.nullFilter(this.names)));
  }

  // 静的メソッドとして処理を分割する

  static nullFilter(array) {
    return array.filter((val) => {
      return val !== undefined && val !== null;
    });
  }

  static removeUnderscore(array) {
    return array.map((val) => {
      return val.replace(/_/, ' ');
    });
  }

  static removeDuplicate(array) {
    return [...new Set(array)];
  }
}

④ライブラリで関数をチェーン化する

可読性が向上し、ロジックをライブラリに委譲することでコード量も削減される。

import _ from 'lodash';

export default class NameList {
  constructor() {
    this.names = [
      'alonzo church',
      'Haskell_curry',
      'Stephen_Kleene',
      'john von Neumann',
      'Stephen Kleene'
    ];

    console.log(this.formatNames());
  }

  formatNames() {
    return _.chain(names)
            .filter((val) => !_.isUndefined(val) && !_.isNull(val))
            .map((s) => s.replace(/_/, ' '))
            .uniq()
            .value();
  }
}

ライブラリによる関数のチェーン化はライブラリ処理セットを使用するしかなく、表現力に限界がある。
また、チェーンに汎用性をもたせたり、部分的に実行したりすることができないため、再利用性に乏しい。

⑤関数合成を使う

関数合成(パイプライン化)…ある関数の出力が次の関数の入力となるようにデータ型・引数の数(arity)を一致させ、次々に処理を実行するよう関数をつなげること
→データ型の制限なくあらゆる関数を組み合わせる柔軟性を実現する

Ramdaによる関数合成を使って以下の操作を実装する。

①DB(オブジェクト)からデータを検索し
②CSV形式の文字列に変換し
③DOMに追加する

import R from 'ramda';

// DBオブジェクト
const students = {
  '001': {
    firstName: 'Alonzo',
    lastName: 'Church',
  },
  '002': {
    firstName: 'Haskell',
    lastName: 'Curry',
  },
};

// ①を実行する関数を定義
const findObject = R.curry((db, id) => db[id]);
const findStudent = findObject(students);

// ②を実行する関数を定義
const csv = ({ firstName, lastName }) => `${firstName}, ${lastName}`;

// ③を実行する関数を定義
const append = R.curry((elementId, info) => {
  document.querySelector(elementId).innerHTML = `<p>${info}</p>`;
});

// 関数合成
const showStudent = R.pipe(
  findStudent, // ①
  csv, // ②
  append('#student-info'), // ③
);

// 実行
let result = showStudent('001'); // Alonzo, Church

関数のカリー化

カリー化…期待される数より少ない引数で関数を呼び出した場合に、残りの引数を取る別の関数を返すよう変換すること

function max(x, y){
    return x > y ? x : y;
}
max(1,2); // 2

// カリー化された関数
function _max(x){
    return function(y){
        return x > y ? x : y;
    }
}
_max(1)(2); // max(1,2)

// 一部の引数が適用された新しい関数をつくる
const compareToOne = _max(1); // function max(y) { return 1 > y ? 1 : y;  }
compareToOne(2); // 2

// ライブラリを使って容易にカリー化が可能
const _max = _.curry(max);

※上記compareToOneのように、次に引数を渡した時点で評価される関数を部分適用関数という

なぜカリー化が必要か?
  • 部分的に評価された関数を作れるため、関数の再利用性が増す
const findObject = R.curry((db, id) => db[id]);
const findStudent = findObject(students);
// 別機能を持つ関数を容易に作成できる
const findTeacher = findObject(teachers);
  • 引数の数を1つに統一できるため、関数合成がしやすい

⑥コンビネータ、モナドを使う

コンビネータ

…関数合成にあたって、関数間のフロー(if/elseなど)を統合するための関数
→複雑な処理フローでも関数チェーンを使うことが可能になる
ex.tap(Kコンビネータ)、sequence(Sコンビネータ) など
※KやSはラムダ計算の記号名

例 : alternation (ORコンビネータ)…if-else文をエミュレートする関数

const alternation = R.curry((func1, func2, val) => {
  func1(val) || func2(val);
});

const showStudent = R.pipe(
  // if-else文を書くことなく関数をチェーン化できる
  alternation(findStudent, findTeacher),
  csv,
  append('#student-info'));

let result = showStudent('001');

コンテナ、ファンクター、モナド

  • コンテナ…引数の型統一、単項化のためのラッパーオブジェクト
    ex. LodashオブジェクトやPromiseオブジェクト
  • ファンクター…コンテナ内の値に関数を適用し、再度コンテナでラップするための関数
    ex. _.map()やPromise.then()など
  • モナド…特定の処理(nullチェック、条件分岐)が実行できるファンクター
    ex.eitherモナド、Maybeモナド、IOモナドなど
なぜファンクター/モナドが必要か?
  • 引数の単項化・関数の純粋化を実現し、関数合成を容易にする。
  • Javascriptのtry-catchによるエラー処理は関数のチェーン化ができず、また関数の参照透過性を損なう。
    →モナドを用いることで、エラー発生時も処理をチェーンすることができる。
  • DOMを扱う場合も、少なくともアプリケーションの観点からは不変であるようにIO処理を実現できる。

⑦非同期処理をPromiseで処理する

Promise.then()による関数合成により非同期処理のチェーン化を実現できる

⑧イベント駆動処理をRxJSで処理する

Rx.Obserbableをモナドとし、イベント駆動処理のチェーン化を実現できる

参考になったサイト

JavaScript ( 時々 TypeScript ) で学ぶ関数型プログラミングの基礎の基礎 #1 – 高階関数について – PSYENCE:MEDIA

8
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
8
9