この記事では、継続渡しスタイル(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);
getLength
はstr.length
を返していたが、getLengthCps
はstr.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 を使っていない、以下のコードがあったとする。
少し長いが、getEmployeesPageProps
とgetOfficesPageProps
の概要さえ理解できれば問題ない。
これらの関数の返り値をコンポーネントに渡して 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' ] } }
getEmployeesPageProps
とgetOfficesPageProps
はどちらも、以下の処理を行っている。
- 引数として渡された
sessionId
を使って認証を行う - 認証に失敗した場合はその旨を返し、処理を終了する
- 認証に成功した場合は手に入れた
companyId
を使ってEmployees
もしくはOffices
を取得し、それを含んだGetPageProps
を返す
このうち、1
と2
は全く同じコードなので、これを共通化したい。
CPS を使って「認証が成功した後の処理を引数として渡す」という書き方にすることで、これを実現できる。
以下のcontinueWithAuth
は「認証が成功した後の処理」をcont
として受け取り、認証が成功したときにのみcont
を呼び出している。
こうすることで、1
と2
の処理を共通化し、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);