👻

宣言的プログラミングをバックエンド実装に取り入れる実装例の考察

2023/10/28に公開

はじめに

こんにちは。
株式会社CHILLNNという京都のスタートアップにてCTOを務めております永田と申します。

2021年に書かれたこちらの記事で、現代のソフトウェア開発の主流は宣言的プログラミングだと言及されています。

上記の記事では、宣言的プログラミングの代表例としてReactとk8sが紹介されています。
実際、宣言的プログラミングといえば、Reactを用いた宣言的UIでのフロントエンド実装をイメージする方も多いかと思います。

仮にフレームワークを想定しなかったとして、宣言的プログラミングを行うとはどういうことでしょうか?

私はこの問いに対する実践的な回答を持ち合わせておらず、自分が上記の記事を正しく実用的に理解できているとは思えなかったため、宣言的プログラミングをより深く理解する必要があると感じ、本記事を執筆することにしました。

本記事では、宣言的プログラミングを実用的に捉え、フレームワークを前提としない場合の宣言的プログラミングに則ったバックエンドの実装例を考察します。

宣言的プログラミングとは何か

宣言的プログラミングは、命令的プログラミングと対をなす概念です。

Wikipedia上での宣言的プログラミングの定義は以下のようになっています。

宣言型プログラミングは、現行式枠外の外部状態への代入コマンド、および外部状態の現行式への影響(副作用)といった命令的な性質を持たないパラダイム

先ほどもあげたこちらの記事では、

「時系列に基づいた状態の宣言と、フレームワーク側による状態遷移処理が宣言的プログラミングの中心的な概念である」

と説明しています。

より感覚的な説明を探していたところ、Quora上でこんな投稿を見つけました。
以下に引用させていただきます。

例えば、下記のような図があり、プログラムの目的は「地点Aから地点Bへの移動」です。
Alt text
1つ目のパターンとして、下記の2つの命令を組み合わせることが許可されているとします。
Alt text
あなたは

→↑↑→→ 

というコードを書き、プログラムとして提出し、以下のように目的が達成されたことが確認されます。
Alt text
これが命令的プログラミングだとします。
命令的プログラミングの場合、

→→→↑↑

でもいいし

↑↑→→→

でも同じ結果を得ることができます。
2つ目のパターンとして、座標の概念を導入します。
そして、Aを座標(0,0)、Bを座標(3、2)とします。
Alt text
パターン2では「座標の移動を自動で計算する関数F」が与えられていて、開始地点と終了地点を入力するだけで自動で移動が計算されます。

A=(0,0)
B=(3,2)
移動=F(A,B) 

Alt text
Fが実際に計算ではじき出す移動が、以下のいずれかで表されるものであるかは分かりません。(最適化次第です)

(中略)

しかし、目的である"移動"を計算してくれる関数Fを使えば、あなたはAの座標とBの座標を宣言し、両方をFに与えて得られる結果が移動であると宣言すれば、最終的な目的を達成するプログラムになります。こうして関数Fの出現によって、宣言型プログラミングが可能になりました。

上記の2つの例を見ると、前者に比べ後者では、最終的な目的に至る命令が関数によって隠蔽されています。つまり、状態遷移を実現するにあたって関心ごとが以下の2つに分割されていると解釈することができます。

  1. どのように目的地に至るかを命令すること
  2. 最終的にどこに行くべきか宣言すること

以下は、こちらのスライドからの部分的な引用ですが、
命令的プログラミングがプログラムを 「計算機への命令」 だと捉えていたのに対し、宣言的プログラミングでは 「関数を利用して値を得る計算」 だと捉えると、両者の違いが理解できるかと思います。

宣言的と命令的の違いを掴んだところで、具体的な実装例の考察を進めていきましょう。

バックエンドへの宣言的プログラミングの適用

文頭で紹介した言葉を再度引用します。

「時系列に基づいた状態の宣言と、フレームワーク側による状態遷移処理が宣言的プログラミングの中心的な概念である」

このフレームワークという観点を少し掘り下げてみます。

フロントエンドの責務を「画面を介して、ユーザーとのインタラクションを行うこと」だと考えてみます。前提となる画面というインターフェースは、WebブラウザやNativeAppなど具体性が高く複雑で、描画ロジックはビジネスロジックからは切り離されていることが理想だと同意していただけると思います。すでに我々の目の前にはReactが存在しているので、描画処理を丸投げすることができるフレームワークがカバーすべき状態遷移処理のスコープとその有用性が想像できます。

同様にバックエンドの責務を単純化するなら「クライアントの要求に対して、ドメインモデルの状態遷移を行うこと」だと考えられるでしょうか。バックエンドではフロントエンドに比べて責務を達成するための自由度が高く、ドメインモデルの状態遷移もアプリケーションによってさまざまです。ドメインモデルの状態遷移はバックエンドの責務それ自体であると考えることもできるため、フレームワークが担える状態遷移処理のスコープは曖昧で有用な共通処理を見出すことは難しく感じます。

では、本記事のテーマとして挙げさせていただいた、フレームワークを前提としない宣言的プログラミングはどのようなものになるのでしょうか?

具体的な実装例

宣言的プログラミングの表現形について具体例を探していると、一休CTOの伊藤さんが先日のQiitaのカンファレンスで登壇された際に利用された資料をあげてくださっているのをみつけました。

資料のテーマは「関数型プログラミングと型システムのメンタルモデル」というものでしたが、扱っているテーマは近く、実際に宣言的プログラミングに言及されていました。

もう少し掘り下げて、実際に伊藤さんが行われている実装の詳細を調べたところ、こちらの動画を見つけました。

動画の中で「TypeScriptによるGraphQLバックエンド開発 ──TypeScriptの型システムとデータフローに着目した宣言的プログラミング」について解説されています。

実装詳細

簡単にエッセンスを紹介します。

紹介されているアーキテクチャでは、バックエンドの処理をイベントを契機に遷移する状態遷移であると捉え、
Alt text
IOで状態遷移を行う純粋関数をサンドイッチし、繋げていくことで処理を進めていきます。
Alt text
neverthrowを利用することでエラー処理をライブラリに任せ、関数型言語のような書きごこちでメソッドを繋げて実装を行なっていました。

DBへのデータ参照など外部アクセスが必要な場合は、カリー化を行うことで関数モジュールをDIしています。登壇時点ではすでに一休の商用プロダクトでこのアーキテクチャを試しており、全てのコードをデータの型定義と純粋関数だけで書き上げているとおっしゃられていました。

動画内では実際に運用されているコードも紹介されていたためぜひ一度見てみてください。

とにかく制約がきつく、その恩恵として処理の流れが高い抽象度で連続的に表現されており、理想的なコードだと感じました。また、全ての関数がイベントを起点としたドメインモデルの変換(状態遷移)を目的としているため、関数の責務が明確になっているかつ、将来的なドメインモデルの変更を吸収する余地が多分に残されていると感じました。

実装例を見て思ったこと

前章で紹介した実装例から自分が感じた有用性は以下のような点です。

  1. イベントを起点として状態を遷移させるというメンタルモデルを持つことで、一つ一つの関数の責務を強く意識し続けることができる
  2. 関数を呼び出すコードで状態遷移の単一データフローが端的に示されているため、特定のフローを理解するコストが低い

ドメインへの理解が浅いとしばしば手続的な処理のネストが深くなっていってしまって、処理のフローを追うことが困難になっていきます。
紹介されていたアプローチでは、イベントを起点として全てのコードを特定の状態から特定の状態への変換だと認識するという強い制約を構造的に加えていることで、ドメインに対する誤った解釈をする事態を防ぐことができていると感じました。

バックエンドにおける宣言的プログラミングとは

本記事でのリサーチを通して自分が得た理解は、
以下のようなメンタルモデルで実装を行うことで、ある程度宣言的プログラミングに準拠した実装を行うことができるということです。

  • プログラムを、イベントを起点としたドメインモデルの状態遷移だと認識する
  • イベントの前後で、ドメインモデルの状態が別のドメインモデルに遷移する
  • ドメインモデルの状態は型として、変換処理はコードとして別々に表現される

まとめ

紹介されていた実装例は0→1での開発について紹介されたものです。
すでに運用中のシステムを宣言的プログラミングに移行していくためには、かなりドラスティックな変更になると考えられるため、漸進的なアプローチを行なっていく必要があります。

本記事を書いている中でとても重要だと思った観点は、
本番運用しているシステムのアーキテクチャに新たなパラダイムを導入する余地を残しておくべきだという点です。
引用したスライドの中に、以下のような言及がありました。

業務ロジックをIOから切り離し、計算機への命令ではなく純粋なロジック(計算)として考えられるようにしたい

これにより色々なパラダイムを適用できる可能性が生まれる

この意見に深く共感しました。

システムの複雑性は運用を続ける中で常に増大し続けます。一方で、それらのカウンターとして新たなパラダイムも次々に生まれていきます。システムを、できるだけ小さく独立したパーツを組み合わせて作り上げることで、漸進的にパラダイムを導入していくことが可能になります。

自分は経営者でもあるのですが、事業というのは常に伸び続けるわけではなく、必ず停滞するタイミングがあります。エンジニアの責務として、停滞したタイミングでソフトウェアの寿命を伸ばすようなリファクタリングを行うことが期待されます。その際、既存のアーキテクチャの課題を特定するためには、さまざまなパラダイムを事前に試しておくことで、現在の自社のアーキテクチャを相対化しておくことが必要だと思っています。

本記事を執筆する上で、システムアーキテクチャの範疇に異なるパラダイムへの受容性という観点を取り入れることができ、個人的に大きな学びだったと感じました。

以上、ご覧いただきありがとうございました。

参考

CHILLNN Tech Blog

Discussion