📘

TypeScript コーディングテクニック #3 【関数編その1】

2024/07/22に公開

はじめに

TypeScript のコーディングテクニックを紹介するシリーズの第 3 回です。

前回で内容を予告していたのですが、ほぼ半年ぶりの投稿になってしまいました。またゆっくり投稿していこうと思います。

想定する読者は、プログラミングをある程度経験したうえでさらにコード品質を高めたい方です。初心者向けではないと思います。

第 3 回は、関数でどのようなことができるかを整理していきます。TypeScript と題していますが、 TypeScript に限らずあらゆるプログラミング言語に通ずる考え方だと思います。

関数でできること

処理の抽象化

一連の処理を関数にまとめて名前を付けることで、簡潔に処理を記述できます。全体的な処理の流れを見通しやすくなり、ソースコードのメンテナンスがしやすくなります。

const load = async (): Promise<Data> => {
  // 100 行の複雑な処理...
};
const modify = (data: Data, modification: Modification): Data => {
  // 100 行の複雑な処理...
};
const save = async (data: Data): Promise<void> => {
  // 100 行の複雑な処理...
};

const applyModification = async (modification: Modification): Promise<void> => {
  // 300 行の処理を抽象化して 3 行で済ませる
  const data = await load();
  const modifiedData = modify(data, modification);
  await save(modifiedData);
};

処理の共通化と再利用

処理を他の処理と共通化して、使いまわすことができるようにします。ソースコードの記述量が減り、メンテナンスコストも削減できます。

ただし裏を返せば、1 つの関数の修正が複数の処理に影響してしまいます。共通化と再利用のためには丁寧なコード設計が必要です。

const applyModification = async (modification: Modification): Promise<void> => {
  const data = await load();
  const modifiedData = modify(data, modification);
  await save(modifiedData);
};

const reset = async (): Promise<void> => {
  // `save` 関数を再利用して 100 行節約
  await save(defaultData);
};

スコープと隠蔽

関数の内部で定義した変数はその関数の外から参照することができません。変数を参照できる範囲のことをスコープといいます。

すべての関数は独自の関数スコープを持ち、内部で宣言された変数を外部から隠蔽します。

内部の変数は隠蔽されているので、他の関数スコープの変数と名前が重複することを考慮する必要はありません。

const load = async (): Promise<Data> => {
  // ローカル変数 `dataAccess` は外部からは参照できない
  const dataAccess = await Reader.create();
  // ...
  return result;
};
const modify = (data: Data, modification: Modification): Data => {
  // 中間データを変数としておいているが、外部から参照できるのは結果だけ
    const intermediateData1 = ...;
    const intermediateData2 = ...;
  // ...
  return result;
};
const save = async (data: Data): Promise<void> => {
  // `load()` の中で使っている変数と同じ名前だが、違う役割
  const dataAccess = await Writer.create();
  // ...
};

const applyModification = async (modification: Modification): Promise<void> => {
  // `dataAccess` や `intermediateData` に直接アクセスすることはできない(隠蔽されている)
  const data = await load();
  const modifiedData = modify(data, modification);
  await save(modifiedData);
};

引数に関数をとる関数

JavaScript の関数は、変数やオブジェクトのプロパティ、関数の引数や返り値として扱うことができます。このような値を「第一級オブジェクト」といいます。

引数に関数をとることで、一部の振る舞いを後から自由に設定でき、処理の共通化をより柔軟に行うことができます。

Array.prototype.map()new Promise() などの組み込み機能にもこの設計が使われています。

const difference = <T, U>(
  data: readonly T[],
  other: readonly U[],
  // equal には任意の比較関数を渡すことができる
  equal: (l: T, r: U) => boolean
): T[] => {
  return data.filter((l) => !other.some((r) => equal(l, r)));
};

const deletedElements = difference(
  before,
  after,
  // オブジェクトのプロパティで比較する
  (b, a) => b.id === a.id
);

返り値で関数を返す関数(関数閉包)

JavaScript の関数は第一級オブジェクトなので、関数の返り値として関数を返すことができます。

返り値の関数は、もとの関数のスコープにある変数を参照できます。これを関数閉包(クロージャ)と呼びます。

オブジェクト指向ではクラスのメソッドとプライベートフィールで実現する機能ですが、関数が第一級オブジェクトであれば関数で実現できます。

const createCounter = (
  initialCount = 0
): {
  readonly increment: () => number;
  readonly decrement: () => number;
  readonly reset: () => number;
  get count(): number;
} => {
  let count = initialCount;
  // 返り値の関数を経由して `count` を参照・更新できる(関数閉包)
  // `count` の値を直接書き換えることは禁止(隠蔽)している
  return {
    increment: () => ++count,
    decrement: () => --count,
    reset: () => (count = initialCount),
    get count(): number {
      return count;
    },
  };
};

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.increment(); // 3
counter.decrement(); // 2
counter.count; // 2
counter.reset(); // 0

おわりに

今回は JavaScript の関数でできることについて紹介しました。

次回は関数についてより深く理解するために、「処理の分類」について紹介しようと思います。

Discussion