#はじめに
関数型プログラミングを調べたり勉強していると、しばし「宣言型プログラミング」(declarative programming)という言葉を目にします。しかし、この宣言型プログラミング、はたまた、「宣言型」(declerative)という言葉は一体どのような意味を持っているのでしょうか。結論を言ってしまえば統一的な定義はないようです。しかし、「宣言型」のもともとの意味を遡ることでこれが意味することが分かってきました。また、個人的な解釈も含みますが、「関数型プログラミング」との違いも見えました。紐解いてみましょう。
#平叙文と命令文
宣言型プログラミングと対比的な立場にあるのが命令型プログラミング(=手続き型プログラミング)ですが、これらの用語はそもそもどこから来たのでしょうか。出典を探しましたが見つからなかったため、私の推測になりますが、この2つは自然言語における平叙文(declarative sentence)と命令文(imperative sentence)の区別に由来するものと思われます。平叙文は、「AはBである。」などの客観的な事実に関する記述です。これは辞書的な記述と言えるでしょう。また、平叙文は否定「でない」を否定文も含まれ、また、接続詞「そして」などで文を連結(重文)することもできます。命令文は、「Aせよ。」のような文です。
#宣言的知識と手続き的知識
また、この2つの文章の区別に対応する知識は、それぞれ宣言的知識(declarative knowledge, propositional knowledge, know-that)と手続き的知識(命令的知識、procedural knowledge, know-how)と呼ばれて区別されています。
宣言的知識はそのまま辞書的な文の知識ですが、手続き的知識はある種、明文化し難いような経験的な知識や暗黙知になります。これを明文化すると料理のレシピなどのハウツー本や、googlemapの目的地までの指示などステップごとに命令文が列挙された表現になります。これは通常、「アルゴリズム」という言葉で想起するものと考えて良いでしょう。
#手続き型プログラミング
さて、ここまで段階で手続き的プログラミングが手続き的知識に由来し、また、対応しているというのは腑に落ちるかと思います。つまり、料理のレシピを読者に対して書くように、各ステップを順々にコンピュータに対して書くのが手続き型プログラミングというわけです。例えば、次の関数を見てみましょう。
// foo :: number -> number
function foo(x) {
x = x + 1;
x = x * 2;
return x
}
ここでは、引数のxにあれせよ(+1)、これせよ(*2)、という命令が順に書かれているため、手続き型(命令型)的な書き方と言えるでしょう。
#宣言型プログラミング
問題は、平叙文(もしくは、宣言的知識)と宣言型プログラミングがどのように対応しているか、です。残念ながら、宣言型プログラミングに統一的な定義はないようで、ネット上でも異なった多くの定義にあふれています。
例えば、stackoverflowのこの記事で議論されていました。宣言型の意味は抽象的である、宣言型の意味は関数型である、宣言型の意味は手続き型ではないことである、宣言型の意味は参照透過性である、などなどです。また、宣言型という術語の使用に否定的な記事もありました。異なる定義を追っても混乱するので、手元にあった本で比較的わかりやすかった説明を引用します。
宣言的であるということは、出力の性質がどういうものなのかに着目し、それだけを記述させるということです。たとえば、ソートを達成するにあたり、「配列の先頭要素と次の要素を比較して、次の要素が小さければ入れ替え、さらに次の(以下略)」などと書かされるなら宣言的ではありません。「配列のある位置の要素は、それ以上の要素よりも小さくあるべし」と書くだけですむなら宣言的です。(関数型プログラミング実践入門, p12)
##述語と性質
ここで重要になってくるのが「性質」という概念です。宣言型の元ネタと考えられる平叙文に立ち戻って考えましょう。平叙文とは基本的に「AはBである。」という形の文でした。平叙文を形式言語、つまり、論理式と考えるならば、「AはBである。」などの表現における、Aは対象で「はBである。」は述語ということになります。述語の種類も幾つかに区別されますが1、その中の一つに性質があります。
性質は例えば、「ソクラテスは人間である」における「は人間である」などの対象の普遍的な特徴の表現ですが、述語は形式言語(述語論理)では「$x$は人間である」と表現されます($x$は自由変数とします)。詳しくは定義しませんが、$x$は自由変数なので「$x$は人間である」は開論理式と呼ばれます。述語論理の意味論では、自由変数$x$を含む開論理式は$x$に何らかの定数$a$を入力したら真偽を出力する関数と考えられます。例えば、「$x$は人間である」の$x$に「ソクラテス」を代入したら真を出力します。
この開論理式と否定や連言などの基本的な論理結合子(真理関数)のみでだけで行うプログラミングを宣言型プログラミングと呼びます。「Aを○○せよ」という命令文の集まりが手続き型プログラミングだとしたら、「$x$はBである。」という性質、ないしは、述語、ないしは、開論理式の集まりで行うプログラミングが純粋な意味で宣言型プログラミングであると言えるでしょう。
##述語から関数一般へ
上記の考察に従って、「宣言型プログラミングは、コードを述語(開論理式)の集まりで記述することである」、としましょう。この定式化に沿うのがPrologなどの論理型言語です。実際、Prologは真偽しか出力を持たない開論理式(精確には開論理式を少し変形させたホーン節ですが)のみでコーディングするため純粋な宣言型プログラミングを可能にします。ただ、この定式化では適用可能な言語の範囲が狭すぎますので、真偽しか出力を持たない開論理式だけでなく関数一般と考えます。つまり、「宣言型プログラミングは、コードを関数の集まりで記述することである」とします。このように考えれば、haskellなどの関数型プログラミング言語も宣言型プログラミング言語とみなすことができます。
#関数型と宣言型の違い
上記の考察より、宣言型プログラミングとは関数を使ってプログラミングすることとします。では結局、宣言型プログラミングと関数型プログラミングは何が違うのでしょうか。私の解釈では、次のように、宣言型プログラミングとは純粋関数を構成する記述に対しても制約を与えるものです。
- 宣言型プログラミングでは、(純粋)関数は数学的な記述のみで構成される。
- 関数型プログラミングでは、(純粋)関数は関数の条件を満たしていれば良く、手続き的な書き方が混ざっても構わない。
数学的な記述とは何か、と聞かれたら答えるのは容易ではありませんが、とりあえず数理論理学、集合論、圏論といった数学の基礎的な言語による表現であるとします。
例えば、上述した関数fooは明らかに純粋関数(純粋関数に関してはこちらにまとめました)ですが、関数の中で変数に再代入しています。しかし、数学では変数への再代入は基本的にありえず数学的な記述に反します。そのため、fooのような関数は、関数型プログラミングでは問題ないが、宣言型プログラミングでは許容されないと考えられます。宣言型プログラミングでは、例えその関数の純粋性は当然として書き方も数学的な書き方が要求されます。そのため、上を数学的な記法で書き直すとしたら、
// foo2 :: number -> number
function foo2(x) {
const _x2 = x + 1;
const _x3 = _x2 * 2;
return _x3
}
となるでしょう。つまり、関数fooにおける命令的な表現(変数への再代入)は新しい定数の宣言という形に書き換えられました。これは、(関数内での定数宣言はconst/let文よりもwhere文で後述するのが一般的かと思われますが)数学的に特に問題になるものでありません。
###純粋関数だが宣言型プログラミングでない例
別の例を見てみましょう。
// piyo :: number[] -> string[]
function piyo(xs) {
let ys = [];
for (let i = 0; i < xs.length; i++) {
ys[i] = xs[i].toString();
};
return ys
}
piyoという関数は数字の配列を単にstringに変換する単純なものです。ここで用いられるfor文は配列の各要素を一つ一つ取ってきてそれぞれに命令を行うというものであり、私が考えるに典型的な手続き型プログラミングの表現方法です。なぜなら、これは次のような命令の列を一般化したものだからです。
function piyo(xs) {
let ys = [];
ys[0] = xs[0].toString();
ys[1] = xs[1].toString();
ys[2] = xs[2].toString();
...
return ys
}
このような書き方だとpiyo(xs)とはなんであるか、に対して「piyo(xs)とは、xsの一つ目をこうして、2つ目をこうして、...、n個目をこうしたものである」といった答えであり、これは手続き的記述、レシピ的な記述に対応していると言えます。そこで、これを宣言的に書くと例えば次のようになります。
// piyo2 :: number[] -> string[]
function piyo2(xs) {
return xs.map(x => x.toString())
}
これはmapという高階関数でラムダ式(x => x.toString())を関数の入力である配列に割り当てているということです。このように書けば、「piyo2(xs)とは、xs.map(x => x.toString())である」と答えられます。これはxsという変数をもつ関数であり、そのため、これは宣言型プログラミングと言えるでしょう。そして、上のpiyoの場合はこのような表現になっておらず宣言型とは言えないでしょう。
#まとめ
宣言型プログラミングの「宣言型」の大もとと思われる平叙文から宣言型プログラミングの意味を考察してみました。まとめると、
- 関数型プログラミング:そこで使用される純粋関数は数学の関数の条件を満たす。
- 宣言型プログラミング:関数型プログラミング + 関数は数学的な記法で書かれる。
これらの意味は外延的にほとんど同じですが、これらの言葉の成り立ちから考察すると、これらの意味は内包的には大きく異なっていると言えます。例えば、「xは太陽から三番目の惑星である」と「xはわれわれが住む星である」という関数は両方共、地球のみ要素とするシングルトン集合を出力しますが、2つの表現の意味は異なっています。これと同じようなことが関数型と宣言型にも言えるのではないでしょうか。
-
アリストテレスのカテゴリー論によれば述語の種類は次の10種類に分けられます、(1)実体、(2)量、(3)性質、(4)関係、(5)場所、(6)時、(7)姿勢、(8)所持、(9)能動、(10)受動。 ↩