Google Testing BlogにSimplify Your Code: Functional Core, Imperative Shellという記事があったので紹介

はじめに

Google Testing BlogにSimplify Your Code: Functional Core, Imperative Shellというタイトルの記事があったので紹介します。

https://testing.googleblog.com/2025/10/simplify-your-code-functional-core.html

Simplify Your Code: Functional Core, Imperative Shellの記事では、コードを簡素化するFunctional Core, Imperative Shell(以降FCIS)についてかなり短く解説されています。ただそれが短く省略されていることもあるので詳しく紹介&解説したいと思います。

動画

動画で知りたい人のために、Gemini Notebookでこのブログを動画にしました。

そもそもなぜFCISが重要か

WebアプリケーションやモバイルアプリケーションでDBやネットワークを利用する場合、 それぞれの処理が複雑にからみあったコードを書いてしまうことがあるのではないでしょうか。 そうなると、 単純に処理の流れを追いづらくなるだけでなく、実際にDBやネットワークアクセスをしないテストコードを書くためにテストダブル(モックなどの擬似的なオブジェクト)を大量に書くことにもなると思います。

それを解決する基礎となる考え方がFCISのパターンです。これはコードをシンプルにするための基礎であって複雑なことではありません。 単にコードをシンプルにすることなので、もしかしたらFCISを知らなくてもこの考え方でコードを書いている人はいるかもしれませんが、FCISというパターンとして言語化されていることには意味が大きいはずです。

さらに、AIを使ってコードを書いていく時代でもFCISの考えを使うことによるデメリットはほぼないため、知っておくと良い話題でしょう。

FCISとは何か

記事のテーマはコードにビジネスロジックと副作用が絡み合った状態になっている場合に、 FCISを検討したらどうでしょうという内容です。

FCISはコードを次に2つに分離します。

  • Imperative Shell(命令型なシェル/外殻)
  • Functional Core(関数型コア)

記事ないの図としては外側に外側にシェルがあり、それがCoreを包み込んでいます。 こちらでアスキーアートで表現してみます。

Imperative Shell
┌──────────────────────┐
│                      │
│    Functional Core   │
│                      │
└──────────────────────┘

ブログではサンプルコードを引用しさらに改変しつつ説明してみます。

サンプルはまずユーザーに有効期限切れ通知メールを送信する処理が書かれていて、 ロジックと副作用が混在する悪い例です。

// ユーザーに有効期限切れ通知メールを送信する処理
// 悪い点: ロジックと副作用が混在している
function sendUserExpiryEmail (): void {
  // DBからユーザー一覧を取得
  for (const user of db.getUsers()) {
    // サブスクの期限がまだきていないなら、何もしない
    if (user.subscriptionEndDate > Date.now()) continue;
    // フリートリアルなら、何もしない
    if (user.isFreeTrial) continue;
    // メール送信
    email.send(user.email, "アカウントの有効期限が切れました " + user.name + ".");
  }
}

これをテストしようとしたらかなり大変です。

  • DBはダミーデータを返すようにしたい
  • Date.now()はそのままだと現在時間
  • email送信も実際には送信しないようにしたい

つまり、テストダブルを用意しDIしたりすることになるはずです。

これをFCISではビジネスロジックと副作用と分離し、 Functional Coreはビジネスロジックを純粋な処理だけとすることでテストしやすく、 Imperative ShellはDB呼び出しやメール送信を副作用とし、命令的に処理するようにしています。

まずはFunctional Coreで書き換える例

// ユーザーの配列と期限を引数とし、サブスク切れユーザーを返す
function getExpiredUsers(users: User[], cutoff: Date) : User[] {
  return users.filter( user =>
    user.subscriptionEndDate <= cutoff && !user.isFreeTrial
  );
}

// ユーザーの配列を引数とし、サブスク切れメールのアドレスと本文を配列で返す
function generateExpiryEmails ( users: User[] ) : Array<[string, string]> {
  return users.map( user =>
    ([user.email, “アカウントの有効期限が切れました “ + user.name + “。”])
  );
}

Imperative ShellでFunctional Coreを使って書き換える例。

// Imperative Shell
const users = db.getUsers();
const now = Date.now();
email.bulkSend(
  generateExpiryEmails( // Functional Coreの関数
    getExpiredUsers( // Functional Coreの関数
      users, now
    )
  )
);

これでImperative Shellという外側に対して、 内側にFunctional Coreが来ています。

さらに別のサンプルとして、Functional CoreはそのままにImperative Shellを別にしても使えるよというのも示されています。

// ユーザーにリマインダーメールを送信するFunctional Coreな関数
function generateReminderEmails(users: User[]) : Array<[string, string]> {
  return users.map(user => [
    user.email,
    `サブスクが次の日にちに切れます ${user.subscriptionEndDate.toDateString()}, ${user.name}.`
  ])
}

const fiveDaysFromNow = new Date(
  Date.now() + 5 * 24 * 60 * 60 * 1000 // 今日+5日
)

email.bulkSend(
  generateReminderEmails( // 作成したFunctional Core
    getExpiredUsers ( // 先ほどの例と同じFunctional Core
      db.getUsers (), fiveDaysFromNow
    )
  )
);

getExpiredUsersはサブスク切れかどうかを引数として与えられた日にちと比較して判断するものでした。 その日にちを+5日すると、5日後より前にサブスク切れするユーザーにメールを送るようです。

これで、Functional Coreである関数のテストはテストダブル(つまりモックやら)なしに高速に行えるようになりました。 Functional Coreのテストで高速にさまざまなパターンをテストできもするでしょう。 また、ビジネスロジックが変わりメールを送る条件が変わったとしても、 Functional Coreのテストを書き換えてそれが成功しているのであれば、 少なくともその変更を副作用の処理とは切り分けて確認できます。

感想

処理したいコードの中に純粋にできるロジックと副作用がまざっていると読みづらいし、テストがしづらい。純粋なロジックには大抵の場合、その処理の要件(やりたいことや、やる必要のある条件)があり、それをテストしやすくするとミスを少なくすることができる。DBやネットワーク通信はシステムの内側の場合もあるがコードの外界からの情報であり、それを副作用として分離することで純粋なロジックを守れる。副作用を分離すると言っても特別なことじゃない。単にFCISでは境界線を意識するということを言っているんだと思います。

その他

Googleのサンプルは間違っている

皆さんは気が付きましたか?

私のこの記事ではサンプルに手を加えていますが、 Googleの記事はサンプルコードが間違ってるように思います。

サンプルそのまま引用します。

// Sending a reminder email to users
function generateReminderEmails(users: User[], cutoff: Date): Array<[string, string]> {...}
const fiveDaysFromNow = ...
email.bulkSend(generateReminderEmails(getExpiredUsers(db.getUsers(), fiveDaysFromNow)));

generateReminderEmailsには引数が2つ定義してありますが、 二つ目のcutoff: Dateを使っていません。

そもそもuser.subscriptionEndDateがあるってことはサンプルの一つ目で示していて、 引数のcutoff: Dateはいらないし、 あっても5日以内にサブスクが切れますという曖昧なメールになります。 メールが遅れて届くかもしれないんで、そんなサーバーの生成日付で曖昧なことにする必要はなく、 サブスクが切れる日付を伝えればいいだけでしょう。

そもそもサンプルコードの関数に未使用の引数を定義するのは悪手でしょうから、ただのミスだと考えます。

Tech on the Toilet (TotT)

Google社内で実施されているトイレの中に印刷した技術記事を貼る活動があり、 そこで貼られた記事内容をブログにしたということだそうです。 トイレ休憩時に読むようなボリュームで、学習と成長を支援するって感じらしいです。