【プログラミング初心者脱却】手続き型プログラミングと宣言型プログラミング

この記事では、まだプログラミング経験が浅い方向けに、プログラミングにおける手続き型のアプローチと宣言型のアプローチについて解説していきます。
一般的に、プログラミング学習したばかりの頃は手続き型のアプローチでコードを書くことが多く、徐々に宣言的なアプローチに慣れていくケースが多いかと思います(私自身がそうでした)。

最近の動向としては宣言的なアプローチでプログラミングすることが推奨されるケースも多くあり、手続き的なコードの書き方ばかりをしているとどうしても初心者感が出てしまいます。
プログラミング初心者を脱却して、中級者を目指していく上で、宣言的なプログラミングのアプローチは避けて通れません。

この記事では、宣言的なプログラミングに慣れる手助けができればと思っています。

配列に対する処理

まずは具体的なサンプルコードを見ながら、手続き型と宣言型でどのような違いがあるかを見ていきます。

与えられた数値の配列に対して、偶数だけの要素の合計値を取得するという要件を満たすコードを書きたいとします。

以下はJavaScriptを使ったサンプル。

const numbers = [1, 2, 3, 4, 5, 6, 7];

// 手続き的なアプローチ
let sum = 0;
for (const num of numbers) {
	if(num % 2 === 0) {
		sum += num
	}
}
console.log(sum) // 12

// 宣言的なアプローチ
const result = numbers
				.filter(num => num % 2 === 0)
				.reduce((acc, cur) => acc + cur, 0)
console.log(result) // 12

以下はJavaを使ったサンプル。言語が異なるだけで処理の内容は同じです。

var numbers = List.of(1, 2, 3, 4, 5, 6, 7);  
  
// 手続き的なアプローチ
var sum = 0;  
for (var n : numbers) {  
    if (n % 2 == 0) {  
        sum += n;  
    }  
}  
System.out.println(sum);  
  
// 宣言的なアプローチ
sum = numbers.stream()  
        .filter(n -> n % 2 == 0)  
        .mapToInt(Integer::intValue)  
        .sum();  
System.out.println(sum);

文法の違いや標準で用意されている関数(メソッド)の違いはあるものの、実装の考え方はどちらも同じです。
手続き的なアプローチでは、for文を使って処理の流れを記述し、if文で条件に合致する場合に変数に値を加算し、最終的な合計値を算出します。
宣言的なアプローチでは、配列(リスト)が持つメソッドを利用して、1つの式で最終的な結果まで取得します。

どちらの方が分かりやすいと感じましたか?

手続き型と宣言型とは

そもそも手続き型のアプローチと宣言型のアプローチはどのようなプログラミングスタイルを指すのでしょうか。

手続き型プログラミングとは、どうやって処理を進めるかの手順を記述していくプログラミングスタイルです。
一方で宣言型プログラミングとは、達成したいことを先に宣言する、何をしたいのかを記述していくプログラミングスタイルです。

英語的に考えると、HowとWhatの違になります。
手続き型プログラミングはHow、つまり、実現したいことをどうやって実現するのか、その具体的手順をプログラミングしていきます。
宣言型プログラミングはWhat、つまり、何をしたいのかを記述するプログラミングです。

先の例では、for文とif文を使ったコードは、実現したいことのHowを書いています。
一方の配列の関数を使った処理は、欲しいものをそのままコードに落とし込んでいます。

// 与えられた配列の
numbers
	.filter(num => num % 2 === 0) // 偶数だけを抽出して
	.reduce((acc, cur) => acc + cur, 0) // 合計値を計算

結論が先か結論が後か

社会人としての業務に関わるコミュニケーション(例えば報連相や、確認、質問、プレゼンなど)では、結論から先に伝えることが良いとされています。
なぜ結論が先の方が良いかというと、その方が分かりやすくて伝わりやすいからです。
先に結論を伝え、その後に結論に対する理由や根拠を示すのが、わかりやすい伝え方の鉄則です。

一方で、最初に具体的な説明がだらだらと続き、最後に結論が来るような説明は、聞き手側にとってはいつ結論を知れるのかわからず、ストレスを感じてしまいます。

プログラミングの世界でも概ね同じようなことが言えます。

手続き型のアプローチは、要件を実現するための処理の流れを書くため、処理の流れを最後まで追わなければやりたいことが分かりません。(もちろん関数名や変数名、コメントなどである程度分かりやすくすることは可能です)
一方、宣言的なアプローチでは何をしたいか、という結論を先に書くため、手続き型のアプローチに比べて分かりやすいと感じやすくなります。

ただし、先の例の場合、JavaScriptの宣言的なアプローチのサンプルでは、filter関数やreduce関数の知識、アロー関数の知識などがなければ読むのは難しいでしょう。
Javaのサンプルでも、ストリームやラムダ式などの概念を知っていないと読むのは難しいかもしれません。
しかし、ある程度プログラミングに慣れて知識も身についてくると、宣言的に書かれているコードの方が分かりやすいと感じるようになります。(少なくとも私も徐々にそう感じるようになりました。)

プログラミングの経験が浅い段階ではifやforなどの制御構文を駆使してやりたいことを実現することが多いかと思いますが、アルゴリズムを考えることにある程度慣れてきた後は、宣言的な書き方で実現できないか考える習慣を身に着けると良いと思います。

SQLは宣言的

現場でも扱うことの多いプログラミング言語の中で、宣言的なアプローチをとるプログラミング言語の代表例はSQLです。
SQLは、どのデータが欲しいのか(何がほしいのか)を英語に近い文法でそのまま宣言的に記述します。
そのため、基本的にSQLを書くときは処理の手順(データの検索のアルゴリズムや、結合や集約に関するアルゴリズム)を意識する必要はありません。

-- SQLのサンプル
-- どのデータが欲しいか、を直接記述する
SELECT user_id, user_name 
FROM users
WHERE user_name = 'Alice';

※パフォーマンスのチューニングを行う場合は実行計画などを見ながら中のアルゴリズムを意識しなければいけないケースもありますが。。

複雑なデータの取得を1つのクエリで実現しようと思うと、結合や集約、サブクエリなどを駆使して複雑なSQLを記述しなければいけないケースもあります。
プログラマーの中にはそのような複雑なSQLを書くのが苦手な方もいます。
その要因の一つとして、普段プログラミングをするときに手続き的なアプローチで処理を考えているため、SQLを書く際にも処理の流れで考えてしまい、SQLがうまく組み立てられないケースもあるようです。
あるSQLの本に書かれていましたが、普段プログラムを書いているプログラマーの人よりも、プログラミングの経験はないが普段Excelでデータ入力や分析をしている人たちの方がSQLの習得が速かった例もあるようです。

普段プログラミングをしているけどSQLを書くのが苦手という方は、SQLは宣言的であることを意識できるようになると、苦手意識が薄れることもあるかもしれません。

フロントエンドの手続き型と宣言型

Webアプリケーションを開発する場合、JavaScriptのDOM操作によって画面遷移することなく動的に画面要素を変更することは多くあります。
昔はJavaScriptによるDOM操作も手続き型のアプローチが主流でしたが、最近ではVue.jsやReactなどのフレームワークの登場により、宣言的に記述する方が主流となってきました。

サンプルコードと共に違いを見ていきます。

例えば、以下のように、ボタンを押すとテーブル要素が表示されるようなWebページを考えます。

Webページサンプル

JavaScript標準の技術だけで実装すると、以下のようになります。
以下はHTMLファイルです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テーブル追加サンプル</title>
</head>
<body>
  <h1>サンプル-JavaScript</h1>
  <button id="create-table-button">テーブル作成</button>
  
  <div id="table-container"></div>
  
  <script src="main.js"></script>
</body>
</html>

以下はJavaScriptファイルです。

'use strict'

function createTable() {
  // <table>要素を作成
  const table = document.createElement("table");
  table.border = "1";
  table.style.borderCollapse = "collapse";
  table.style.width = "300px";

  // データ(ヘッダー + ボディ)
  const headers = ["名前", "年齢", "職業"];
  const rows = [
    ["田中 太郎", "30", "エンジニア"],
    ["佐藤 花子", "25", "デザイナー"],
    ["鈴木 次郎", "28", "マーケター"]
  ];

  // ヘッダー行の作成
  const thead = document.createElement("thead");
  const headerRow = document.createElement("tr");
  headers.forEach(text => {
    const th = document.createElement("th");
    th.textContent = text;
    th.style.backgroundColor = "#f2f2f2";
    headerRow.appendChild(th);
  });
  thead.appendChild(headerRow);
  table.appendChild(thead);

  // 本文行の作成
  const tbody = document.createElement("tbody");
  rows.forEach(rowData => {
    const row = document.createElement("tr");
    rowData.forEach(cellData => {
      const td = document.createElement("td");
      td.textContent = cellData;
      td.style.textAlign = "center";
      row.appendChild(td);
    });
    tbody.appendChild(row);
  });
  table.appendChild(tbody);
  
  // DOMに追加
  document.getElementById("table-container").appendChild(table);
} 

// ボタンにクリックイベントを追加
document.getElementById("create-table-button").addEventListener("click", createTable);

まず、HTMLを見ると、テーブルが作られるであるdiv要素はありますが、これだけだとどのようなテーブルが作成されるのかが想像できません。

<!-- テーブル要素の構造は不明 -->
<div id="table-container"></div>

JavaScriptのコードを見ると、tableの要素を作成して追加する処理がありますが、処理の流れ追わなければどのような構造になるかは分かりません。
コメントのおかげでかろうじてコードを読まずとも分かる箇所もありますが、このコードの保守性が高いとは言いずらいです。

jQueryによる簡略化

一昔前、jQueryというJavaScriptのライブラリが流行りました。
jQueryを使うと、DOM操作を標準のJavaScriptよりも簡潔な記述で実現できるようになります。
先と同じ実装をjQueryで書き換えたのが以下のコードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テーブル追加サンプル</title>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
  <h1>サンプル-jQuery</h1>
  <button id="create-table-button">テーブル作成</button>
  <div id="table-container"></div>
  
  <script src="main.js"></script>
</body>
</html>
'use strict'

function createTable() {
const headers = ["名前", "年齢", "職業"];
  const rows = [
    ["田中 太郎", "30", "エンジニア"],
    ["佐藤 花子", "25", "デザイナー"],
    ["鈴木 次郎", "28", "マーケター"]
  ];
  
  const $table = $("<table>").css({
    borderCollapse: "collapse",
    width: "300px"
  }).attr("border", 1);
  
  // ヘッダー
  const $thead = $("<thead>");
  const $headerRow = $("<tr>");
  $.each(headers, function (_, text) {
    $("<th>").text(text).css("background-color", "#f2f2f2").appendTo($headerRow);
  });
  $thead.append($headerRow).appendTo($table);
  
  // 本文
  const $tbody = $("<tbody>");
  $.each(rows, function (_, rowData) {
    const $row = $("<tr>");
    $.each(rowData, function (_, cell) {
      $("<td>").text(cell).css("text-align", "center").appendTo($row);
    });
    $tbody.append($row);
  });
  $table.append($tbody);
  
  // DOMに追加
  $("#table-container").append($table);
}

// ボタンにクリックイベントを追加
document.getElementById("create-table-button").addEventListener("click", createTable);

JavaScript標準技術で実装したコードに比べると、いくらかコードは短くなり、全体的に読みやすくもなっています。
しかし、HTMLファイルを見ただけでHTML要素の構造が分からないという根本的な課題は解決されておらず、処理の流れを読む必要があることも先ほどと変わりません。

Vue.jsによる実装

続いては、近年SPAのWebアプリケーションで使用されることの多いVue.jsで同じことを実現した場合です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テーブル追加サンプル</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
  <style>
    table {
      border-collapse: collapse;
      width: 300px;
    }
    
    th {
      background-color: #f2f2f2;
    }

    th, td {
      border: 1px solid black;
      text-align: center;
      padding: 4px;
    }
  </style>
</head>

<body>
  <div id="app">
    <h1>サンプル-Vuejs</h1>
    <button @click="createTable">テーブル作成</button>
    <table v-if="rows.length > 0">
      <thead>
        <tr>
          <th v-for="header in headers" :key="header">{{ header }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, rowIndex) in rows" :key="rowIndex">
          <td v-for="(cell, colIndex) in row" :key="colIndex">{{ cell }}</td>
        </tr>
      </tbody>
    </table>
  </div>
  <script>
	'use strict'
	
	const { createApp, ref } = Vue;
	
	createApp({
	  setup() {
	    const headers = ref(["名前", "年齢", "職業"]);
	    const rows = ref([]);
	    
	    const createTable = () => {
	      if (rows.value.length > 0) {
	        return;
	      }
	      rows.value.push(["田中 太郎", "30", "エンジニア"]);
	      rows.value.push(["佐藤 花子", "25", "デザイナー"]);
	      rows.value.push(["鈴木 次郎", "28", "マーケター"]);
	    }
	    return { headers, rows, createTable };
	  }
	}).mount('#app');
  </script>
</body>
</html>

CSSスタイルをHTMLファイルに移したので、若干HTMLファイルの行が長くなりましたが、注目すべきはtable要素の部分。
Webページの動作としてはJavaScript標準やjQueryで実装した時と同じですが、今回Vue.jsによる実装では、HTMLの中に最初からtable要素が定義されています。
つまり、JavaScriptの処理を読まなくても、画面に表示される要素の構造が把握できるようになります。

<table v-if="rows.length > 0">
  <thead>
	<tr>
	  <th v-for="header in headers" :key="header">{{ header }}</th>
	</tr>
  </thead>
  <tbody>
	<tr v-for="(row, rowIndex) in rows" :key="rowIndex">
	  <td v-for="(cell, colIndex) in row" :key="colIndex">{{ cell }}</td>
	</tr>
  </tbody>
</table>

Reactによる実装

続いては、Vue.jsと並んで近年使用されることの多いReactを用いたサンプルです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テーブル追加サンプル</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <style>
    table {
      border-collapse: collapse;
      width: 300px;
    }
    th {
      background-color: #f2f2f2;
    }
    th,td {
      border: 1px solid black;
      text-align: center;
      padding: 4px;
    }
  </style>
</head>
<body>
  <div id="root"></div>
  
  <!-- React App -->
  <script type="text/babel">
    const { useState } = React;
    function App() {
      const [tableData, setTableData] = useState([]);
      const headers = ["名前", "年齢", "職業"];
      const rows = [
        ["田中 太郎", "30", "エンジニア"],
        ["佐藤 花子", "25", "デザイナー"],
        ["鈴木 次郎", "28", "マーケター"]
      ];
  
      const handleClick = () => {
        setTableData(rows);
      };
      return (
        <div>
          <h1>サンプル-React</h1>
          <button onClick={handleClick}>テーブル作成</button>
          {tableData.length > 0 && (
            <table>
              <thead>
                <tr>
                  {headers.map((header, index) => (
                    <th key={index}>{header}</th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {tableData.map((row, rowIndex) => (
                  <tr key={rowIndex}>
                    {row.map((cell, cellIndex) => (
                      <td key={cellIndex}>{cell}</td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
  </script>
</body>
</html>

Reactを触ったことがない人にとっては独特な書き方に感じる人もいるかもしれません。
Vue.jsとはまた違った書き方ですが、考え方としては似ています。
DOMを構築する部分は以下のようになっています。

<div>
  <h1>サンプル-React</h1>
  <button onClick={handleClick}>テーブル作成</button>
  {tableData.length > 0 && (
	<table>
	  <thead>
		<tr>
		  {headers.map((header, index) => (
			<th key={index}>{header}</th>
		  ))}
		</tr>
	  </thead>
	  <tbody>
		{tableData.map((row, rowIndex) => (
		  <tr key={rowIndex}>
			{row.map((cell, cellIndex) => (
			  <td key={cellIndex}>{cell}</td>
			))}
		  </tr>
		))}
	  </tbody>
	</table>
  )}
</div>

これも、table要素が構築されることがJSX(Reactで用いられる構文)を見るだけで判断することができ、構造を理解するのに処理の流れを細かく追う必要がありません。

Vue.js、Reactいずれにしても、手続き的なアプローチで実装を考えているとフレームワークの良さを最大限に引き出すことは難しいです。宣言的なアプローチで実装を考えることが大切です。

インフラ構築の手続き型と宣言型

インフラ環境を構築する場合も、手続き的なアプローチと、宣言的なアプローチがあります。
サーバーに対して環境構築を行う場合、コマンドを一つずつ入力して環境構築していくことも可能ですが、同じ構成のサーバーを何度も構築する場合、何度も同じコマンドを入力するのは手間がかかりますし、ミスも発生しやすくなります。

同じコマンドを何度も入力する手間を省略し、入力ミスや漏れを防ぐ方法として、コマンドをまとめたシェルスクリプトを作成する方法があります。

例えば、Linuxサーバー(RetHat系)にWebサーバー(Apache)を構築したい場合、以下のようなシェルスクリプトを作成すれば、スクリプトを実行するだけで構築できます。

#!/bin/bash

# Apache をインストール
sudo apt update
sudo apt install -y apache2

# Apache を有効化・起動
sudo systemctl enable apache2
sudo systemctl start apache2

# ファイアウォールを設定(必要に応じて)
sudo ufw allow 'Apache'

シェルスクリプトでは、上から順番にコマンドが実行されていくため、処理の順番を意識して書く必要があります。つまり、手続き型のアプローチになります。
このようにスクリプトを作っておくと、毎回同じコマンドを入力する手間は省けますが、実行時にエラーが起き場合に、途中から再開することが難しいといった課題もあります。
同じシェルスクリプトを複数回実行した場合、上から順序よく全ての処理が実行されていきます。内容によってはエラーになったり、複数回同じコマンドが実行されることで思わぬ不具合につながる可能性もあります。

そのようなシェルスクリプトの課題を解決するためのツールとして、構成管理ツールと呼ばれるツールがあります。これらのツールは、サーバーがどのような状態であるべきかを定義します。つまり、宣言的なアプローチをとります。
以下は、Ansibleという構成管理ツールで、Apacheの導入を定義したファイルのサンプルです。

# apache-setup.yml
- name: Apache Web サーバーをインストール&起動
  hosts: webservers
  become: yes

  tasks:
    - name: Apache をインストール
      apt:
        name: apache2
        state: present
        update_cache: yes

    - name: Apache を起動&有効化
      service:
        name: apache2
        state: started
        enabled: yes

    - name: UFW で Apache を許可
      ufw:
        rule: allow
        name: 'Apache'

単純な記述量はシェルスクリプトよりも増えていますが、こちらはサーバーのあるべき状態を宣言的に記述されているため、処理の流れを追うことなく、サーバーの状態を把握することができます。
また、何度実行しても、同じ状態になることが保証されています(冪等性)。

関数型言語への準備

現代のプログラミング言語は大きく2つのパラダイムがあります。1つがオブジェクト指向言語で、もう1つが関数型言語です。関数型言語は私自身経験が浅く、まだ学習中の身ではありますが、宣言型プログラミングの発想がないと理解するのも使いこなすのも難しいです。

これから関数型言語を学習してみたい方は、いろんな場面で宣言的なアプローチで実装するように心がけてみると、学習がスムーズになるかもしれません。

まとめ

色んな分野での手続き型アプローチと宣言型アプローチの例を見てきました。
この記事では基本的に宣言的である方が分かりやすくて良いという趣旨で書いてきましたが、実現したい内容によっては宣言的に書く方が逆に分かりにくく感じる場面もあることでしょう。
また、使用しているプログラミング言語やフレームワークによって思想や書き方の向き不向きもあります。使用している技術の思想や得意分野に合わせて、柔軟に発想を切り替えてコードの書き方ができると、保守性の高いプログラムを作りやすくなります。


システム開発に関するご依頼・お問い合わせはこちらから。
https://www.techno-core.jp/system-contact

===========

新人研修に関するご依頼・お問い合わせはこちらから。
https://www.techno-core.jp/contact

============

採用に関するご応募はこちらから。
https://www.techno-core.jp/recruit