30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

継続渡しスタイルを使ってプログラムの見通しをよくする

この記事では、継続渡しスタイル(continuation passing style、以下 CPS)の概要と、CPS の活用例を書いていく。

この記事に出てくるコードの動作確認は TypeScript の4.7.4で行っている。

後続の処理を引数として渡す

関数が終わった後に実行される後続の処理をその関数の引数として渡すスタイル、そういったプログラムの書き方を、 CPS と呼ぶ。

例えば、以下のようなコードがあるとする。

const getLength = (str: string): number => str.length;

const n: number = getLength("hello");
console.log(n); // 5

getLength("hello")の結果をnに代入し、それを使ってconsole.logを実行している。

getLengthを CPS に書き換えると次のようになる。

const getLengthCps = <T>(cont: (x: number) => T, str: string): T =>
  cont(str.length);

getLengthstr.lengthを返していたが、getLengthCpsstr.lengthを「関数が終わった後に実行される後続の処理」であるcontに渡している。

numberを受け取る関数ならどんなものでも、contとして渡すことができる。

const getLengthCps = <T>(cont: (x: number) => T, str: string): T =>
  cont(str.length);

getLengthCps(console.log, "hello"); // 5
console.log(getLengthCps((length) => length * 3, "foo")); // 9

CPS を使ったリファクタリング

CPS の利用例のひとつとして、特定の条件を満たしたときにのみ後続の処理を実行する、というプログラムを書いてみる。

CPS を使っていない、以下のコードがあったとする。
少し長いが、getEmployeesPagePropsgetOfficesPagePropsの概要さえ理解できれば問題ない。
これらの関数の返り値をコンポーネントに渡して View を作ることを想定している。

type Employees = string[];

type Offices = string[];

type GetPageProps<T> = (
  sessionId: string
) => { ok: false; message: string } | { ok: true; data: T };

const CORRECT_SESSION_ID = "123";

const auth = (
  sessionId: string
): { ok: true; companyId: string } | { ok: false } => {
  if (sessionId === CORRECT_SESSION_ID) {
    return {
      ok: true,
      companyId: "1",
    };
  }
  return {
    ok: false,
  };
};

const getEmployeesPageProps: GetPageProps<{ employees: Employees | null }> = (
  sessionId
) => {
  const authResult = auth(sessionId);
  if (authResult.ok === false) {
    return {
      ok: false,
      message: "Unauthorized",
    };
  }

  const dummyDb = new Map([["1", ["Alice", "Bob"]]]);

  return {
    ok: true,
    data: { employees: dummyDb.get(authResult.companyId) ?? null },
  };
};

const getOfficesPageProps: GetPageProps<{ offices: Offices | null }> = (
  sessionId
) => {
  const authResult = auth(sessionId);
  if (authResult.ok === false) {
    return {
      ok: false,
      message: "Unauthorized",
    };
  }

  const dummyDb = new Map([["1", ["London", "Paris"]]]);

  return {
    ok: true,
    data: { offices: dummyDb.get(authResult.companyId) ?? null },
  };
};

console.log(getEmployeesPageProps("xyz")); // { ok: false, message: 'Unauthorized' }
console.log(getEmployeesPageProps("123")); // { ok: true, data: { employees: [ 'Alice', 'Bob' ] } }
console.log(getOfficesPageProps("xyz")); // { ok: false, message: 'Unauthorized' }
console.log(getOfficesPageProps("123")); // { ok: true, data: { offices: [ 'London', 'Paris' ] } }

getEmployeesPagePropsgetOfficesPagePropsはどちらも、以下の処理を行っている。

  1. 引数として渡されたsessionIdを使って認証を行う
  2. 認証に失敗した場合はその旨を返し、処理を終了する
  3. 認証に成功した場合は手に入れたcompanyIdを使ってEmployeesもしくはOfficesを取得し、それを含んだGetPagePropsを返す

このうち、12は全く同じコードなので、これを共通化したい。
CPS を使って「認証が成功した後の処理を引数として渡す」という書き方にすることで、これを実現できる。

以下のcontinueWithAuthは「認証が成功した後の処理」をcontとして受け取り、認証が成功したときにのみcontを呼び出している。
こうすることで、12の処理を共通化し、3として任意の処理を渡せるようになる。

const continueWithAuth = <T>(
  cont: (companyId: string) => { ok: true; data: T },
  sessionId: string
): ReturnType<GetPageProps<T>> => {
  const authResult = auth(sessionId);
  if (authResult.ok === false) {
    return {
      ok: false,
      message: "Unauthorized",
    };
  }
  return cont(authResult.companyId);
};

const getEmployeesPageProps: GetPageProps<{ employees: Employees | null }> = (
  sessionId
) =>
  continueWithAuth((companyId) => {
    const dummyDb = new Map([["1", ["Alice", "Bob"]]]);
    return {
      ok: true,
      data: {
        employees: dummyDb.get(companyId) ?? null,
      },
    };
  }, sessionId);

const getOfficesPageProps: GetPageProps<{ offices: Offices | null }> = (
  sessionId
) =>
  continueWithAuth((companyId) => {
    const dummyDb = new Map([["1", ["London", "Paris"]]]);
    return {
      ok: true,
      data: {
        offices: dummyDb.get(companyId) ?? null,
      },
    };
  }, sessionId);

参考資料