コンテンツにスキップ

プログラミング言語:高レベル概要

このセクションでは、プログラミング言語の目的と一般的な分類について説明します。

プログラミング言語とは何か?なぜ必要なのか?

一般的には、プログラミング言語は、人間のような考え方を持つ存在にとって、コンピュータプログラムの設計、表現、構築、理解を容易にするためにデザインされた、コンピュータの抽象化です。

なお、プログラミング言語は、アセンブリ言語を含むように、より自由な定義の場合もありますが、 このモジュールでは、「プログラミング言語」とは、C/C++、Python、Rust、Java、JavaScriptなどのような、現代の「人間向け」プログラミング言語のみを含むものと定義します。 人間が使用しないような言語、つまり、アセンブリ言語やバイナリマシンコードは含まれません。

前のモジュールで説明したように、コンピュータの最も低いレベル(ハードウェアに最も近い)の抽象化は、命令セットアーキテクチャ(ISA)ですが、ISAでのプログラミングは人間にとって非常に直感的ではありません。 そのため、人々は、理解しやすく使用しやすいより高レベルな抽象化を構築することを決めました。 それがプログラミング言語の存在理由です。

プログラミング言語は人間にとって使いやすいように設計された高レベルの抽象化であるため、そのプログラミングモデルは通常、実際のコンピュータハードウェアからは遠い位置にあります(ISAと比較して)。 たとえば、レジスタは、ほとんどのISAで見られるハードウェアの詳細が、ほとんどのプログラミング言語では抽象化されています。

register image description

以下は、プログラミング言語で一般的に見られるいくつかの概念と、それらの「低レベルの実装」(ハードウェア上での抽象化)の比較です。

プログラミング言語の概念 ISAでの実装 説明
変数/定数 データを保持するメモリの領域 "Hello, World!"のような文字列、1.22のような数値、またはデータツリーのような「もの」
関数 (コンパイルされた言語では通常)ISAの命令を保持するメモリの領域 特定のことができるツール
構造体 (コンパイルされた言語では実際には)実行時に存在しない 「学生の構造体は名前、年齢、点数を持つ」という構造体の定義

プログラミング言語の一般的な分類

現在、たくさんのプログラミング言語が存在します。 それぞれに固有の特徴、抽象化、トレードオフがあり、特定の使用ケースに適しています。 しかし、それらの間にはいくつかの共通点があり、プログラミング言語は一般的に複数の視点から大きなグループに分けることができます。

このセクションでは、プログラミング言語を分類するための一般的な方法と、各カテゴリの言語の一般的な特徴について説明します。

実行モデル:コンパイル型対インタプリタ型

プログラミング言語を分類する一般的な方法の1つは、実行モデルによるもので、実行モデルは「コンパイル型」、「インタプリタ型」、またはその両方のハイブリッドがあります。

コンパイル型言語

前のセクションから思い出してくださいが、ほとんどすべてのコンピュータプログラムの実行時(プログラムが実行される時)は、ハードウェア上でのISAによって定義された命令の実行に帰結します。

ただし、プログラミング言語は人間のために設計されたものであり、コンピュータハードウェアではありません。 そのため、プログラムをプログラミング言語で書くためのソフトウェアを使用して、プログラムをコンピュータで実行できる形式に変換する方法が必要です。

コンパイル型言語では、コンパイルされた言語の開発者がプログラミング言語で書いたコード(完全なプログラムまたはプログラムの一部)を 「コンパイラ」と呼ばれるソフトウェアによって、プログラムが実行される前に機械実行可能コードに変換します。 そして、その機械コード形式のソフトウェアアプリケーションは、ハードウェア上で直接実行できます。

graph TD

    subgraph 開発者側
        ソースコード -->|コンパイル| 機械コード
    end

    subgraph ユーザー側
        機械コード -->|実行する| ハードウェア上
    end

コンパイル型言語に関連するいくつかの用語があります:

  • ソースコード:プログラミング言語で開発者が書いたコード。
  • 機械コード:直接ハードウェア上で実行できるコード。
  • コンパイル:ソースコードを機械コードに変換するプロセス。
  • コンパイルされたアプリケーション:コンパイルされた(ソースコードからの)ソフトウェアアプリケーションで、通常は機械コード形式で、ハードウェア上で直接実行できます。
  • バイナリ実行可能ファイル:機械コードが通常2進数形式であるため、ハードウェア上で直接実行できるコンパイルされたアプリケーションは通常、「バイナリ実行可能ファイル」と呼ばれます。
  • コンパイル:「コンパイル」の動詞形。
  • コンパイラ:コンパイルを行うソフトウェア。
  • コンパイル時:コンパイルが行われる時刻。
  • 実行時:プログラムが実行される時刻。

デコンパイル

機械コードをソースコードに戻すことができるかどうか疑問に思うかもしれません。 そう、実際に可能です。 そのようなプロセスはデコンパイルと呼ばれ、デコンパイルを行うデコンパイラは数多く存在します。

コンパイル型言語の利点は次のとおりです:

  • 高速でハードウェア効率が良い:コンパイル型言語の最大の利点は、高速でハードウェア効率が良いことです。 コンパイルのプロセスにより、コンパイルされたアプリケーションのすべての命令ができる限り効率的な方法で有用な仕事を行うようになります。 現代のコンパイラが行える最適化の幅広さについては多くの人が驚愕するでしょう。 ソースコード内の無効なコードを特定して除去することから、ハードウェアとの相性が良くなるようにソースコードを「書き換える」ことまで、多岐にわたる最適化が実現できます。 さらに、コンパイルされたアプリケーションは、インタプリタ型言語の場合とは異なり、中間レイヤーを導入せずにハードウェアと直接対話するため、より高速に実行され、より少ないメモリを使用します。
  • (時には)より良いバグ対策:多くの場合(変数の型のような場合など)、コンパイル型言語の静的性質は、ソースコード内の多くのエラーをコンパイル時に検出し、実行時にユーザーが経験するグリッチを引き起こさないようにします。

欠点:

  • 通常は学習と使用が難しい:コンパイル型言語は通常ハードウェアに近いですので、学習にはより多くの労力が必要です。 それらを使用することも通常はより難しいです。 ハードウェアに近いということは、メモリをはじめとする多くのことを(多くの場合、インタプリタ型言語の場合とは異なり)自分で管理する必要があることを意味します。 また、コンパイル型言語では通常、同じアプリケーションを書くためにより多くのコードが必要です。
  • 柔軟性が低い:コンパイル型言語の静的性質は、実行時に変数の型を変更するなどの特定の操作が難しくなります。
  • ハードウェア固有:マシンコードはハードウェアに最も近い抽象化ですので、異なるオペレーティングシステムに対応するためには通常、異なるバイナリ実行可能フォーマットを持つアプリケーションを開発する必要があります。 たとえば、Windows向けにコンパイルされたアプリケーションは、LinuxやmacOSでは実行できません。 また、x86 ISAを対象とするPCアプリケーションは、ARM ISAを実装しているスマートフォンでは通常実行できません。

一部の人気のあるコンパイル型言語は次のとおりです:

  • C/C++:多くの年月が経過してもなお、C/C++は世界で最も人気のあるコンパイル型言語の1つです。 正しい使い方で使用すると、ハードウェアに対する最高の制御と優れたパフォーマンスを提供しますが、学習のコストが高く、特にメモリ管理は困難です (ただし、スマートポインタやRAIIなどの機能を使用すると、これが緩和される場合もあります)。
  • Rust:比較的新しいモダンなプログラミング言語です。 最も学習が難しい言語の1つですが、一度使い方に慣れると、メモリの安全性に関してたくさんの恩恵を受けることができ、モダンな構文と設計の直感的さと簡潔さも魅力です。

インタプリタ型言語

コンパイル型言語とは異なり、インタプリタ型言語は実際にソースコードを特別なソフトウェアである「インタプリタ」を使用して実行します。 インタプリタがソースコードの各「高レベル」命令を読み取り、実行します。


graph TD

    subgraph 開発者側
        ソースコード
    end

    subgraph ユーザー側
        ソースコード -->|インタープリタによって解釈される| インタプリタ -->|実行される| ハードウェア上
    end

ある一面では、ソースコードは直接理解することができる「仮想ハードウェア」(インタプリタ)によって実行されると考えることができます。 しかし、インタプリタの実行は最終的には機械コードに帰結するため、実際には、ソースコードが実行時にインタプリタによって動的に機械コードに変換されると考えることができます。

たとえば、次のPythonのコードを考えてみましょう。

a = 125
b = 137
c = a * b
print(f"{a}{b}の積は{c}です")

プログラミングの経験がなくても、上記のコードは理解するのは簡単です。 このコードは、125と137を掛け算し、それをコンソールに表示します (つまり、コードが実行されるターミナルに表示されます)。

このコードはどのようにインタプリタによって実行されるのでしょうか? インタプリタは実際には各行を読み取り、実行します:

  1. 最初の行「a = 125」は、メモリに領域を割り当て、その領域に「a」という名前を付けて、そこに125を書き込むことで実行されます。
  2. 2番目の行「b = 137」も同様に実行されます。
  3. 3番目の行「c = a * b」では、以前に「a」と「b」という名前で割り当てられたメモリ領域を読み取り、それらの値を掛け合わせた結果を「c」という名前で割り当てられた別のメモリ領域に書き込みます。
  4. 4番目の行「print(f"{a}と{b}の積は{c}です")」では、メモリ上の「a」、「b」、および「c」を読み取り、それをコンソールに表示します。

注意

上記の説明は、ハードウェア上で実際に起こることについては完全に正確ではありません。 Pythonコードの実際の実行は、Pythonの実行エンジンであるPython仮想マシンと呼ばれるもののスタックベースのシステムを考慮に入れて行われます。

しかし、今のところハードウェアの詳細についてはあまり心配しないでください。 上記の説明は、インタプリタ型言語が提供する抽象化の良い説明を提供しています。

インタプリタ型言語の利点は次のとおりです:

  • 動的な性質により、コードの柔軟性が高くなります。 たとえば、Pythonでは、変数の型を実行時に変更することができます(整数から文字列への変更など)。 これは通常、コンパイル型言語では禁止されている操作です。
  • 通常学習および使用がはるかに簡単です。 インタプリタは、プログラミングを非常に簡単にするような人間にとってよりフレンドリーな抽象化を提供します。 インタプリタ型言語で同じソフトウェアを書く場合、通常はコンパイル型言語よりもはるかに短いソースコードになります。 また、インタプリタ型言語では通常、メモリを手動で管理する必要はありません。 インタプリタがそれを処理します。
  • クロスプラットフォームおよびクロスISAの互換性がより良いです。 インタプリタは、コンピュータハードウェアにさらなる抽象化レイヤーを提供するため、特定の言語のインタプリタがインストールされているコンピュータ上で、その言語で書かれたソフトウェアを実行できます。 それがISAやオペレーティングシステムによらずできる理由です。

欠点:

  • 遅くてハードウェア効率が悪い。 明らかに、インタプリタがソースコードの各行を読み取り、解析し、実行するというプロセスは、直接機械コードを実行するよりも遅いです。 実際のところ、Pythonコードは等価なC++コードに比べて20倍から200倍遅くなる場合があります。
  • (時には)デバッグが難しい。 インタプリタ型言語の柔軟性のため、デバッグが難しい場合もあります。 極端な場合、インタプリタ型言語のすべてのエラーはランタイムエラーです。 なぜなら、ソースコードのコンパイル時の分析がないからです。 アプリケーションに1回につき1000回起こるバグがあり、アプリケーションを開発するときには気づかれない場合でも、不運なユーザーがそれを実行するとランタイムエラーが発生する可能性が高いです。

ハイブリッド技術

「純粋な」コンパイル/インタプリタ型言語以外にも、いくつかの技術が存在し、その境界を曖昧にします:

Just-In-Time(JIT)コンパイル

この技術により、ソースコードの一部が実行時にコンパイルされることができます。

JITコンパイルにはいくつかの利点があります:

  • JITコンパイラがインストールされているコンピュータでプログラムを実行できます。
  • JITは通常、インタプリタ型言語よりも高速です。 JITコンパイラはソースコードを機械コードにコンパイルするため、実行時の状態にアクセスできるため、通常のコンパイル器よりも利用可能な最適化をいくつか見つけることができます。
Intermediate Language(IL)とVirtual Machine(VM)

一部の言語は、人間が読めない中間表現にソースコードをコンパイルすることをサポートしています。 実行時に、その中間表現は「仮想マシン」と呼ばれるソフトウェアの一部によって実行されます (オペレーティングシステムをインストールできる仮想マシンとは異なります)。 その最もよく知られた例がJavaとそのバイトコード表現です。

このような技術は、速度とクロスプラットフォーム互換性のトレードオフを提供します。 ソースコードが中間表現にコンパイルされるため、コンパイラはいくつかのコンパイル時の最適化を行うことができます。 また、仮想マシンの導入により、中間表現は仮想マシンがインストールされている任意のコンピュータ上で実行されることができます。

中間表現は、ソースコードを記述するためではなく、コンパイラによって生成されるものであることに注意してください。

プログラミングパラダイム:状態と状態を持たない

プログラミング言語を分類する別の方法は、プログラミングパラダイムで分類することです。 基本的に、「プログラミングパラダイム」とは、コンピュータプログラムを設計し理解するためのメンタルモデルであり、またコンピュータの抽象化とも考えることができます。 ほとんどのプログラミング言語では、さまざまなプログラミングパラダイムを使用してソフトウェアを構築することができますが、一部の言語では特定のパラダイムを推奨しています。

一般的に、2つの大きなプログラミングパラダイムのファミリーがあります:状態と状態を持たない。

状態を持つプログラミングパラダイム&オブジェクト指向プログラミング(OOP)

状態を持つプログラミングパラダイムでは、状態の概念を利用し、現代では「オブジェクト指向プログラミング(OOP)」とも同等です。

状態は、プログラムの実行中に変化し、その動作に影響を与えるものです。 通常、これらの状態は、プログラムに割り当てられたメモリの領域に保存されます。 状態を持つプログラミングパラダイムでは、プログラムの動作は入力だけでなく、状態にも依存するため、同じ入力でもプログラムの出力は異なる場合があります。

たとえば、ボタンと保存された数値がある単純なアプリケーションを考えてみましょう。 数値は0から始まり、ボタンが押されるたびに数値が1だけ増え、それが画面に表示されます。 この場合、「数値」がプログラムの状態です。 ボタンが押されるたびに変化し、画面に表示されるものに影響を与えるからです。

以下の図は、状態を持つパラダイムのプログラミングモデルを示しています:

graph TD

    入力 -->|変化させる| プログラムの状態
    入力 -->|影響する| プログラムの動作
    プログラムの状態 -->|影響する| プログラムの動作

状態を持たないプログラミングパラダイム&関数型プログラミング

状態を持たないプログラミングパラダイムでは、関数(数学的な意味で)としてプログラムを抽象化し、任意の副作用を持たないようにします。 状態の使用は通常禁止されます。 状態を持たないプログラミングパラダイムは通常、「関数型プログラミング」とも呼ばれます。

たとえば、2人の人の年齢差を計算するプログラムを考えてみましょう。 関数型プログラミングパラダイムでは、このようなプロセスを以下のように抽象化することができます:

flowchart LR

    人A([人A])
    人B([人B])

    人Aの年齢([人Aの年齢])
    人Bの年齢([人Bの年齢])

    出力([出力])

    人A --> 年齢を取得 --> 人Aの年齢
    人B --> 年齢を取得 --> 人Bの年齢
    人Aの年齢 --> 年齢差を計算
    人Bの年齢 --> 年齢差を計算
    年齢差を計算 --> 出力

ご覧のように、処理の各ステップ(年齢を取得または年齢差を計算)の唯一の役割は、入力を出力に変換することです。 副作用(画面に何かを表示したり、メモリの内容を変更したりすることなど)はありませんし、同じ入力は常に同じ出力になります。 入力に加えて、プログラムの動作に影響を与える状態はありません。

プログラミングパラダイムとプログラミング言語

異なるプログラミングパラダイムには、それぞれ利点と欠点があります。 たとえば、関数型プログラミングは、状態がないためにプログラムの動作をより簡単に理解できます (状態を持つプログラムの動作はより予測不能であり、特に並行性の場合には入力と状態の両方に依存します)が、 状態を持つプログラミングパラダイムは、より強力で複雑なプログラムを作成することができます。

そのため、ほとんどのソフトウェアアプリケーションでは、異なるプログラミングパラダイムを異なる側面で使用することが一般的であり、一つのパラダイムのみを排他的に使用することはありません。 一般的な方法は、状態を持つオブジェクトとしてのプログラムの高レベルの構造を設計し、機能を実装するために関数型プログラミングを使用することです。

同様に、ほとんどの現代のプログラミング言語は、複数のプログラミングパラダイムをサポートしています。 ただし、いくつかの言語では特定のパラダイムを推奨しています。

たとえば、状態を持つパラダイム/オブジェクト指向プログラミングをサポートする言語として:

  • Java:Javaはオブジェクト指向プログラミング(OOP)のために最も人気のある言語の1つです。
  • C/C++:C/C++はOOPのサポートが良好な多パラダイム言語です。
  • Python:Pythonもマルチパラダイム言語です。
  • Rust:先に述べたように、Rustは関数型プログラミングを主に推奨するマルチパラダイム言語ですが、OOPもサポートしています。

状態を持たないパラダイム/関数型プログラミングをサポートする言語としては以下があります。

  • LISP:古いプログラミング言語の1つで、数学に特化しています。 関数型プログラミングを現代において最もよく表す例です。
  • Rust:先に述べたように、Rustは関数型プログラミングを主に推奨するマルチパラダイム言語です。
  • Python:先に述べたように、PythonもOOPや関数型プログラミングに特に傾倒しているわけではないマルチパラダイム言語です。

汎用言語対ドメイン固有言語(DSL)

プログラミング言語を分類する第3の方法は、それが適用範囲で分類することです。 プログラミング言語は、2つの大きなグループに分類することができます: 汎用言語ドメイン固有言語(DSL)です。

汎用言語

汎用言語は、一般的な使用のために設計されたプログラミング言語です。 つまり、何のためのソフトウェアでも構築できる言語です。 前述の言語のほとんどがこのカテゴリに含まれます。 C/C++、Python、Rust、Javaなどを含みます。

ドメイン固有言語

ドメイン固有言語(DSL)は、特定の分野に特化した言語です。 数学、データベース操作、データ分析、人工知能などのような分野に特化したものです。 厳密に言えば、多くのDSLは、このセクションで紹介されたプログラミング言語の定義とは一致しないため、 ソフトウェアを構築するために設計されたものではありません(ソフトウェアを構築するためとは異なります)が、 プログラミング言語を「指定の作業をより簡単にする抽象化またはモデル」という定義に一般化すれば、 DSLはこの定義に合致します。

DSLのいくつかの例:

  • SQL:SQLは、データベース操作のために設計されたDSLです。
  • Mermaid:Mermaidは、グラフ、フローチャート、マインドマップなどをテキスト形式で効率的に表現するためのDSLです。
  • Slint:Slintは、DSLを使用してUI要素を表現するUI(ユーザーインターフェース、ボタン、ウィンドウ、スライドバーなど)のフレームワークです。
  • バッシュ:Bashは、ターミナル上での操作をより簡単にするためのDSLと見なすことができます。
  • MATLAB:MATLABは、数学に特化した人気のあるDSLであり、マトリックスやマトリックスの逆行列など、さまざまな数学的概念と操作をサポートしています。

おもしろい(主観的)なおことば:開発者の感想

個人的には、MATLABがなおも最も人気のある言語の1つであることには驚嘆しています。 その際立たしいIDE、古い構文、反人間的なデザインなどにもかかわらずです (例えば、中括弧ではなく1からインデックスが始まることが一般的で、かっこが使われることが多い)。

MATLABは私が真剣に学んだ最初の言語でしたが、今では私の最も好きではない、最も使用頻度の低い言語です。 私は新しい開発者がMATLABを主要な言語とすることを強く勧めず、 現在MATLABを使用しているユーザーが、自分自身とオープンソースコミュニティのためにPythonに切り替えることを強く奨励します。

結論

このセクションでは、プログラミング言語が何であり、その一般的な分類について説明しました。

一般的に、プログラミング言語は人間のような考え方を持つ存在にとって、コンピュータプログラムの設計、表現、構築、理解を容易にするための抽象化です。

プログラミング言語は3つの異なる方法で分類できます:

  1. 実行モデルによる分類:
    • コンパイル型言語:プログラムの実行前にソースコードが機械コードに変換される。
    • インタプリタ型言語:インタプリタが直接ソースコードを実行する。
    • ハイブリッド技術:Just-In-Time(JIT)コンパイルとIntermediate Language(IL)&仮想マシン(VM)。
  2. プログラミングパラダイムによる分類:
    • 状態を持つパラダイム&オブジェクト指向プログラミング(OOP)
    • 状態を持たないパラダイム&関数型プログラミング
  3. 適用分野による分類:
    • 汎用言語:あらゆるタスクのために使用できるプログラミング言語。
    • ドメイン固有言語(DSL):特定の分野に特化したプログラミング言語。

おめでとうございます! プログラミング言語の高レベルな理解を持つようになりました。 次に、ほぼすべての現代の汎用言語で見られる一般的な抽象化とアイデアについて見ていきます。