低水準なアルゴリズムはプログラミング言語と不可分、高水準なアルゴリズムはコードへの具体化が必要

1年8ヶ月前の記事だが……。

ゼロからプログラミングを学ぶ上で言語選択より大切なこと:It’s Party Time!:エンジニアライフ

上記記事にて、筆者の経験より、

つまり今回、何が言いたいのかといいますと、プログラミングをゼロから勉強するのであれば、「どの言語を勉強するか」という観点より「まずはアルゴリズムをしっかり学ぶ」ことの方が大事だと思うのです。

――こんな主張が書かれている。

筆者の言わんとする内容は分かるし、概ね同意するところなのだけど*1、全く本筋から離れたところで、「アルゴリズム」という言葉を使っている点が気になる。気になるというか、「アルゴリズム」という表現のために誤解してしまう人がいそうというか。

というのも、一言でアルゴリズムといっても、基本情報技術者試験で出てくるような低水準なもの(ソースコードに近いもの)から、クイックソートの概要だとか開発の打ち合わせで「大体こんな感じの流れで」と提示されるアレのような高水準なもの(抽象寄りのもの)まで、幅があるのだ。仮にここでは前者を「低水準アルゴリズム」、後者を「高水準アルゴリズム」と区別しよう。

低水準アルゴリズム

アセンブラ(構造化アセンブラを除く)ならいざ知らず、高水準言語を使う場合、低水準アルゴリズムの内容はそのまま高水準言語で書き下すことができるケースが多い。つまり一般的な高水準言語と低水準アルゴリズムの抽象度はほぼ同じだ(と、このエントリでは仮定する)。このあたり、低水準アルゴリズムの表記にフローチャートが使われることが多いために「フローチャート不要論」が噴出する要因となっている気がする。

(しかし、もしあなたがアセンブラ(構造化アセンブラ除く)でプログラミングしなくてはならないとしたら、低水準アルゴリズムを表現したフローチャートは有用なはずだ。そもそもフローチャートは、高水準言語が存在しない頃に、高水準言語相当の内容を表現するために考え出されたツールなのだ。それに、フローチャートで低水準アルゴリズムしか表現してはならない、なんてルールは存在しない。例えば構造化設計などで用いられるサブプログラム構造図の類は、フローチャートで表現されていたりする――しかしそこには「I := I + 1」という記述は存在しない)

高水準言語と低水準アルゴリズムの抽象度がほぼ同じということは、本来なら「高水準言語の抽象度や抽象化の方向、ライブラリの有無によって低水準アルゴリズムの内容が変化する」ということにつながる。

例えば「1から10まで改行しつつコンソール画面に表示する」というプログラムを考えてみよう。

JavaScript(Node.js)ではこんな感じだ。

var i;
for (i = 1; i <= 10; i++) {
  console.log(i);
}

このアルゴリズムを簡単に書くと「1から10まで数値をカウントアップしつつ、各値を画面出力する」といった感じだろうか。より正確には「変数iを使用し、初期値として1を設定する」とか「変数iの値を画面出力してからカウントアップする」とか、「変数iの値が10を超えたらカウントアップ終了」とか、もっと細かい内容になる。伝統的なアルゴリズムではないだろうか? 仮に基本情報技術者試験で出題されるとしたら、上記に準じたフローチャートないし擬似言語になると思う。

Rubyではどうか?

(1..10).each {|n| puts n }

このアルゴリズムは「1から10まで、各値を順番に画面出力する」となる。JavaScriptでの例のアルゴリズムにもとづく実装も可能だが、比較的抽象度の高いRubyにはそぐわないだろう。

SchemeGauche)ではどうか?

(for-each print (iota 10 1))

このアルゴリズムは「1から10まで昇順に整数を保持するリストを生成し、リストの各要素に対して画面出力用の関数を適用する」となる。Lispの方言なので、LISt Processingらしくリストで抽象化された世界観だからリスト処理するのが王道だと思う。

蛇足ながら、PowerShellではどうか?

1..10

これは……なんと表現するべきだろうか?

ともかく、このような簡単なプログラムでも、使用する言語の抽象度や方向性によって実装が大きく違ってくる。その差異は、低水準アルゴリズムを考える上で大きな影響を及ぼすだろう。上記のJavaScript版に合致した低水準アルゴリズムは、RubySchemeには向いていないのだ。

ただ現状では、長らく手続き型の系譜の言語が使われてきたこともあり、教育の場で提示される低水準アルゴリズムの中身は手続き型の言語を前提としたものが多い、という印象がある。また昨今の開発で使用される言語には抽象度の高いものも増えてきているが、それ以前の抽象度低めの言語を前提とした低水準アルゴリズムも多いように思う。

高水準アルゴリズム

高水準アルゴリズムは、そのまま高水準言語で書き下すことができない。高水準アルゴリズムの抽象度は高水準言語よりも高い。

例えばクイックソートアルゴリズムは高水準アルゴリズムになる。アルゴリズムの解説を読んで仕組みを理解したとして、しかしその時点ですぐさま何の問題もなく実装できる人は少ないだろう。クイックソートアルゴリズムの解説は幾分と抽象的な内容であることが多い。いざ実装しようとすると、例えば「配列を2つの部分配列にして、その部分配列に対して2つの部分配列――という過程をどうやって実現するの?」という疑問から始まり、再帰するとして「停止条件をどうするか?」とか、「スタックが心配なのでループで実装したいけど、どうやるの?」とか、色々と疑問が出てくるものだ(C言語の場合)。

高水準アルゴリズムそのものは高水準言語の影響をさほど受けない。しかし抽象度の違いゆえに、高水準アルゴリズムを高水準言語でそのまま書き下すことはできない。具体化するためには一工程必要だ。そして、その「一工程」の中身は使用する高水準言語に左右される(HaskellC言語では、同じクイックソートでも随分と異なるはずだ)。

低水準アルゴリズムと高水準アルゴリズムの境界は?

特に存在しない。低水準アルゴリズムと高水準アルゴリズムは1つのスペクトラム(連続体)となっている。

しいて言えば、プログラミング言語で直接書く下すことができるか否かが分水嶺だが……プログラミング言語Aでは書き下せないがプログラミング言語Bでは書き下せる、というケースがあるので、この定義では言語によって分水嶺が動くことになる。

アルゴリズム学習」と聞いて思い浮かべるもの

再掲。

つまり今回、何が言いたいのかといいますと、プログラミングをゼロから勉強するのであれば、「どの言語を勉強するか」という観点より「まずはアルゴリズムをしっかり学ぶ」ことの方が大事だと思うのです。

プログラミング学習の初期なので、学ぶアルゴリズムの内容は低水準アルゴリズムや低水準よりの高水準アルゴリズムになるだろう。

ここで、我々が「アルゴリズム学習」と聞いた時、一定の割合で「低水準アルゴリズムを表現したフローチャートを使った座学」というスタイルを思い浮かべる人がいる(私もその1人)。

そのような人たちの中には、

  1. 低水準アルゴリズムはほぼそのまま高水準言語で書き下すことができる。
  2. なら「(低水準アルゴリズムを表現した)フローチャート」は開発には不要だよね。
  3. 不要なフローチャートを使う「アルゴリズム学習」なんていらない!

――このような飛躍した発想にもとづいて「まずはアルゴリズムをしっかり学ぶ」という主張に噛み付く人がいる可能性がある。

もっともこれは、よく考えてみれば「『アルゴリズム学習』にて低水準アルゴリズムを表現する手法として何を使うか?」という問題(フローチャートか、構造化チャートか、擬似コードか、何らかのプログラミング言語か)にすぎない。程度の差はあれどアルゴリズム的なものの学習は必要だろう。

そもそも「アルゴリズムアルゴリズムでも、低水準アルゴリズムの学習は必要なのか?」という疑問も無くはないだろうが、少なくとも私は(プログラミング未経験者への教育の初期段階では)必要だと思っている。

ただ、高水準言語の抽象度等がめまぐるしく変化している一方で、既存のアルゴリズム学習で取り上げられる低水準アルゴリズムの内容や範囲の変化が遅々としている、という面があるように思う。このあたりのミスマッチは解消すべきではないだろうか?

極端な話、例えば伝統的なC言語風forループに合致した繰り返しスタイルの低水準アルゴリズムは、Rubyのeachメソッドを使うスタイルにはそぐわないと思う。または再帰によるスタック枯渇を避けるためにループ化した低水準アルゴリズムと、LuaScalaSchemeで末尾再帰化してみた実装とか。

だから、低水準アルゴリズムに限っては「じゃあ、実際に使用する言語にあわせてアルゴリズム学習の中身を再構築しようか?」という話になるべきだ。

もう一つ、今度は高水準アルゴリズムにも当てはまる話だが、例えば「言語機能 or 標準ライブラリに○○の機能があるから、○○のアルゴリズムの学習は不要だろう」という雑感から飛躍して「現在のアルゴリズム学習自体が不要」という主張に至る人もいて、やはり「まずはアルゴリズムをしっかり学ぶ」という主張に噛み付くかもしれない。

現実には、標準ライブラリ等に実装が用意されている場合でも、少なくともそのアルゴリズムの特性については学習しておくべきだ。

例えばクイックソートは標準ライブラリ等に実装が用意されていることが多いので、使用する言語によっては書けなくても問題ないだろう。しかしその特徴を知っていないと、安定であることが求められる場面でクイックソートしてしまう可能性がある。

二分探索もライブラリに実装が用意されていたりするアルゴリズムだが、自分で実装できなくとも、少なくとも例えば扱うデータが常にソートされている点から「線形探索ではなく二分探索を使えばよい」ということに気づく程度には特徴を知っている必要があるだろう。それに、ライブラリの実装は探索対象のデータがメモリ上にあることが前提となっていたりするので、例えば巨大なログファイル上のログ(1行1ログのテキスト形式で、日時でソートされている)の検索で使用したいのならば、自前で何とか実装できる程度にアルゴリズムを勉強する必要がある。

もちろん、特性を勉強する際に「単なる座学では不十分だから、自前で実装してみよう」という考え方もあるし、その逆パターンで「実装を読み解いて、アルゴリズムの解説との対応を調べる」という方法もあるので、学習の過程で「自前で実装する」という行為は否定しない。学習の一貫として自前で実装することと、実際の開発で既製品を用いるか自前で実装するかという問いは、分けて考えるべきだ。*2

個人的には「『どの言語を勉強するか』という観点より『まずはアルゴリズムをしっかり学ぶ』ことの方が大事」とまでは主張できない。低水準アルゴリズムにおいては採用する言語にもとづいて学ぶべき範囲・内容を調整すべきだと考えているし、高水準アルゴリズムにおいては実際に実装する際の工夫や注意点が使用する言語によって異なるからだ。

しかしそれはアルゴリズムの学習そのものを否定したり軽視したりするものではない。言語によって姿形は変わるものの、「どうやって問題を解くか」という解法としてのアルゴリズムは重要なのだ。

*1:これは、自分が組込み系のCプログラマだからかもしれないが、自分自身が書いたコードについて、それがどういう意味を持ち(==どのようなアルゴリズムで)、どういう効果をもたらすのか(==どのような結果を得られるのか)、分からないのは恐ろしい。

*2:私個人としては、少なくとも実際の開発においては、欲しい機能が標準ライブラリや枯れたメジャーなライブラリに用意されているなら、よっぽど酷い代物でもない限りそれを使うだろう。