プログラミング言語の選び方、そのやり方を学ぶ

(編注:2016/7/27、頂いたフィードバックを元に記事を修正いたしました。)

プログラムを習得しようとする場合に最初に決めることの1つは、どのプログラミング言語を学ぶかです。誰かが決めてくれたり、その言語を使うクラス、あるいは使う必要のあるフレームワークによって指示されていたりする場合もありますが、少なくとも幾つかの選択肢があることが多いでしょう。

1つ目(そして2つ目)の言語を選ぶのは、なかなか難しいものです。というのも、選択肢が多過ぎるのです。ある言語と別の言語がどう違うかは、必ずしも明確ではありません。自分がしたいことを実現するのにどちらが “より適切” かとなると、さらによく分からなくなります。本稿では、選択肢となり得る言語の概要と、各言語を差別化するものについて説明することによって、読者の皆さんが確かな情報に基づいて学ぶ言語を決定できるよう、手助けをしたいと思います。

本稿は、言語のレビューでも、 “最善の” プログラミング言語を見つけるヒントでもありません。言語の様々なタイプについて説明し、各タイプに属する言語の例を挙げますが、どのカテゴリの言語にするかや、その中のどの言語にするかという選択は、ご自分で行ってください。本稿を読んで、十分なコンテキストと用語を知り、ご自分で確かな情報に基づいて厳密に言語の調査を続けられるようになれば幸いです。

言語を選択する

プログラミング言語は、一般に多くの様々なカテゴリに分類されます。これらは、しばしば文献内で “パラダイム” と呼ばれます。本稿でも触れますが、どのパラダイムに属するかの境界線が明確でない場合が珍しくなく、その傾向は、特により新しい言語で顕著です。多くの言語は、非常に似通っていて、違いは、大抵、機能にではなく、構文、エコシステム、コーディング規約といったものにあります。しかしながら、パラダイムは、詳細を見比べる言語の数を絞るのに役立ちます。

その中で、比較する言語は、全て同じことができるということを心に留めておくことが重要です。つまり、どの言語でも、画面への表示、計算、ファイルの読み込み、インターネットへの接続などの操作ができるのです。それをどのように実現するかが異なり、差が出るのは、多くの場合、使いやすさです。言語によって、統計的計算により適していたり、Webサイトの構築に向いていたり、ハードウェアとの対話が得意だったりします。これが、まずどのようなことをしたいのかをよく考えてから言語を探し始めることが重要である理由の一つです。

抽象化レベル

プログラミング言語を最も明確に区別できる特徴の1つから説明しましょう。ハードウェアの抽象化のレベルです。コンピュータの内部動作の多くがプログラマから見えにくい言語は、通常 “高水準” 、それに対して、こういった低い水準の詳細がプログラマからよく見える言語は、 “低水準” と呼ばれます。この区別は、カテゴリ分けというよりもレベル分けです。多くの言語は、抽象化レベルにおいては、 “中間水準” と考えることができます。

一般に、抽象化には代償が伴い、より高い水準の言語ほど、パフォーマンスが下がります。とは言うものの、高水準言語と呼ばれるものでも、そのパフォーマンスはほとんどのアプリケーションの実行に差し支えないレベルです。従って、アプリケーションの許容範囲のパフォーマンスが出せる “最も高い” 水準の中から言語を見つけるべきです。

また、抽象度が増すと、プログラムの内部動作がさらに見えにくくなります。つまり、裏で繰り広げられる “マジック” が増えるということです。プログラマから見えないということは、それによってプログラマの入力が減るため、望ましい機能とみなされる場合も多いですが、プログラムが異常終了したりパフォーマンスが期待値に及ばなかったりして、その原因を探る必要が出てくると、もどかしく思うでしょう。どの程度のマジックが適度かは、個人の好みの問題で、多くの言語で試して初めてどの程度が自分にとって心地良いのか分かるかもしれません。

話が複雑になる前に、この水準の複数の異なる “層” と各層に当てはまる言語の例を見ましょう。

  • 抽象化は皆無あるいはほとんどない 一番下の水準には、一般にアセンブリコードと言われる言語があります。アセンブリは、CPUが理解できるマシンへの命令をほぼそのままマッピングするものです。従って、アセンブリのマッピングはCPUタイプごとに異なり、多くの種類が存在します。コード内の命令文は、1文がマシンへの1つの指示となっています。例えば、「この値をメモリ内のこの場所に格納しなさい」、あるいは、「メモリ内のこの場所とあの場所に置かれている値を足しなさい」というようなものです。

    この種のコードは、通常、例えば、オペレーションシステムのカーネルのように、非常に低い水準のデバイスと対話する場合や、動画のデコードのように、手持ちのハードウェアのパフォーマンスを最大限まで引き出す場合に必要なコードにのみ使われます。初めてのプログラミング言語としてアセンブリ言語を選択することは、ほとんどありません。

  • マシン非依存 さらに少し進んでみると、C言語があります(明らかに、同じC系言語のC++ではありません)。このカテゴリに属する言語もやはり、比較的低水準です。コードのマッピングは、ハードウェアが実行する計算と似ていて、両方とも同じようにメモリの管理を要求します(「2MBのメモリをください」、「もう、使い終わったので必要ありません」といった感じです)。しかし、関数(再利用できるコードの集まり)やループ(複数回、実行されるコード)、(メモリに対する注釈であり、例えば文字ではなく数字を含むということを示す)といった、抽象性も提供します。

    このカテゴリの言語で書かれたコードは、通常コンパイルされます。これは、書いたコードを実行する前に、必ず「コンパイラ」に通すということです。コンパイラとはハードウェアが理解できるコードにプログラムを変換するためのプログラムです。これによってC言語はマシンに依存しない言語となります。コードは一度書くだけでよく、ハードウェアのタイプに合わせてコンパイルされます。

    C言語ライクな言語は、(コードが密接にハードウェアにマッピングされるため)機能性が優れていることが多いことと、概念的にとても単純であることから(ここにマジックはほとんどありません―あなたが書いたコードが実行されます)、人気があります。これらの言語の欠点は、プログラムの全てのステップを詳細に書き出さなければいけないため、大量に長いコードを書かなければいけないことです。もし、パフォーマンスが一番気になる事項なら、これらの言語を検討してみてください。

  • 中間水準の言語 ここで説明するカテゴリでは、高パフォーマンスを目指しながら、さらにハードウェアで使われる低水準の言語に抽象性を加えた言語をご紹介します。ここで加えられる抽象性には様々な形態が存在しますが、一般的な例としてはクロージャ(ざっくり説明すると、プログラムの実行中に構築される関数です)や、半自動のメモリ管理ジェネリクス(違う型のデータを操作できる関数。例えば、文字列と数字のどちらかをキーワードとして使える辞書)などです。これらを使えば、もっと簡潔なコードを書き、一段階ずつの長ったらしい列挙型のデータをオフロードしてコンパイルできます。例として、このカテゴリで有名な言語にC++、Swift、Rustがあります。

  • ランタイムを伴う言語 この層の言語にはランタイムも含まれています。プログラムを実行すると、ガベージコレクション(不要なメモリを自動的に処理する機能)や、グリーンスレッド(マシンに複数のプロセッサを搭載するよりも、より効率的に、より多くの計算を並行して実行する機能)のような、言語の一部である他のコードが同時に実行されます。これらの機能はパフォーマンスペナルティ(パフォーマンスに敏感なアプリケーションをプログラミングしていなければ、おそらく気が付きません)を伴うことが一般的ですが、プログラミングをよりシンプルに、より安全にしてくれます。

    ランタイムがあると、他の言語と相互作用するプログラムを書くのが難しいというケースが多くなります。例えば、複雑な数学的な計算を高いパフォーマンスで実装するというような場合です。具体的には、FORTRANやCに実装されることの多い大規模な行列乗算があります。この場合、ランタイムを持つ言語では、そのライブラリの優位性を活かすことが困難です。このカテゴリで人気のある言語には、JAVA、Scala、Go、C#が挙げられます(開発環境が全て.NET Frameworkの場合)。

  • インタプリタ言語 この層の言語を使って書かれたプログラムは、上述のカテゴリに含まれる言語よりずっと遅いというのが一般的ですが、書くのは非常に簡単という場合が多いです。インタプリタ言語で書かれたコードの最大の強みは、一部だけを実行できる点です。コードの一部を書き、実行して、それからもっと多くのコードを書き、先に実行したコードに続くように、新しいコードを実行することができます。一度きりの計算を1つずつ素早く構築したい時に便利です。また、プログラムのどこがうまく機能してないのかを調べたい時にも役に立ちます(プログラムを実行させながら状態を調べることができます)。

    多くの場合、インタプリタ言語はコードの挙動についても、かなり寛大です。モンキーパッチ(実行プログラムの振る舞いを変更します)やコードの評価(一部のプログラミングを実行しながら、ファイルやネットワークから読み取ったコードを実行します)、型の変換(数字を含む文字列を直接、単純な数字として使えます)などが可能になります。しかし、この寛大さは新しい種類のバグを導いてしまいます。そうなると、どのコードが実行されていないのかが瞬時に分からないので、バグを見つけて修正することが難しくなります。そのため、複雑で実行時間の長いソフトウェアは、通常、コンパイルされた言語で書かれ、インタプリタ言語はコードの管理ツールや、データアナリティクス、一度きりのスクリプト、Webサイトなどで使われます。Webサイトの開発は、コード上を素早く行き来するという能力が重要な例です。ここでは、インタプリタ言語の少し書いたらすぐ実行というアプローチがぴったりです。

    世の中にはインタプリタ言語がたくさん存在しています。最も人気が高い言語は、Python、PHP、JavaScript、Perl、Ruby、Luaなどです。

  • 特殊な言語 ある特定のユースケースに対応するために構築された言語があります。多くの場合、どのレベルの抽象性が提供されるのかを示すのは困難です。対象となる使用に関しては非常に高水準ですが、標準から離れることを行う場合にはかなり低水準になるためです。通常、このような言語を使うのは、この言語を構築した目的と全く同じことを行いたい場合に限られます。一般的な例に、R(統計的な計算)、MATLAB(高度な数値計算)、SQL(データベースのクエリ)、Coq(プログラミングに関する証明の記述)、Prolog(論理型の推論)などがあります。この特殊な言語についてはあまり取り上げません。なぜなら、あなたが使う必要に迫られる場合は、その言語について知っているはずだからです。

  • 高水準な言語 この言語は、ここまでに説明してきた言語が使う計算モデルとは一線を画すものが多いです。これは、ハードウェアがコードを実行する方法(小規模な計算またはメモリ操作を1つずつ行う)と簡単に一致させられるため、開発者はアルゴリズムの高水準なプロパティに集中することができます。この言語で書かれるコードは、様々な面でマシン語というよりも実行できる数式という感じがします。結果として、この言語で書かれたプログラムは、コンパイルさえうまくできれば、バグが少なく、より正確に動作することが多いです。

    この抽象化レベルの言語は “従来の” プログラムを構築する時に使用でき、使用されてきました。しかし本当に力を発揮するのは、他のプログラム、または自分のコードの既に検証済みのプロパティや不変条件の振る舞いを解析し結論を出す時です。例えば、これらの言語はプログラムが特有の条件下では決してエラーを出さないことや、パフォーマンスの最適化をしても、最適化をしていない遅い方の実装と同じ答えを出すことを証明できます。

    パフォーマンスが特に重視されない場合や、コードの正確性について厳密な保証が欲しい場合なら、このような言語を使ってみたくなるでしょう。ただし、使い始めは少し取っ付きにくいかもしれないので覚悟してください。あなたの書いたコードが実際に正確であることをコンパイラに認識させるまで、苦労する場合があるためです。この層に属する言語で有名なものとして、Haskell、F#、Coq、LISP、OCamlが挙げられます。

厳格性

こうして適切な抽象化レベルを決定しても、学ぶ言語を選定するまでには、大抵の場合、そこからまた長い道のりになります。各層に含まれる言語は、機能面ではかなり似通っているものの、構文はそれぞれ異なっているためです。そこで、同一の層に属する言語をさらに細かく評価するため、新たな水準を持ち込むのが有用かもしれません。この状況で私が言語を比較する際にたびたび使う水準が、 “厳格性” です。厳格性の高い言語は、コードを書く際の難易度も高くなります。その言語のコンパイラが、書いたコードに対して「言語の文法に “正しく” 準拠している」と認識するための基準が厳格になるためです。しかしそのコードは、コンパイルに成功しさえすれば、コードを書く際の苦労は正しいことだったのだと確信できます。逆に、厳格性の低い言語はコードを書く際の制約が少なくなりますが、プログラムの実行中に問題が発生して異常終了する確率も高くなります。

厳格性の高低とはどんなものかを示すため、最も低いレベルから順番に、どういう状況を指すのかを以下で説明します。

  • やりたい放題 このレベルの言語では、ほとんどのことが何のチェックもされずに許容されます。Sの文字をtrueの値に追加したい?いいですね、それでいきましょう。+の動作は、引数を全て無視し、常に「1つの指輪が全てを支配する」という文字列を返すようにする?それも問題ありません。プログラミング言語にここまで柔軟性があると、本当に面白い振る舞いを実現することができます。例えばプログラムの実行中に評価やコードの変更を可能にすることもできます。一方これは、書いたコードに、実行してみるまで確認されない部分もあることを意味します。それを覚悟の上でこの言語を選択する、または実行したい動作が、プログラムに誤りが含まれていた時に再試行しても大ごとにはならないようなものならば、この言語は優れたものだと言えるでしょう。しかし実行結果が、実行→クラッシュ→修正→再実行のループとなった場合は、いら立ちが募る状況になるので、別の言語を試したくなるかもしれません。

    このカテゴリに属する言語には、JavaScript、PHP、Perl、Ruby、そして議論の余地はありますがLISPがあります。一方、とんでもなくばかげたコードを記述していないかどうか程度はチェックする、もう少し厳格な言語もありますが、大きく分けると、そんな言語もこのカテゴリに属します。PythonとTypeScriptは、こうした言語の代表例です。これらは「動的型付け」言語と呼ばれることもあります。

  • 合理性を維持しようと努力している このレベルの言語は、コードが論理的に振る舞うことを前提としています。例えば+ が 2 つの数値を引数に取って、1つの数値を返す関数だった場合、この関数を上書きして、trueの論理値を返すように変更することはできません。この前提によって、実行時の型が不正であることから発生するバグを多数除外しています。しかし同時に、例えばユーザーの入力を文字列から数値へ変換するのが、より困難になるという側面もあります。このレベルの言語でもやはり、チェックの多くを回避するためのショートカット(例えばGoではinterface{} 、C言語ではvoid* 、JavaではObject)は存在しますが、全般的に、論理的なコードを書くように強要されます。この種の言語の例としては、Go、C、C++、Javaがあります。「静的で強い型付け」の言語と言われることもあります。「静的型付け」と「強い型付け」との間には違いがあると主張する文献もありますが、どちらもこの層に属します。最上位層の言語では両方が必要になります。

  • 不正は絶対許さない この層は、「おふざけは一切なし」の世界です。プログラムの中で数値と文字列を混同するような、愚かな振る舞いはしていないことをコンパイラに認識させる必要があるだけでなく、危険なことは一切しないことを保証する必要もあります。従って、この種の言語でプログラムを書く場合、不変データを書き換えること、複数のデータが同時に書き換えられるように設定すること、ワイプ操作を実行した後のデータを読み込むことなどはできない場合があります。このルールを実現する具体的な仕組みとしては、データの変更を許可しない(Haskell)、コンパイル時に複数のプロパティをチェックする(Rust)など、様々なアプローチがあります。また、この2つ以外で同じカテゴリに属する言語として、Scala、Swift、F#があります。

学習の始め方は決まりがあるのか?

プログラミングを学びたい意欲はあるけれど、具体的に思い描いているユースケースがほとんどない、あるいは全くない場合、上記のどのカテゴリから選べばいいのか、見当が付けにくいかもしれません。こんな状況に置かれている人は、より強く興味を引かれているのがどの分野なのかを自問するといいでしょう。低水準言語を学ぶ場合、必然的にコンピュータのCPUとメモリの動作を中心に学習を進めることになります。その結果、オペレーティングシステム(OS)、デバイスドライバ、リソースを最大限に活用するゲームなどを構築する時、製品のパフォーマンスを向上させるにはどうすればいいのか見当がつけられるほどの、確かな基礎知識を身に着けることができます。一方、高水準言語から学習を始める場合、低水準言語に含まれるハードウェア制御の詳細を気にかけることなく、すぐにアルゴリズムやデータ構造に取り組むことができるという特徴があります。過去に数学またはそれに近い分野の学習経験がある場合や、早く解決したい問題に直面している場合は(例:データアナリティクス、ちょっとした便利ツールを作りたい、Webサイトなど)、こちらが向いているかもしれません。

言語の切り替え

上記の説明で、皆さんがどの言語を選んで学び始めればいいのか、適切な判断が下せると期待しています。ただし避けられないことですが、学習を進めるうちに、選んだ言語が自分のやりたいタスクには不向きと気付いたり、その言語でよく理解できない部分があって学習が進まずイライラしたりすることがあります。こんな事態に直面した時には、迷わず別の言語に切り替えて学習を続けましょう。多くの場合、新しい言語はほとんど、構文については今まで学んでいたものと違う印象を受けるでしょう。特に、同格性や抽象化について同水準の別言語を選んだ場合はなおさらです。

“遠く離れた” 言語に切り替える場合は、新しく学ばなければいけない概念が多いので、より多くの困難が伴います。幸い、それまでに学んだことを新しい言語に流用することは簡単にできます。変数、文字列、関数、モジュールなどの概念は、ほとんどの言語に共通するからです。さらに、学んだ言語の数が増えるほど、学習は容易になっていきます。経験豊富な開発者が大抵、幾つもの言語に通じている理由はここにあります。ある言語を一度学んでいれば、別の言語を学ぶのは簡単です。そして新しい言語を学んだことによって、既に使っている言語でプログラミングする際、その作法が変わることは珍しくありません。新しい言語を選ぶ、または別の言語の作法をごっそり流用するのは、プログラマの成長過程において自然なことです。恐れずに挑みましょう。

では幸運を祈ります。どの言語を学ぶかで悩み過ぎないようにしてください。特に学習を始めたばかりなら、仮にさらに別の言語を学びたくなったとしても、今学んでいることは後で必ず役立ちます。