関数プログラミング実践入門 ──簡潔で、正しいコードを書くために
番外編●特別コラム「関数プログラミングから見る,Immutable Infrastructureとは? ―問題の複雑化に対応するための共通点を探る」
長らく歴史のある「関数プログラミング」ですが,最近,種々の条件が揃いはじめ,ますますよく登場するキーワードになりつつあるようです。『関数プログラミング実践入門 ──簡潔で,正しいコードを書くために』(WEB+DB PRESS plusシリーズ,技術評論社,2014年11月発売)では,プログラミングの観点で正面から,関数プログラミングについて解説しています(この点については,別記事の「本書について」および「本書の構成」をご参照ください)。
しかし,ここ最近,プログラミング界の範囲を越え,インフラ界隈でも,しばらく前からホットな話題である「Immutable Infrastructure」などで「関数プログラミング」「関数型言語」という単語が時折登場してくる今日この頃。しかし,実際のところ,「えぇ,インフラで?」と,どのような関係があるの? そもそも関係があるの? などなど,言及されている詳しい背景を含めて,いまひとつわかりにくいのが現状ではないでしょうか。
先に答えを明かすと... 実際のところ,あくまで話題/アイディアレベルで関係がある程度で,それほど関係はありません。そのため,書籍『関数プログラミング実践入門』(以下,本書)ではインフラについては取り上げません。
ただ,普段なかなか触れられることは少なく良い機会なので,以下本記事では「番外編」として,さらに見聞を広げるべく,関数プログラミングから見たときのImmutable Infrastructureについて,現状を簡単におさらいしつつ少し考えてみることにしましょう。
「関数プログラミング」と「Immutable Infrastructure」の関係
本書中では,関数プログラミングを念頭に置いた関数型言語においては,変数の中に入っている値を変更する破壊的代入は,禁止されているか極力制限されていると説明しています。つまり,変数の中身は一度決まったら「Immutable」(不変)であり,状態(State,ステート)を持ちません。
一方,インフラの世界では「Immutable Infrastructure」という考え方が広まってきています。
- 一度構成したコンポーネント(ホスト,コンテナ等)の内容を変更しない
- 同じ内容のコンポーネントをいつでも作成できる
- 変更するのではなく,新規に作成して切り換える
- 不要になった古い構成のコンポーネントは破棄する
ことにより,簡潔で正しいデプロイを実現します。
コンポーネントを「変数」に,コンポーネントの設定/構成内容を「値」に,それぞれ置き換えて考えれば,破壊的代入がないことと,Immutable Infrastructureはとても良く似ています。共に,変数に今何が入っているか,コンポーネントが今どうなってるか,といった状態を気にしないようになっています。元々,Chad Fowler(チャド・ファウラー)氏がImmutable Infrastructureを紹介したブログ記事でも,関数型言語との関係が述べられていました。
「不変性」の導入は,プログラミングにおいても,インフラ構築においても,問題の複雑化に対応するための一つの解決策になっています。
「変更」を前提としたインフラ構築の問題点
一度構築したコンポーネントを変更していく場合,手順書や証跡(何らかが行われたことを示す証拠/痕跡)の作成/管理がどうしても煩雑になります。また,手順書は大抵スクリプトやコードに加え,自然言語で記述されるでしょう。誰が読み取っても同じように構築できるかと言われると一抹の不安が付き纏(まと)います。
「Infrastracture as Code」ということで,Puppet,ChefやAnsibleといったオーケストレーションツール(構成管理ツール)を利用していても,昨日構築したときと同じように今日も「manifest」「recipe」「playbook」が適用でき,しかも,本当に同じ状態になってくれるかは定かではありません。外部環境が変化するためです。
たとえば,APTやRPM,Portageといったパッケージマネージャのリポジトリは,アップデートなどで時々刻々と変化します。バージョンを指定しない限り,同じバージョンのパッケージが入らないかもしれません。逆に,バージョン指定していると,リポジトリからそのバージョンは消えてしまっていて,インストール自体が失敗するかもしれません。
オーケストレーションツールによる操作は,通常,羃等性(べきとうせい,何度適用しても一度適用するのと同じという性質)が求められます。多くのツールは羃等なオペレーションを行うモジュールを提供しており,ユーザはそれを利用します。しかし,ここに時間の経過をはじめとした外部環境の変化が入ってくると,羃等性だけでは同じ状態が作れない可能性が生じます。
外部の環境に依存して操作の結果が変わってくるということは,本書で説明しているプログラミングの概念に対応させると「副作用を持つ」ということに対応します。入力だけから出力が決まらず,外部要因によっても出力が変わるものが副作用です。オーケストレーションツールによる操作は,理想的には,今の状態のコンポーネントを入力に,次の状態のコンポーネントを出力する
コンポーネント -> コンポーネント
という「副作用を扱う必要のない」関数相当でなくては,「いつ実行しても同じ」にはなりません。しかし,実際には,
コンポーネント -> IO コンポーネント
というIOモナドを伴う関数(IOモナドアクション)に相当します。本記事ではモナドやIOモナドについて詳しく説明できませんが簡単に言うと,IOモナドとは「副作用を扱うことのできる」しくみです。プログラミングにおいては,副作用はテストの難しさなどに繋がるとされます(本書では「副作用の扱い難さ」についても取り上げています)。プログラミングのみならず,インフラ構築においても,「副作用」は我々の前に立ちはだかるのです。
問題点に対する解決策
問題を解決するには,恐らくいくつか方法があるでしょう。それは,
- 変動する外部環境も内部化してしまう
- 一度しか操作を適用しない
などで,後者が,LXC(Linux Containers)やDockerといったコンテナ技術と結び付き,Immutable Infrastructureに繋がります。
変動する外部環境の内部化
「変動する外部環境も内部化してしまう」とは,変動するような外部環境を利用しないという対応になります。つまり,外部環境を「不変」にしたものだけを利用しようという試みになります。たとえば,パッケージリポジトリの問題であれば,望むバージョンのパッケージだけが含まれるミラーリポジトリを構築しておき,そこからのみインストールされるようにするといったような対策です。
本書で解説しているモナドの文脈で考えるのであれば,「実行時点の外部環境に依存させるためにIOモナドアクションで記述していた操作を,外部環境をある時点/状況に固定することでReaderモナドアクションで記述できるようにした」という対応に相当します。Readerモナドとは「参照する環境を共有することのできる」しくみで,Readerモナドアクションはそのしくみを伴う関数です。すべての操作の型を,
コンポーネント -> IOコンポーネント
という型から
コンポーネント -> Reader 固定された外部環境 コンポーネント
という固定環境を参照することのできるReaderモナドアクションの型へ変更します。すると,いつ操作を実行したとしても,Readerモナドアクションの実行時に与える環境(内部化した外部環境)が変わらなくなるため,同じ結果が得られるようになります。
既存のオーケストレーションツールだけで可能な方法であり,まったくのゼロからの再構築も可能な反面,ミラーリポジトリを用意するなど,内部化のための管理対象の増加が問題となります。また,どうしても内部化することができない要素もあるかもしれません。
一度しか操作を適用しない
「一度しか操作を適用しない」とは,操作した時点の結果を保存してしまい,コピーして使い回すということです。つまり,その時点で得られた結果だけを信用し「不変」にしようという試みです。
後にも先にも真に一度しか行われない操作であれば,副作用があるかどうかは外延的には(つまり,ブラックボックステスト的な方法で判明する性質としては)気にならなく(わからなく)なります。
コンポーネント -> IOコンポーネント
であった操作の型から,
コンポーネント -> コンポーネント
と単純にIOモナドの文脈が外れるようなものです。興味があるのは各々の操作における実行時という1点のみで,その後は保存された結果だけを使います。
大前提として一度のみしか操作を考えないため,ゼロからどのように構成してきたかとその結果は得られても,もう同じ操作で一度ゼロからの再構成ができるとは限りません。しかし,余計な管理対象を増やすこともありません。せいぜい結果の保存と参照を行うしくみが必要なだけです。また,一度しか操作しないので羃等性がなくても問題ないということになります。
そして,結果の保存と参照をLXCやDockerのようなコンテナイメージという形で提供/実現することで,最近のImmutable Infrastructureの形になります。
まとめ
関数型言語では,副作用を扱う箇所/扱わない箇所を明確に切り分けることができるものがあり,プログラム設計に良い影響を与えています。
同様に,ステートレス(Stateless)なImmutable Infrastructureを利用する場合も,ステートフル(Stateful)な部分(DBやLogger)とステートレスな部分(アプリ等)を明確に切り分ける必要が生じ,こちらもまたアーキテクチャ設計に良い影響を与えています。
Immutable Infrastructureを便利に思いつつ,データが不変であるような言語は不便であると思うのであれば,それはダブルスタンダードと言えるかもしれません。逆もまた同様です。
Immutable Infrastructureについては,実際にこういった経緯でできあがったものかはともかく結果的に,関数プログラミングの知見をプログラミング以外に応用することが,問題解決のための有効な手段となっている例と見ることができるでしょう。ある知見を適用できるのは,その知見が得られた分野のみに限りません。視野を広く持ち,他分野へあるいは他分野から知見を取り入れることは,私達の世界を良いものにすることでしょう。