BeAct Co., Ltd.

BLOG
社員ブログ

コールバック関数をPromiseに?関数が状態を持つ?~JavaScriptの関数で遊ぼう!~

こんにちは! 今日もコーディングを楽しんでいますか?

こんかいはタイトルのとおり、 JavaScript の関数を遊びながら学習してみようという記事です。じつはこの関数というのが面白くて、なんと「『関数を返す』関数」「『関数を引数にする』関数」を定義できてしまいます。

というわけで、このちょっと不思議で、慣れると強力な「関数」をおもちゃにして、遊んでみましょう!


「関数を返す」とはどういうこと?

さて、そもそも「関数を返す関数」といっても、今ひとつイメージしにくいですよね。百聞は一見にしかず、以下のコードをご覧ください。

/**
 * 関数を返す関数
 * @returns {() => string}
 */
function funcFactory() {
  /** これが、返す関数です */
  const theFunction = function() {
    return 'こんにちは!';
  };

  // え? 何を返してるの?!
  return theFunction;
};

funcFactory() は、見ての通り関数です。では、この関数が返すはなにか? 上のコードの一部を抜粋してみましょう。

  /** これが、返す関数です */
  const theFunction = function() {
    return 'こんにちは!';
  };

  // え? 何を返してるの?!
  return theFunction;

関数の定義を変数に代入し、その変数を return する……。つまり、「関数という」を返しているのです。そう、JavaScript において、関数とは「値」なのです!

さて、先の関数を利用して、値として帰ってきた関数を使ってみましょう。

/** 関数という値が、代入されました */
const receivedFunc = funcFactory();
/** だから、こんな事ができます */
const text = receivedFunc();
console.log(text); // こんにちは!

// せっかちさんは、こんな書き方をしたいかも?
console.log(funcFactory()()); // こんにちは!

このように JavaScriptでは、関数を「値」として扱えるため、「『関数を返す』関数」を定義できる、というわけです。

「関数を引数にする」とはどういうこと?

上記のコード例では、あらかじめ用意した関数を、変形用の関数の中に置いていました。でも、「関数を値として使える」というのなら、以下のように「引数として」関数を与えても構わないはずです。

/**
 * 関数を受け取って、関数を返す関数
 * @param {(p0: string) => string} fn 引数として受け取る関数
 * @returns {(p1: string) => string}
 */
function funcFactory(fn) {
  /**
   * 返す関数の中で、引数の関数を使ってみます
   * @param {string} p1
   * @returns {string}
   */
  const theFunction = function(p1) {
    return fn('こんにちは') + p1;
  };

  return theFunction;
};

/** 引数としての関数 */
const fn = function(text) {
  return text + '、';
};
/** fn を引数にして、新たな関数を取得します */
const receivedFunc = funcFactory(fn);
/** この関数を使ってみると… */
const text = receivedFunc('世界!');
// こうなります!
console.log(text); // こんにちは、世界!

// せっかちさんは、こんな書き方をしたいかも?
console.log(funcFactory(fn)('世界!')); // こんにちは、世界!

関数 fn を定義したうえで、それを「値」として引数にする……。これが「『関数を引数に取る』関数」という意味です。

なお、コード例を短くするために、以降はアロー関数(fat arrow 関数)で書くこととします。

/**
 * 先ほどと同じコード
 * @returns {() => string}
 */
const funcFactory = () => { 
  return () => 'こんにちは!';
};

関数の変形

本題に入る前に、まずは頭の準備運動から。先ほどの「高階関数」の性質を利用して、関数を変形してみましょう。

カリー化 (Currying)

カリー化は関数変形の基本にして、最も重要な概念です。まずはこの「カリー化」を示すコードをご覧ください。

/**
 * 元の関数
 * @param {number} paramA
 * @param {number} paramB
 * @param {number} paramC
 * @returns {number} 引き算の結果
 */
const origFunc = (paramA, paramB, paramC) => {
  return paramA - paramB - paramC;
};

/**
 * …を、「カリー化」する関数
 * @param {number} paramA
 * @returns {(paramB: number) => (paramC: number) => number} 「カリー化」した関数
 */
const curriedFunc = paramA => { 
  return paramB => {
    return paramC => origFunc(paramA, paramB, paramC);
  };
};

// 元の関数を使う
const origResult = origFunc(4, 2, 1); // 4 - 2 - 1 の結果、つまり 1

// カリー化すると……
const secondFunc = curriedFunc(4); // origFunc(4, paramB, paramC) の paramB を必要とする関数が生まれる
const lastFunc = secondFunc(2); // origFunc(4, 2, paramC) の paramC を必要とする関数が生まれる
const curriedResult = lastFunc(1); // origFunc(4, 2, 1) の結果を得られる

// 短く書くなら……
const result = curriedFunc(4)(2)(1); // 要するに、このように書けるようにする事が「カリー化」の目的です

カリー化とは、複数の引数を必要とする関数を、「1つの引数だけを受け取り、残りの引数を受け取る関数」を返すように変換することです。その関数を使って次の引数を与え、さらに残りの引数を受け取る関数を受け取る……を繰り返すことで、最終的に元の関数と同じ結果を得ることができます。

部分適用 (Partial application)

カリー化とよく混同されがちなのが、この「部分適用」です。違いは、カリー化が「複数の引数を、先頭から1つずつ適用してゆく」のに対し、部分適用は「複数の引数を、順番や引数の数を無視して適用してゆく」ものであることです。

/**
 * 元の関数
 * @param {number} paramA
 * @param {number} paramB
 * @param {number} paramC
 * @returns {number} 引き算の結果
 */
const origFunc = (paramA, paramB, paramC) => {
  return paramA - paramB - paramC;
};

/**
 * …を、「部分適用」する関数
 * @param {number} paramC
 * @returns {(paramB: number) => number} 「部分適用」した関数
 */
const paFunc = paramC => { 
  // 最初から第1引数を入れてしまっていることに注意!
  return paramB => origFunc(4, paramB, paramC);
};

// 元の関数を使う
const origResult = origFunc(4, 2, 1); // 4 - 2 - 1 の結果、つまり 1

// 部分適用関数を使うと……
const tempFunc = paFunc(1); // origFunc(4, paramB, 1) の paramB を必要とする関数が生まれる
const paResult = tempFunc(2); // origFunc(4, 2, 1) の結果を得られる

// 短く書くなら……
const result = paFunc(1)(2); // 要するに、このように書けるようにする事が「部分適用」の目的です

コールバック関数を変形してPromise化できる!

JavaScript のビルトイン関数の一部には、引数として「成功時のコールバック関数」「失敗時のコールバック関数」を必要とするものがあります。しかし、コールバック関数を安易に使うと、いわゆるコールバック地獄への第一歩となりかねません。

Promise を用いた非同期処理さえできれば、async/await を使って見通しよく書けるのに……という悩みを解決するのが、このテクニックです!

一例として、 navigator.geolocation.getCurrentPosition() という、ビルトイン関数を変形してみましょう。これはスマホなどの「端末の現在位置を、緯度と経度の座標で取得する」というものです。業務で JavaScript を使っている方には、おなじみの関数かもしれませんね。

さて、この関数の仕様は以下の通りです。

interface Geolocation {
  getCurrentPosition(
    /** 座標の取得に成功したときのコールバック */
    success: (position: GeolocationPosition) => void,
    /** 座標の取得に失敗したときのコールバック */
    error?: (positionError: GeolocationPositionError) => void,
    /** その他のオプション */
    option?: PositionOptions,
  ): void;
}

この関数には、成功時、失敗時、それぞれで実行されるコールバック関数を与える事になっています。これを、関数の変形を用いて Promise 化してみましょう!

/**
 * コールバック形式の非同期関数を Promise 化する
 * @template T
 * @template {unknown[]} U
 * @param {(success:(p0:T)=>void, error:(p1:any)=>void, ...args:U)=>void} fn 対象の関数
 * @param {unknown} [thisObj] 任意の this バインディング(例:navigator.geolocation)
 * @returns {(...args:U)=>Promise<T>} プロミス化関数
 */
const promisify = (fn, thisObj) => {
  // その関数にとっての this が `fn` でなければ、this を bind する必要がある
  /** 対象の関数 */
  const boundFn = thisObj ? fn.bind(thisObj) : fn;

  return (...args) => {
    return new Promise((resolve, reject) => {
      // ここで、本来の関数の使い方をする
      boundFn(resolve, reject, ...args);
    });
  };
};

// ↑の関数の使い方

/** プロミス化関数。option を引数として呼び出せるようになる */
const getCurrentPosition = promisify(
  navigator.geolocation.getCurrentPosition,
  navigator.geolocation
);

try {
  /** 成功時は、値を直接得られる! */
  const geopos = await getCurrentPosition({timeout: 3000});
  console.log(geopos.coords.latitude, geopos.coords.longitude);
} catch(e) {
  // 失敗時の e は、もともとの失敗時のコールバック引数がそのまま来る!
  console.log(e);
}

promisify 関数は、対象の関数に与えるコールバックとして、 Promiseresolve()reject() を渡すことで、全体を Promise として扱えるように、関数を変形しています。もちろん、オプション部分が不要な場合は await getCurrentPosition() と、引数を与えずに関数を実行すれば OK です!

……といったように、関数を「値」として受け取り、「別の形にして返す」というアイデアは、promisify のような実用コードでも生きています。では、「関数が状態まで持つ」としたら? 次のトピックでは、その秘密を探ってみましょう。

状態を持った関数、クロージャ(関数閉包)

これまで述べたように、関数を値として扱う「関数ファーストクラス」の性質によって、JavaScript では柔軟な処理構造を作ることができます。加えて JavaScript は、「関数型プログラミング言語」としての特徴もある程度備えており、これを利用したテクニックも存在します。

関数型プログラミングにおける「関数」とは、参照透過性、つまり「どんな状況でも、同じ引数を与えた時は同じ結果を返し、その処理による影響を発生させない」という性質を持ちます。しかし、「関数自身が状態を持ち、実行のたびに状態が変わる」……そんな「副作用」を持つ関数が欲しい、という場合もあるでしょう。クロージャは、内部に状態を持つことで、それを実現するテクニックです。

上の動作サンプルの、左側の入力らんに数字を入力すると、それに 5 が足された結果が、右の入力らんに表示されます。コード例としては、よく見かける実装かもしれません。

では、この処理の核心部分のコードを見てみましょう。

/**
 * クロージャを作る関数(エンクロージャ)
 */
const createClosure = () => {
  // ここは「レキシカル環境」
  /** レキシカル環境に定義された変数を「レキシカル変数」と呼びます */
  const lexicalVar = 5;
  /**
   * クロージャ
   * @param {number} param
   */
  const closure = param => {
    return lexicalVar + param;
  };
  
  return closure;
};

/** クロージャを得ます */
const myFun = createClosure();

関数 createClosure は、closure という関数を返す関数です。こうした「クロージャを生成する関数」を、しばしばエンクロージャ(enclosure)と呼びます。

このコード例の最後の行で、変数 myFun にこの closure を代入し、 myFun(数字) として呼び出しています。しかし、考えてみればこれは不思議なことです。 もう一度クロージャのコードを見てみると……。

  const lexicalVar = 5;
  /**
   * クロージャ
   * @param {number} param
   */
  const closure = param => {
    return lexicalVar + param;
  };

変数 lexicalVar には、確かにこの関数の外側で 5 を代入していますが、それはあくまでも closure スコープの外側の話です。さらに、先ほどお伝えしたように「関数スコープ内で定義した変数は、処理がスコープを抜けると失われてしまう」というルールであるはずです。ならば、myFun(数字) を呼び出すたびに lexicalVar を「覚えて」足し合わせられるはずがないのです。

もっと甚だしい例をお見せしましょう。

同じ myFun 関数を実行するたびに、最初にエンクロージャに与えた引数が、1 ずつ増えています! では、こちらのコードの核心部分を見てみましょう。

/**
 * クロージャを作る関数(エンクロージャ)
 * @param {number} initialValue
 */
const createClosure = initialValue => {
  // ここは「レキシカル環境」
  /** レキシカル環境に定義された変数を「レキシカル変数」と呼びます */
  let accumulator = initialValue;
  /**
   * クロージャ
   * @param {number} param
   */
  const closure = param => {
    const result = accumulator + param;
    accumulator++;

    return result;
  };
  
  return closure;
};

/** クロージャを得ます */
const myFun = createClosure(5);

関数 createClosure が、引数を受け取るようになっています。関数 closure の定義の前に、accumulator という変数に、引数を代入していますね。

問題は、この closure の実装です。

  let accumulator = initialValue;
  /**
   * クロージャ
   * @param {number} param
   */
  const closure = param => {
    const result = accumulator + param;
    accumulator++;

    return result;
  };

closure スコープの中で、accumulator をインクリメントしているではありませんか! つまり、やはり closure 関数自身が accumulator の値を「覚えて」いて、呼び出されるたびに 1 を足している……と考えるのが自然です。

手品のタネは「レキシカル環境」

関数の常識を破る、このクロージャ。でも実は、エンクロージャとクロージャの間にある隙間、ここに秘密があるのです。

それを踏まえて、2つ目のクロージャのコードを、もう一度見てみましょう。

const createClosure = initialValue => {
  // ここは「レキシカル環境」
  /** レキシカル環境に定義された変数を「レキシカル変数」と呼びます */
  let accumulator = initialValue;
  /**
   * クロージャ
   * @param {number} param
   */
  const closure = param => {
    const result = accumulator + param;
    accumulator++;

    return result;
  };
  
  return closure;
};

createClosure 関数は、自身の関数スコープを持っています。このスコープにある変数は、

  • initialValue (関数自身の引数)
  • accumulator
  • closure

の3つです。paramresult は、あくまでも closure 関数のスコープ内変数であって、createClosure 関数のスコープとはみなされません。

さて、ここでエンクロージャ関数、すなわち createClosure が関数として呼び出されたとします。

createClosure(5) で呼び出したよ!

// よし、関数スコープの変数を評価しよう!
{
  initialValue: これは「引数」そのもの。つまり 5。
  accumulator: それに初期化される。つまり 5。
  closure: これは関数だから評価は据え置き。あとで評価する時は、今の initialValue, accumulator を参照できるようにしなくちゃ。
}

上の模式的な説明のうち、

closure: これは関数だから評価は据え置き。あとで評価する時は、今の initialValue, accumulator を参照できるようにしなくちゃ。

を見てください。JavaScript の高階関数では、外側の関数が実行された時点でのスコープ内の変数やその値(環境)への参照を、内側の関数は保持し続けるのです。「『内側の関数が生成されたときの、スコープの環境』の記録」への参照、と言い換えてもいいでしょう。

この「スコープの環境への参照」のことを、レキシカル環境と呼びます。

クロージャの便利な使い方

もちろんクロージャにも「コールバック関数をPromise化する」のような、便利な使い道があります。

例えば「ES6 Class のインスタンスのようなオブジェクト複数作りたい。でも export class ... で定義するほど大掛かりでもない」という場合。ここでは、複数の銀行口座の開設と、その入出金を行う仕組みを、クロージャで実装してみましょう。

この実装の核心部分は、こちらです。

/**
 * 複数人の銀行口座を作成する関数
 * @param {number} initialBalance 最初に入金する金額
 */
const createAccount = initialBalance => {
  /** この口座の残高 */
  let balance = initialBalance;
  /**
   * この口座の取引
   * @param {number} amount 取引額(負数は出金)
   */
  return amount => {
    balance += amount;
    return balance;
  };
};

/** A さんの口座を開設 */
const accountA = createAccount(1000);
/** B さんの口座を開設 */
const accountB = createAccount(10);

この程度の機能であれば、ES6 Class ではなくクロージャを使えば、それぞれのレキシカル環境が個別に生成されますので、複数の口座を作り、管理することができます!

別の例として、「似たようなイベントリスナを、簡単に作れたらいいのに」という場合を考えてみましょう。

「ボタンに応じて特定の文字の色を変える」のために、似たようなイベントリスナを多数記述するのは、DRY 原則から見ると好ましくはありません。クロージャは、こういうときにも便利です!

/**
 * クロージャを作る関数(エンクロージャ)
 * @param {string} color 色名
 * @param {HTMLElement} text HTML要素
 * @returns {()=>void} クロージャ
 */
const createClosure = (color, text) => {
  /**
   * クロージャ
   */
  return () => text.style.color = color;
};

// 以下は省略したコードです
const text1 = document.querySelector('#text1');
const red = createClosure('red', text1);
const black1 = createClosure('black', text1);

// ボタン1を押すと、指定した文字が赤くなる
button1.addEventListener('click', red);
// リセット1を押すと、指定した文字が黒くなる
reset1.addEventListener('click', black1);

ちょっとした工夫ですが、膨大な繰り返し記述を避けることができる、便利な道具になります!

クロージャの注意点

エンクロージャを実行すると、その時点で新たなレキシカル環境が生成されます。例えば下の例では、 createClosure 関数を3回呼んでいるので、レキシカル環境が3つ作られています

このコードでは、エンクロージャから得たクロージャを、以下のように const (再代入できない) によって宣言した変数に代入しています。

/** 演算関数を渡して、クロージャを得ます */
const myFun1 = createClosure((p) => p + 3);
const myFun2 = createClosure((p) => p - 5);
const myFun3 = createClosure((p) => p * 2);

上の動作サンプルは単純なものなので気にする必要はあまりないのですが、この変数が存在している限り、レキシカル環境への参照は残り続け、メモリから消すことはできません。規模の大きなコードでは、イベントリスナの破棄がなされなかったときと同様、いずれメモリリークを起こしてしまうかもしれません。

これを防ぐため、「必要がなくなったら、ガベージコレクションの対象にしたい」という場合、1つめの方法として、

/** 再代入可能な let で、クロージャを受けます */
let myFun1 = createClosure((p) => p + 3);
let myFun2 = createClosure((p) => p - 5);
let myFun3 = createClosure((p) => p * 2);

/** 不要になったら、 null を再代入して参照を切り離します */
myFun1 = null;
myFun2 = null;
myFun3 = null;

と、 null を再代入して参照を切り離せば、ガベージコレクションの対象となります。

または2つめの方法として、

/** そもそも変数がスコープ内なら、処理がスコープを抜けるたびに、変数への参照が切り離されます */
button1.addEventListener('click', () => {
  const value = +output1.value;
  const myFun = createClosure((p) => p + 3);
  const result = myFun(value);
  output1.value = `${result}`;
});
button2.addEventListener('click', () => {
  const value = +output2.value;
  const myFun = createClosure((p) => p - 5);
  const result = myFun(value);
  output2.value = `${result}`;
});
button3.addEventListener('click', () => {
  const value = +output3.value;
  const myFun = createClosure((p) => p * 2);
  const result = myFun(value);
  output3.value = `${result}`;
});

と、スコープの特性を活かして、暗黙に参照を切り離す方法が考えられます。

まとめ

さて、この不思議なおもちゃの遊び心地は、いかがだったでしょうか? 高階関数の性質を利用して、関数を Promise の形に変形したり、レキシカル環境と紐づくクロージャを作ったりしました。少しでも「面白い!」と感じていただければ幸いです。

難しく見えてしまった方もいらっしゃるかもしれませんが、これらはすべて「関数とは、値である」というルールから自然に導かれる形であることは、覚えておいて損はありません!

こんかい紹介したコードは、ほんの入り口にすぎません。関数と親しむことで、JavaScript はもっと自在に、もっと楽しく扱えるようになります!

こんかいのお話は、以上です。それでは、よきコーディングライフを!