宣言するプログラムと命令するプログラム @Tomofiles
目次
はじめに
こんにちは、Tomofilesです。
今回は、プログラミングパラダイムに関するお話です。
初心者エンジニアにとっては、アルゴリズムを一から考えて、プログラムで表現できるようになるのが、開発者としての最初の関門かと思います。
しかし、アルゴリズムが構築できて、プログラムが組めるというのは、現在のソフトウェア開発のテクノロジーにおいては、全体における一部の基礎が理解できたことに過ぎません。
もちろん、とても大切な基礎ではあるのですが、プログラムのもう一つの側面も知っておかないと、現在のテクノロジーの動向を掴むのに非常に苦労することになります。
キーワードは、宣言的です。
前回までの記事にも、何回か登場していますね。
宣言的なプログラミングというのは、具体的にどういうことなのかを、この記事では考えていきたいと思います。
それでは、今回もちょっと長いですが、張りきっていきましょう。
プログラムとは伝統的には命令
プログラムというのは、コンピュータに動作を指示するための方法であり、何を・どこから・どこへ・どうする、という、はっきりとした具体的な手続きのことを指します。
アルゴリズムというのが、まさにこの手続きを実現するための方法論であり、コンピュータを利用する上での基本であると言えます。
この手の話は、突き詰めると数学や論理の世界に入ってしまうので、深入りはあまりしないほうがいいと思いますが、私は以下が分かれば良いかなと考えてます。(私も深い話は分かりません)
- 順次、分岐、反復
- 復帰(サブルーチン)
プログラム(アルゴリズム)とは、上から下へ順次実行され、何らかの条件を評価して分岐し、何らかの状態の間、指定された範囲を反復(ループ)し、これらの組み合わせにより、期待する結果を導き出すものです。
プログラムは、ときにサブルーチンという、ある目的を果たすまとまった手続きの塊を作り、メインルーチンから呼び出すように構成することで、可読性や保守性を高く保つことができます。
サブルーチンはプログラミング言語によって、関数や、メソッドと呼ばれていたりしますね。
ここまでは、コンピュータの基礎的な考え方の話なので、皆さんも知っていることと思います。
プログラムというのは、コンピュータの黎明期からの基本的な考え方は、コンピュータに命令をするということです。
命令は、具体的な手続きであり、アルゴリズムを用いて、課題を解決する手順を構築し、コンピュータが理解できる入力方法で表現(プログラム)します。
命令型プログラミングの弱点
このような、伝統的な命令型プログラミングには、さまざまなコンピュータ上の課題を自動で解決する手順を構築する力がありますが、伝統的に大きな弱点との戦いも強いられました。
アルゴリズムは人間のミスを誘う
初心者エンジニアの皆さんも、自分が書いたプログラムが思い通りに動かなかったという経験は、あるんじゃないでしょうか?
私たち開発者は、経験的に、プログラムというのはバグが混入しやすいもの、だということを知っています。
特に命令型プログラミングにおけるバグというのは、アルゴリズムつまりロジックが誤っているために発生するバグが、そのほとんどを占めていると言っても過言ではありません。
そういったアルゴリズムのバグは、伝統的に様々なレビューとテストで網羅的に検出し、取り除いてきたという歴史と実績があります。
さらに、命令型プログラミングは、変更に弱いという特徴もあります。
アルゴリズムは手続きであり、手続きというのは、順序と前後関係が重要であり、あとから途中に手順を追加したり除去したりするというのは、大きなリスクがあります。
特に、プログラムを作成した人と、修正する人が違う、または、同じ人でも、プログラムを作成した日と、修正する日が違う場合、前者と後者で、その絶妙なバランスをちゃんと把握できずに、アルゴリズムが崩壊した変更を加えてしまうことがあります。
このように、私たち開発者は、常に気を配って、細心の注意を払って、プログラミングに励むことが、求められるのです。
手順書もまた命令型
初心者エンジニアの皆さんは、ソフトウェアが稼働する本番環境にリリースをしたことがありますか?
リリースは、常に強い緊張感に晒されます。以前の記事でも、実稼働環境での作業は強い緊張感を伴うことを、紹介しましたね。
なぜ強い緊張感に晒されるのか。
それは、シンプルにミスが許されないからです。
リリースは、伝統的に手作業で進めないといけないものであり、人間が実行することにはミスが伴います。
ミスをしないために、手順書を作成してレビューし、作業漏れやコマンドのタイプミス、環境情報の間違い、手順の順序の間違い、などを検出して、防止に務めます。
手順書というのも、手続きによって構成されるので、順序と前後関係のバランスの問題が発生します。
長くメンテナンスされる手順書などは、ツギハギの手順になるため、ときに環境を破壊しかねないバグが混入することもありえます。
ソフトウェアの開発面だけでなく、運用面においても、手続きの扱いというのは、開発者たちを悩ませてきたのですね。
プログラムを宣言するとは
コンピュータは命令通りに動くからこそ
ここまで、伝統的なコンピュータへの指示の方法として、命令型プログラミングの特徴について見てきました。
命令型のアプローチ自体は、コンピュータに価値ある処理を自動的に高速に実行させるための、非常に有用な手段です。
ただ、そのメリットに対して、手続きの脆さというデメリットは、とても大きものでしたね。
コンピュータへの指示を、手続きという方法以外の、別のアプローチを使って、価値ある処理を実行する、という結果だけ享受できれば、もっと便利にならないでしょうか?
例えば、いちいち命令しないと動かないような指示の仕方ではなく、「こういう結果がほしいんだけど!」とか、「こういう状態にしたいんだけど!」って伝えたら、その通りに実行してくれるような。
期待する結果・状態だけ伝えれば、その過程は自動的に判断して、必要な処理を自分で組み上げて実行して、結果だけ示してくれる・状態を整えてくれる、みたいな。
このような、期待する結果・状態(対象の性質)をコンピュータに伝えて(宣言)、処理を実行させる方式を、宣言型プログラミングと言います。
手続きから解放されることで、ヒューマン・エラーの混入する余地は、ぐっと減ります。
開発者により宣言される期待する結果・状態は、手続き上の目的・ゴールであり、その過程には一切関与しません。
その過程は、あらかじめ用意してある様々なサブルーチンを用いて、目的・ゴールに到達できるように、コンピュータが自ら判断して(ときに開発者がサブルーチンを指定して)、サブルーチンを組み合わせて、実行していきます。
外から見れば、宣言的に動いているように見え、しかし、コンピュータの内部では、その宣言を達成するための手続きを自ら組み立てて、命令的に動いて実現しています。
これも、ある意味、抽象化による賜物であり、人とコンピュータの仲介役(宣言型プログラミングの特徴を備えるプログラミング言語)が、すごい頑張っている一例、と言えるかもしれないですね。
SQLも実は宣言型
ここから、宣言型プログラミングの例をいくつか見てみましょう。
まずは、SQLです。
SQLは、データベース(関係データベース)への問い合わせ言語と言われています。
例えば、SELECT文は、データベースから取得したいデータを表現するものであり、いかにしてそのデータを取得するか、という手続きは一切登場しません。
どのテーブルから(FROM句)、どのような条件で(WHERE句)、どのような形式の結果を(SELECT句)、取得したい! と宣言するだけです。
関係データベースの特徴として、制限・射影・結合・和・差・交わり、という機能があるということは、皆さんも新人教育などで習ったかもしれないですね。
この記事を書きながら、私もデータベースの概要について調べなおしていたのですが、色々忘れていたことが蘇ってきました。
私も新人の頃は、こういうわかりにくい話をされるので、データベースはとっつきにくいなと思ったものです。
重要なのは、こういった数学や論理の話ではなくて、宣言型言語というのは、元から様々な機能を備えていて、それをちゃんと把握した上で、宣言するプログラムを書かないといけない、ということです。
ソフトウェア開発というのは、ときに複雑なアルゴリズムを書いて、ゴリ押しで作りたい機能を無理やり作ってしまうことがあります。
ある意味、ロジック・手順・手続きというのは、誰でも考えれば構築できてしまう、という大きな力があるのです。(ただし、長大なアルゴリズムは、崩壊しやすいですが)
宣言型のプログラムというのは、こうはいきません。
元から用意されている様々な機能を知った上で、作りたい機能・状態を実現するための、宣言をしないといけないのです。
つまり、最低限の知識がないと、どうにもなりません。ゴリ押しも無理です。
だからこそ、自分で知識を吸収するようにしないと、宣言型プログラミングの感覚は、一向に習得できないのです。
関数型はミスを防ぐ
次に、関数型プログラミングです。
関数型プログラミングは、とても奥が深い領域であり、極めようとするとかなり頑張らないといけないです。
特に、純粋関数型と呼ばれる、関数に数学的な純粋性を追求する系のプログラミング言語は、一筋縄ではいかないでしょう。(私も、純粋関数型は、よくわかりません)
ですが、関数型というのは、参照透過性という性質に敏感であり、この参照透過性によって副作用を防ぐことができる、というメリットがあります。
副作用というのは、Wikipediaでは以下のように説明されています。
現行計算枠外のいずれかの情報資源が変化するのと同時にいずれかの関数の評価過程も変化してしまう現象
かなりわかりにくい説明ですが、簡単に言うと、
参照透過性 | ある目的を果たすサブルーチン(関数)は、 どこにも誰にも依存せず、同じ入力には必ず同じ出力を返す性質 |
副作用 | サブルーチンが誰かに依存していて、その依存先の変化によって、 入力に対する出力が変化してしまうこと |
という感じになります。
つまり、関数型プログラミングにおける関数のルールを守って、効果的に使うことで、副作用を排除した処理が記述でき、入力と出力の関係がシンプルになるので、複雑さを抑えたプログラムを書くことができるのです。
まだ、今回の宣言型の話と繋がらないですよね。
実はもう一つ、関数型における重要なトピックがあります。
それは、高階関数という考え方です。
高階関数は、関数の引数に関数を渡すことができ、また、関数の戻り値に関数を返すことができる、という関数のことを言います。
これまでお話してきた通り、関数というのは、サブルーチンであり、サブルーチンというのは、ある目的を果たす手続きの塊のことでした。
参照透過性により、関数は、同じ入力には必ず同じ出力を返す性質を持ち、高階関数には、関数の引数・戻り値に関数を授受できる、ということが実現できました。
これらを組み合わせると、何が起こるでしょうか?
例えば、あるデータのコレクションがあったとき、そのコレクションから、ある条件を指定して、ほしいデータを取得したいとします。(つまり、SQLと同じことがしたい)
このとき、もし、コレクション側に以下のような機能が用意されていたら、どうでしょうか?
すべてのデータに対して、引数で受け取った関数を適用して、その結果がTRUEだったデータのみを集めて、返却するよ!
このとき、関数は、例えば、入力としてデータを一つ受け取り、出力としてデータのフィールドの一つと特定の値が一致しているか判断した結果をTRUE/FALSEで返す、というものだったとします。
この関数を、上記コレクションの機能に引数で渡すだけで、なんと、データの絞り込みが実現できてしまうのです。
私たち開発者が行ったことは、条件を判断する関数を用意しただけです。(SQLでWHERE句に条件文を記述するのと、違いはないですね)
伝統的な命令型のプログラミングにおいては、すべてのデータを精査することも、条件を判断することも、コレクションからデータを詰め替えるのも、全てアルゴリズムを構築して、プログラムを実現していました。
もちろん、命令型でもデータの絞り込みは実現できますが、何度も言うように、アルゴリズムには人間のミスが混入しやすいのです。
コンピュータの強みである手続き的処理を、副作用が無い参照透過な関数をインターフェースとして呼び出すことで、人間のミスが混入する余地が圧倒的に少なくなるのです。
この関数型のアプローチは、コレクションに対して条件を(関数で)宣言していることと同じであるため、宣言型プログラミングの一種として扱われています。
ちなみに、上記のコレクションからデータを絞り込む関数のインターフェースは、JavaのStream API
をモデルにしました。
Stream API
がよく理解できていない初心者エンジニアがいましたら、この考え方を参考に、勉強してみてください。そんなに難しい言語仕様ではないです。
すべての資産は宣言してバージョン管理
ここまで、宣言型プログラミングと、その適用例をいくつか見てきました。
宣言型というのは、人間のミスを混入する余地を減らす、画期的なアプローチだという話はしてきましたが、良くも悪くも、ここまでの話はプログラムを書いて、ソフトウェアを構築するシーンにおける、宣言的なアプローチの話に閉じていました。
宣言型の真価は、実は、その先のあると言えます。
宣言型の大きなメリットを、ソフトウェアを構築するためのプログラミングにしか適用しないというのは、もったいないと思いませんか?
例えば、上の節でもお話しましたが、手順書を基本とする稼働環境へのリリースも、宣言的に実施できたら嬉しいと思いませんか?
- サーバのスペックは○○で…
- ミドルウェアは××をインストールして…
- アプリケーションは△△でビルドしてデプロイして…
さらに、
- 負荷が□□%上昇したら◇◇台のサーバで負荷分散して…
- 常時☆☆台のサーバを稼働状態にして、一つでも死んだら、自動で復旧して…
といった、稼働環境に期待する状態も宣言できたら…
そんな夢物語があるもんか、と思うかもしれないですが、これが、現在のテクノロジーの姿であると言えます。
つまり、あるのです。
Infrastructure as Codeと宣言型
私がこの1〜2年で、一番衝撃的だったテクノロジーに、Kubernetes
というものがあります。
プロダクショングレードのコンテナ管理基盤 - Kubernetes
Kubernetes
は、一言で説明するのが難しいのですが、コンテナ仮想化をベースにした、アプリケーションの実行環境と運用管理の機能を提供する、プラットフォームといったところでしょうか?
公式サイトには、以下のような説明があります。
Kubernetes (K8s)は、デプロイやスケーリングを自動化したり、コンテナ化されたアプリケーションを管理したりするための、オープンソースのシステムです。
Dockerなどのコンテナ仮想化は、一つのアプリケーションを一つのコンテナで実行させるような構成であれば、それほど複雑なものではありません。
難しくなってくるのは、複数のソフトウェアから構成されるアプリケーションを、それぞれ一つのコンテナで実行させ、それらをお互いに協調させて構成させる場合です。
Docker Composeなどの、複数のコンテナを協調させるプラットフォームは存在しますが、それでも、コンテナの数が増えて、構成の複雑さが増してくると、扱いきれなくなるようです。
そこで、コンテナ・オーケストレーションシステムを利用して、コンテナをもっと扱いやすくして、アプリケーションを柔軟に構築できるようにしたのが、Kubernetes
というプラットフォームになります。
オーケストレーション (コンピュータ) - Wikipedia
扱いやすくというのが、具体的にどういうことか、見ていきましょう。
Kubernetesというのは、基本的にAWSやGCPのクラウドサービス上でサービス提供されています。つまり、従量課金で利用できます。
クラウド上のKubernetesプラットフォームでは、その上にコンテナベースのアプリケーションを構築できるのですが、その方法は、マニフェストファイル(yaml等)を作成して、Kubernetesに食わせるだけです。
マニフェストファイルのサンプルを、以下の公式サイトの記事から転載します。
Kubernetesオブジェクトを理解する - Kubernetes
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # tells deployment to run 2 pods matching the template
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
下から3行目に、image:
でDockerイメージを指定しています。
Dockerイメージは、インターネット上のイメージリポジトリから取得できるので、ここではどれを使うかだけ宣言しています。
なかほどのreplicas:
に2が宣言されています。
レプリカは、このコンテナをいくつ稼動状態にするかを、宣言できる機能です。
このマニフェストファイルによって、nginx
というWebサーバのコンテナを、常時2つ稼動状態にする、という宣言になります。
お気づきの通り、マニフェストファイルは、宣言的プログラミングの考え方で記述されており、手続きは一切登場していません。
さらに、Kubernetesに対するソースコードの位置づけにもなるので、このマニフェストファイルをバージョン管理システムで管理できるのです。
インフラをソースコードとして扱うという考え方は、Infrastructure as Code(コードとしてのインフラ)と呼ばれています。
Infrastructure as Code - Wikipedia
IaC自体は、様々なツールや手法があり、長年色々検証されてきました。
当初は、おそらくリリースシェルなどの命令型で、デプロイや設定変更を自動化していたのではと、いろいろ調べてて想像していました。
そういうこともあって、長らく、荒唐無稽なただの理想論だと言われていたようですが、このKubernetesは見事にIaCを実現しています。
これにて、伝統的に手順書ベースで行ってきた、稼働環境へのリリースが、完全に自動化できるようになった、ということですね。
おわりに
今回は、プログラミングの枠を超えて、宣言型のアプローチについて、既存のテクノロジーを色々見てきました。
冒頭でお話した、プログラムのもう一つの側面とは、いかに命令せずにコンピュータに処理をさせるかという、視点を変えることと、アプリケーションだけでなくインフラもコーディングするという、開発者のテリトリーの拡大、という2点でした。
命令せずに宣言してプログラムするというのは、非常に便利なアプローチではありますが、宣言できることが具体的に何なのかという知識が求められる、というお話をしました。
ある意味、フレームワークやツール、プラットフォームごとに、この宣言可能な機能が全く違うので、新たな宣言型テクノロジーが現れるたびに、学習が必要になってしまいます。
しかしながら、この手のテクノロジーは、どんどん新しいものが登場しては、衰退していくもの、と割り切って、対応していくのが正しいのかもしれません。
また、終盤でちょっとだけInfrastructure as Codeの話をしましたが、これは、バージョン管理システムにインフラの構築・管理も含めてしまう、というアプローチでした。
バージョン管理システムは、開発者の精神的支柱であるため、ここにインフラの構築や運用管理も含められたら、どれだけ安心できるでしょうか?
今回紹介しなかったですが、CI/CDというビルド・リリースの自動化と継続的運用も、バージョン管理システムが中心に据えられています。
ソフトウェア・アプリケーションの構築が、バージョン管理システムがすべての起点となることで、様々な自動化や変更追跡が実現できるだけでなく、運用も含めて開発者が中心となってすべてを回す、という体制が敷かれるようになります。
最近、GitOps
という名称で、この考え方を耳にするようになりました。
GitOps を使用したサーバーレス時代における最新の CI/CD パイプライン構築 - AWS
近年、様々なところで目にする宣言型テクノロジーも、こういう根本的な開発・運用の思想や考え方を汲んで、進化してきたと考えると、ただそのツールの使い方だけ知っても意味ないですね。
以前の記事でお話した、仕様と実装における、仕様面のアプローチが重要になります。
今回の記事は、そんな世間で浸透しつつある宣言型のアプローチのお話でした。
それでは、今回はこの辺で。