Chienomi

プログラミング学習において特定言語を前提とすべきでない理由

プログラミング::beginners

プログラミングの学習について語るとき、なぜか純然たるプログラミングの話ではなく、SEとしての仕事の話を前提として喧伝されることが多い。

前提として、プログラミングを行う者はSEだけでなく、さらにプログラミングを業務とする者もSEだけではない。 そして、まさかホビイストの技術がSEに劣るなどということもない。

これらの目的をごっちゃにしてはいけない。 つまり、プログラミングの習得はあくまで「プログラミングを習得すること」である。 プログラミングの基本要素は「考察」「設計」「実装」である。

これはプログラミングであって、プログラムの基本要素ではない。 そして、プログラミング言語の基本要素でもない。

そして、当たり前の話として、プログラミングの習得というのは、個別要素の暗記ではない。 ピアノを習得するときに、ピアノの技法や演奏を身につけようとするものであり、決して楽譜を暗記する(暗譜する)ことではない。 もちろん、演奏の一環として暗譜することもあるが、十分な技術があれば初見でも演奏可能だし、暗譜は優先されることではなく、相応の演奏力がある前提で、その演奏を実現するために楽譜があり、楽譜による演奏をより高めるためにそれを覚えるということがある。

プログラミングにおいても、関数や記法を丸覚えすることや、フレームワークを記憶することは技量に寄与しない(もちろん、それを理解するために技量を身につければ、その分の技量は増進するだろう)。

Mimir Yokohamaの授業においては、プログラミング関連授業より前に「プログラミング基礎初級」という授業をウケることを強く推奨している。 「プログラミング基礎初級」授業にはプログラミング言語の設定がなく、主にPerl, Ruby, JavaScript、そして補助的にPython, Lua, Bash, PHP, Cが登場する。

基礎初級以外のプログラミング授業はチェックとして以下のような問いに応えられるかによってチェックされる。(示しているのはごく一部)

  • イテレータを書く
  • 変数の意義と、実際に変数に格納されているものはなにか
  • 複数に代入された配列に対する操作の挙動
  • 式と文の違い
  • 内部イテレータと外部イテレータの説明

また、この記事はプログラミングを教える程度の立場(あるいは技量)にある者にとっての教え方に関する視点で述べられている。 もちろん、初学者が初学に染める上で何に留意すべきか、また特定言語を前提とすることをなぜ避けるべきかということを知る上でも価値はあるが、残念ながら本記事の内容は(可能な限り分かりやすく説明するよう心がけてはいるものの)初学者が理解できるような内容ではない。

そもそも、何が問題であり、どのように違うのかということは、対象について知っていることを前提として説明をせざるをえず、それらを理解するのは私の教えるプログラミング授業の成果であると言ってよく、今これらを読んで理解できるに至れば、それはもはや初学は達成したと言って差し支えない。

おまじない?

私の基準では、プログラミングにおいて「おまじない」という表現を用いる説明は全てが参考にすべきでないものである、と考えている。

これには明確な理由がある。

プログラミングの学習とは、理解へ至る道である。 決して、暗記作業ではないし、テストで点をとる作業でもない。

これを前提として考えると、

  • いかにして理解へ導くか
  • 何を理解しようとするか

ということが、その根幹にある。 その内容に対する良し悪しや、注文はあるかもしれないが、これがそもそも「ない」という話だと、教えようとしている内容も、教える手法も存在していないことになるため、もはや学習としての体をなしていない。 これは、主義主張が割れる余地のあるような話ではなくて、「教える」ということは「何を教えるのか」「いかにして教えるのか」ということありきなのは必定であり、自明である。

プログラミングを理解するにあたり、問題はその「おまじない」は理解する必要がないのか、ということである。

もちろん、内容によっては教えようとすることがデッドロックしてしまうため、一旦「そういうものである」とした上で説明を進めるということがあるが、その「おまじない」はほとんどの場合、総合的な知識として先に教えられないような事情があるものではなく、 「おまじない」として説明したものは大抵の場合説明されることはない。

例えば

process.stdin.resume()
process.stdin.setEncoding('utf8')

var inputString = "";
var reader = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
})
reader.on('line', (line) => {
  inputString = line
})

をおまじない、と教えているものをみたことがある。

これはひどい

これを理解する必要はなく、覚えてしまえば良いのだ、ということだが、 それを理解する機会がないまま、これを理解できない状態で「プログラミングを習得した」と言えるだろうか。

後で覚えれば良いというようなものではなく、「おまじない」とすることで試すことも封じている。 「おまじない」は不可侵のものであり、「値を変更してみる」といった学習作業に対象から除外してしまうからだ。

Node.jsの場合、標準入出力の扱いは、かなりJavaScriptに関する踏み込んだ知識が必要になる。 学習をJavaScriptに限定してしまうと、標準入出力やファイルディスクリプタといった基本的で不可欠な概念を理解するのが非常に困難になる。 この概念を理解するにはRubyが便利だ。1

だが、Rubyでファイルディスクリプタを理解しようとしたときに、IO実体に対してファイルディスクリプタが抽象化されていることを示すのを理解しようとしたときに、IO#reopenはあまりにも簡便すぎてわかりづらい。この部分がピンとこない人にはC言語で説明すると明瞭になる。 (C言語ではファイルディスクリプタ番号を主として扱うためである)

困難を生じる例

参照

JavaScriptやPHPの場合、値が参照であるか否かというのは透過的である。 透過的というと不適切ではないかもしれない。つまり、コントロールできるわけでも、値がなんであるかを定義するまでもなく明らかであるわけでもない。

この場合、「参照とはなんであるか」を理解するチャンスがない。 だから、それが参照なのか値なのかということを意識することが難しく、引数が参照であるということが何を意味するかわからない、というケースがよくある。

Rubyも以前は明示的であったが、現在は透過的になっている。 というのも、Integerの一部(旧Fixnum相当)とFloatの一部、さらにnil, true, falseSymbolが即値である。 将来的にはStringが条件により即値を持つようなことも考えられるから、やはり理解には適さない。

これを理解するためには、CやPerlなどを使う必要がある。

Perlでは次の変数は値であり、

$v = "Hello";

print $v;

次の変数は値への参照の値である。

$v = \"Hello";

print $v;

これの実行結果は例えば次のようになる。

% perl 1.pl 
SCALAR(0x5584a41cd700)

参照はそれを参照(デリファレンス)することにより参照されている値を得ることができる。

$v = \"Hello";

print $$v;
% perl 1.pl 
Hello

この場合の$vは参照の値、つまり、「何を参照していますよ、という値」である。 だから、その値をコピーすれば参照情報がコピーされ、コピーされた参照情報であっても、それをデリファレンスすれば結果は同じである。

#!/bin/perl

$v = \"Hello";
$x = $v;

print $$x;
% perl 1.pl 
Hello

「なんで参照の値渡しなんだ、真の参照渡しじゃないのか!」と思うかもしれないが、実際に真の参照渡しを欲することなんてあまりないと思うので、 真の参照渡しの話はやめておこうと思う。(C#はどっちもできるらしいが、私がC#をちゃんと理解していない)

Perlの場合、「参照」はリファレンス、「参照の値」はリファレント、「参照を参照すること」はデリファレンスと呼び分けられていて、わかりやすい。

これについて、ここではかなり駆け足で説明したので、今参照渡しを理解していない人には何を言っているのか理解できないと思うが、 重要なのは、「参照と値の違いを、JavaScript, PHPなどのみによって説明する場合、コードサンプルによって十分に説明する方法がない」ということである。 (Rubyの場合はObject#dupObject#cloneを使えば強引に説明することはできる)

オブジェクト

もう昔話に近いが、「オブジェクト指向が理解できない」という人は結構多かった。 この問題は、従来のプログラミングとオブジェクト指向の考え方に差異があり、考え方をマッピングすることができなかったためだ。 私も、Rubyを初めて使ったのは2002年で、Rubyがちゃんと使えるようになったのは2008年だから、実に6年余りもオブジェクト指向が理解できなかったことになる。

Perl5にもオブジェクト指向プログラミングはあるのだが、今見てもかなり気持ちわるい。 Matzによれば「オブジェクトの内蔵を見せられているよう」だそうな。

次の例はPerlでStringクラスを定義して、オブジェクトを生成している。

package String;
sub to_str { 
  my ($self) = @_;
  return $self->{value};
}
sub new {
  my $invocant = shift;
  my $class = "String";
  my $self = { 
    value => shift
  };
  bless($self, $class);
  return $self;
}

my $str = String->new("Hello");
print $str->to_str();

確かに、Perlを使ってオブジェクト指向プログラミングを学べば、「あぁ、オブジェクト指向ってこうなってるんだぁ」というのは理解できる。 のだが、明らかにこの理解はオブジェクト指向プログラミングを理解する上で必要のないものである。 ここで理解できるのはオブジェクト指向の機構の内部であり、オブジェクト指向プログラミング言語においてユーザーが知る必要は全くない部分だ。

Rubyで言えば

class MyString
  def initialize(str)
    @str = str
  end

  def to_str
    @str
  end
end

str = MyString.new("Hello")
puts str.to_str

という話であり、Perlのオブジェクト指向は「オブジェクト指向の具体的な仕組みを理解できる」というメリットをかき消して余りあるほど、「オブジェクト指向とはなんなのか理解するには余計な要素をあまりにも大量に必要とし、理解すべき事柄がなんなのかをつかむことすら困難」というデメリットのほうが大きい。

イテレータ

イテレータはモダンなプログラミング言語には例外と並び不可欠なもののひとつだ。

だが、イテレータをどう扱うかというのは、割と最近大きく変わった。

次に示すのはBashのイテレータである。

for i in $@
do
  echo $i
done

JavaScriptにはイテレータがない。 この「イテレータがない」というのには明確な理由がある。 それは、全てのオブジェクトが連想配列を基本としており、なおかつその連想配列はオブジェクトとしての基本情報を持ったものであるため、イテレーションすべきコレクションが独立して定義できないのだ。

一応、for each ... inというものをイテレータだとみなすこともできる。

for each (i in obj) {
  console.log(i)
}

だが、これは「何がコレクションに含まれるのか」が非常に分かりづらく、今や廃止となってしまった。

for ...ofとかObject.keysが追加されたのもちょっと後になってからであり、連想配列に対するイテレーションというのはちょっと困難な面があった。だが、それでも比較的早期ではあり、イテレーションにはこれを使用する。

それよりも早期に入ったのがArray.lengthである。これは、配列限定のObject.keysの数を数えるようなものである。

だから、JavaScriptでは

for (var i=0; i < obj.length; i++) {
  console.log(obj[i])
}

のようにするのが基本であった。 JavaScriptでは3パートforループにnon-arithmeticな内容が書けるかどうかは、処理系依存なのだけど、まぁv8は書ける。

var x = "F"
for (console.log("Start"); x != "Foo"; console.log("Next")) {
        x = x + "o"
}

3パートforループはあくまでループであり、特殊な構造なwhileである。 (Perlはwhileで全く同じに書ける)

一方、コレクションのために3パートforループを使うよりもイテレータを使うようになってきている。 リストイテレータ(配列イテレータ)に関しては割と昔からあった。Perlも

foreach $i (@ary) {
  print $i;
}

とかできるようになっている。ただ、これはリストに対する繰り返しであり、イテレーションのためにリストを展開する必要がある。例えば

foreach $i (1..100) {
  print "$i\n";
}

とか書くことはできるが、これは100個のリストに展開される。

イテレータを流行らせたのはRubyで、そのポイントは「Rubyのブロックがイテレータをめちゃくちゃ使いやすくしたから」だ。

obj.each do |i|
  puts i
end

それ以前にもこういう汎用性のあるイテレータそのものは存在した。例えばLispでは

(foreach ary (lambda (i) (puts i)))

とかできる。Pythonは

for i in obj:
  print(i)

って感じである。

しかし、とにかくRubyの「イテレータはそのオブジェクトの特定のメソッドを高階関数として呼び出すもの」方式は柔軟性が高く、めちゃくちゃ便利だった。このため、JavaScriptフレームワークが次々とRubyっぽいイテレータを採用し、ついにはJavaScriptにも入った。(配列だけだけど。最近はNodeListでもできる)

ary.forEach(->(i) {console.log(i)})

function() {...}形式だと長いし、本体が長くなると見づらいのが難点だけれど、やっぱり使いやすい。

さて、これらを踏まえた上でだが、イテレータがある言語とない言語があり、またイテレータをどのように表現するか、イテレータがどのような位置づけになっているかというのは、言語によって大きく異なる。 リストイテレータしかないような言語だと巨大なコレクションをイテレータで回すのは難しかったりする。

さらに、イテレータ自体が配列に話を限るならそうでないとしても、非常に簡単な概念であるにも関わらず多くの人にとって理解の難しいものでもある。 そのような人にとってイテレータを理解するには多角的にイテレータを見る必要があり、単一の言語での理解は難しい。

概念的プログラミング

プログラミングを学ぶ上で、「何をしようとしているのか」ということを端的に示すにあたりコードを書く、というのはよくある。

論理を示すために、本質部分だけを記述した簡潔な「擬似言語」によって表現するということはよくある。 特にCやJavaの場合、変数の宣言など非本質的部分の記述量が多い。

この問題は、右も左もわからない初学者にとっては、今説明されている内容にあたって何を見るべきなのかのかがわからず、思考を構成できないということに由来する。 擬似言語の必要性はプログラミング言語に慣れた者には理解されづらい面があるのだ。

Rubyは「実行可能擬似言語」を目標としているそうで、非常に簡潔に書けるし、非本質的部分は非常に少なくて済む。 だが、そのRubyですら「バッチリ」ではない。

例えば

num = gets
puts num + 10

とかやるとエラーになる。Perlはこういうことがしやすいようにしてあるので問題ないのだが

$num = <>;
print $num + 10

それは、Perlにとって「書きやすいようにしてあるから」という話であり、Perlが擬似言語として理想的な記述ができるわけではない。 それでも、Perl, JavaScript, Rubyはその気になれば非本質的部分を省略できる度合いが強く、初学に適しているといえるが、Rubyが幅広く学習に適し、なおかつ非常にわかりやすく簡潔に書ける一方、PerlやJavaScriptのように大胆な力技が使えずにそこそこちゃんと書かなければならないケースというのもある。例えば

100.times do
  x = ( x || 0 ) + 2
  puts x 
end

とか。 (いや、この例もxでなく$xでやればいいだけなので、あんまり適切な例ではないのだが、適切な例がパッと出てこなかった)

だから、これらを混ぜ合わせたような論理言語によって表現したいこともある。 あとは、int()やらstr()やらで常に希望する型が得られると想定した説明をしたいこともある。

段階的に学習していくため、初学では余計な要素に気を取られず、ただここだけを見れば良いというのを示したい。 これが、特定の言語での学習というように予め決めてしまうと、このような学習が成立せず、結局「丸覚え」のような学習としては何の役にも立たないことになりやすい。

弊害となるケースの例

改行とセミコロン

モダンなプログラミング言語だと、;は文ターミネータになるが、なくても良い、というのが一般的だ。 なくても良い、というのは、改行が文ターミネータになりうるが、コンテキストが継続している場合はターミネートしない、ということだ。

;を必須とするのは結構古い文化で、C言語なんかがそう。 そして、Perlもそうだ。

これは、

print
"Hello";

と書けるかどうかである。これはカッコが必須な言語だと話が変わってしまうのでおいとくとして、 Perlだと"Hello"printの引数となり、出力される。 Rubyの場合、これは2つの文だと認識されるため、printは何も出力しない。

曖昧な書き方はそもそもできない、という言語が多いが、 「改行がターミネータになるかどうか」「セミコロンが必要かどうか」というあたりに差が出る。 そして、これらは単純な部分なだけに、「ここに差異がある」ということに気づかず、他の言語を習得する際に誤った解釈となる書き方をしてハマるのは、いわば風物詩となっている。

なお、JavaScriptは文化的にセミコロンをつける人が多いが、これは、「JavaScriptにJavaの感覚を持ち込んでる人が多い」ということに由来する。 「プログラミングJavaScript」のDavid Flanagan氏もそのひとりで、同書で

var com;
if (!com) com = {};
else if (typeof com != "object")
    throw new Error("com already exists and is not an object");

if (!com.davidflanagan) com.davidflanagan = {}
else if (typeof com.davidflanagan != "object")
    throw new Error("com.davidflanagan already exists and is not an object");

if (com.davidflanagan.Class)
    throw new Error("com.davidflanagan.Class already exists");

とかやってる。 (モジュールを書くときに自分のFQDNをひっくり返した名前空間を使う、というのはJava特有のルールである)

そして、Javaはステートメントターミネータとしてセミコロンを必須とするので、セミコロンを省略する習慣がない。

だが、セミコロンを省略できる場合、セミコロンをつけたとして、

  • 改行が文ターミネータとして認識される位置で改行されている場合、セミコロンをつけていても意図しないところで文が終わる
  • 行末にセミコロンをつけて問題が発生しない場合、そのセミコロンがなくてもその改行は文ターミネータとして認識される

のでセミコロンをつけることには 全く意味がない。 まぁ、それでも 宗教に口を出すつもりは全くないが。

クロージャ

クロージャというのは非常に重要で、クロージャのない言語だと「どうしてもスマートにできないこと」が生じてくる。

言語としてクロージャがない、といえばCが有名だが、BashやZshなどもクロージャがない。 プログラミング言語として強力なZshだが、この「クロージャがない」ことが「Zshで書くのは厳しい」という限界になる。

クロージャはそのクロージャが定義されたときにクロージャのスコープ内にある変数を、スコープの外から呼び出しても参照できるものである。つまり、スコープを閉じ込めている。 例えば

function yourname() {
  var name = "Mitsuha"
  var callYourName = function() { console.log("Hello, " + name) }
  return callYourName
}

calling = yourname()
calling()

というコードでは、yourname()によってyourname()からcallYourNameという関数がreturnされる。 だから、それを変数としてキャプチャしているため、callYourNameメソッドは呼び出すことができるが、yourname()内で定義されているname変数は関数外からはアクセスできない。

だが、callYourNameがクロージャとしてname変数を参照しているため、このメソッドからname変数を呼び出すことができる。

簡単に説明しているし、理解していない人もこの説明を読むと理解できた気になれるものなのだが、実際に使いこなすにはクロージャを正しく理解している必要がある。 JavaScriptが強力なのもクロージャがあるからであり、クロージャを使いこなすことは初心者を脱する上で不可欠だ。

さらに言うと、JavaScriptでは文化的に非常によく匿名関数を使うため、

calling = (function {
  var name = "Mitsuha"
  var callYourName = function() { console.log("Hello, " + name) }
  return callYourName
})()
calling()

みたいなことを普通にやるのだが、Rubyなんかではあまり匿名関数を多用しないため、同じことはできるのだが、

calling = (->() {
  name = "Mitsuha"
  ->() { printf("Hello, %s\n", name) }
}).call
calling.call

まずこんなことはやらない(大抵、もっと良い方法がある)。

JavaScriptで上記手法が流行った背景には、変数スコープの不自由が背景にあり、今はletがあるので、 letを使うようなモダンな記述が許されるケースにおいてはあまりこのようなことをやる必要もなくなった。

Rubyのブロック

Rubyにはブロックというものがある。 例えば次のようなことができる。

File.open("foo", "w") do |f|
  f.puts "Hello, John."
end

ものすごく便利で、ものすごく使うのだが、要はこれは高階関数であり、もしブロックがなければ

File.open("foo", "w", ->(f) {
  f.puts "Hello, John."
})

と書くべきものである。要は、関数に対して匿名関数をひとつ渡すことができるのだ。 しかも、Rubyの匿名関数はクロージャであるから、より使い勝手が良い。

JavaScriptでもPerlでも、関数を渡すような関数はカッコの中にさらに関数が入る。 このような関数は長くなりがちであるから、一連の呼び出しの中で一部分がすごく長くなる。例えばJavaScriptのEventTarget.addEventListener()

target.addEventListener(type, listener[, useCapture]);

であり、listenerが関数なのだが、結局その後に閉じカッコがくるだけでなく、省略可能なuseCapture値も関数の後に来る。これが大変にみづらい。

一方、Rubyのブロックは呼び出しの最後につくため、非常に明瞭である。 JavaScriptやPerlでは、呼び出しに関数を伴うとコードが読みづらくなるため、関数を引数に取る関数は必要がなければなるべくかかないようにする傾向があるが、逆にRubyはブロックを取ることでわかりやすく書くのが「Rubyらしいコード」である。

だから、Rubyだけで学習している人はこのような書き方が普通であるように思えてしまうのだが、 「1つだけ関数を取る高階関数において唯一の関数を特別扱いする」という機能は非常に珍しい。 だから、Rubyだけで学習していると、「ブロック = 匿名関数」という感覚が身につかず、高階関数の扱いが下手、という事態が生じたりする。

プロトタイプベースオブジェクト指向

JavaScriptはプロトタイプベースオブジェクト指向言語であり、これはDelegateな考え方である。

だが、クラスベースのオブジェクト指向プログラミング言語を下地としてJavaScriptを学ぶと、その美点が理解できない。 要は、JavaScriptにクラスを求める。

JavaScriptがプロトタイプベースオブジェクト指向言語であることには強い意味、そして意義がある。 結局圧力に負けて(あと、JavaScriptの内部にまでプロトタイプベースオブジェクト指向言語の考え方を理解できてない人が増えて)クラスを実装する方向だが、これはもう、「後部座席をつけろという声がとても強かったからF1カーに後部座席をつけました」みたいな話である。 台無しだ。

プロトタイプベースオブジェクト指向の良いところは、言語仕様・言語実装ともにコンパクトにできることだ。 v8などで高速に実行できる処理系があるのだから別にコンパクトにできなくったってクラスがあったほうがいいじゃないか、と思うかもしれないが、そもそもJavaScriptは組み込み用の言語であり、 アプリケーションで機能としてJavaScriptをサポートするために小さなJavaScriptエンジンを書く可能性がある ということを忘れてはならない。 言語仕様が小さければ組み込み言語としてエンジンを書くことは十分現実的だが、複雑化するとそうはいかない。

JavaScriptはそもそも、非常にコンパクトな仕様であり、処理系を書くのが簡単にも関わらず、使い勝手がよく高度なことも書きやすい、というのがその美点であり、存在意義である。 歴史的経緯も踏まえて高速化せざるをえない、配列など複雑な要素を増やさざるをえないのは仕方ないとしても、それは元よりJavaScriptでなくてもよかった、という話になる。

ということは、様々な言語があり、それぞれ良いところ、あるいは目指すところが違う、ということを理解していなければ分からないことだ。 早期に特定の言語に傾倒するとその言語に対する帰属意識が働き、差異を優劣であるかのように感じてしまう。 だから、JavaScriptのような特殊な意義を持つ言語に対して一般的な言語の機能を要求するようなことをしてしまう。

「言語らしさ」と明瞭さ

プログラミング言語にはその言語らしい表現というものが存在する場合が多い。

例えばPerlでは正規表現を積極的に使うこと、行指向テキストとして処理すること、そしてなるべく(省略して)簡潔に書くことが好まれる。また、型を明示することは好まれず、コンテキストによって型を明らかにすることが好まれる。

Rubyではブロックを使ったコードは「Rubyらしさ」の特徴的な例であるし、ダックタイピング指向の強さも特徴的だ。書くべきことを書くのもそうだし、ユーザーコードとしては全体的に破壊的メソッドを使うことが多く、あまりメソッドに値を渡すことが多くないのも特徴的だと言えるのではないだろうか。また、本質的に不必要なことは書かないという傾向も強い。

JavaScriptはイベントコールバック型のプログラミングが特徴的だ。また、オープン構造体を利用したメモ的なプロパティをもたせることや、セッターへの代入が単なる値の代入でなく全体の動作へ影響するような挙動も特徴的だと言える。 さらに、直接にJavaScriptの特徴ではないが、Javaからの文化の持ち込みとして冗長な宣言がされることもあるし、またvar変数が関数全体に有効なスコープの変数として宣言位置に関わらず有効にコンパイルされることから、関数先頭での宣言がなされることも特徴的だ。

Goでは連想配列が静的で非常に使いづらいため、連想配列をあまり積極活用しないこと、インターフェイスという独特の概念があること、並列処理を初歩的な段階から使うことが特徴的だ。

Pythonは全体を通して多様なコードを書かない傾向が強い。そして、「きっちり書くところ」と「暗にわかっているからかかないところ」がくっきり分かれており、簡潔なコードや短いコードよりも平易なコードを書くことが好まれるようだ。 「面倒だからここは省略しよう」という考え方はあまりしない。

プログラミング言語にはそれぞれ「らしさ」があり、「中庸」というものはない。 また、記法に関しても、「多くの言語が採用する記法」と「その言語独特の珍しい記法」があったりする。 Perlの行読みが<>であるところとか、Rubyのブロック変数のdo |i|とか、初見では「えっっ」となってしまうようなものだろう。

だが、そんな記法に関しても一般的な記法が個々にあるとしても、(例えば、メソッド呼び出しは.が一番ポピュラーで次いで->:で呼び出すのはちょっとめずらしい)「最も普通な文法を持つ言語」などというものはないし、「最も普通な考え方」があるわけでもない。

だから、「その言語にとって “らしい” 書き方」をするのが良いのだが、何が一般的なことで何がらしいことなのか理解できず、If you want X, you know where to find it.と言われるような振る舞いをしてしまいがちなのだ。

また、セミコロンのいるいらない、カッコのいるいらない、演算子の優先順位、存在する演算子など、単一のプログラミング言語において「基本的な当たり前の事項」というのは、それしか知らない者にとってはすごく「常識」に思えてしまうのだ。実際はその常識はその言語の中に閉じ込められているのだが、特定の言語のみへの傾倒がすぎるとその常識から抜けることが難しくなる。

用語と構造と関数

JavaScriptのオブジェクトは任意のプロパティを追加することができる。

var img = document.getElementById("MainImage")
img.imgMode = "thumb"

これは、JavaScriptのオブジェクトはいわゆるオープン構造体をしているためであり、このためにJavaScriptはsetter/getterを必要としない。 これはPythonも同様である。

だが、これは一般的な話ではない。 例えば次のRubyコードは有効ではない。

img = Image.new("MainImage")
img.img_mode = :thumb      # このsetterが存在しないならエラーになる

そして、さらっと流したけれども、オブジェクトの名前空間に属するものを「プロパティ」と呼ぶ言語はかなり少ない。 JavaScriptの独特な用法だと言っていい。

さらにいえば、JavaScriptは前提として、オブジェクトが連想配列のようなものになっている。 「連想配列をオブジェクトの基とするのが基本」というのはPerlとかもそうだし、完全なオブジェクト指向ではないが、Luaもそれに近い仕組みである。 そして、JavaScriptは「連想配列」というものがなく、連想配列に相当するものが基本オブジェクトになっており、JavaScriptのいかなるオブジェクトも連想配列様の性質を持っている。だから、連想配列に対する操作(例えばkeysメソッド)がObjectにあり、全てのオブジェクトが連想配列を元にしている以上、連想配列オブジェクトは存在しない。

もちろん、これが全てにおいて良い設計であるとはいい難い。 最も明確な問題となるのは、配列が連想配列になってしまうことだ。 一般的に連想配列はHashとなっており、Hashは高速だが、順序を前提とする場合通常の配列のほうがずっと速く、効率もいい。 にも関わらず連想配列を基本とし、配列が存在しない2のは、「連想配列を基本とすれば、連想配列を実装するだけで幅広いオブジェクトの表現が可能になるから」であり、「小さな実装」のためである。

さて、JavaScriptは連想配列を基とし、オブジェクトの名前空間に属するものは連想配列の要素である。 つまりは、

var x = {a: "John"}

console.log(x.a)

ということが可能である。

だが、全てのオブジェクトが連想配列を基とするというのは結構特殊な話だし、そのような仕様になっている小さな言語に関しても基本型は別に持っていることが多い。 (JavaScriptも非オブジェクトの基本型がなにもないわけではない)

JavaScriptのオブジェクトがオープン構造体であることも、「プロパティ」という用語も、このあたりの事情に由来しており、つまりは「一般的な他の言語にはない、JavaScript特有の事情から生まれた仕様と用語」なのだが、当然ながらJavaScriptしか知らない人からすれば、これは普通のことのように思えてしまう。

例外

例外、要はエラーであり、モダンな言語には不可欠な要素のひとつだ。

だが、「例外」をどう定義するか、どう利用するかということは、大きな齟齬がある。 簡単に言うと、

  • 例外機構がない (Cなど)
  • 標準では利用しない。ユーザーが大域脱出を必要とする場合に利用できる手段である (Perl, JavaScriptなど)
  • 明らかに問題が生じているケース、失敗が通常予期されないケースにおいて例外を投げる (Rubyなど)
  • 失敗したら例外を投げる (Pythonなど)
  • 失敗したら例外を投げ、なおかつ例外の漏出は許されない (Javaなど)

まぁ、最後のはさすがに問題があると思うけれども。 例外は予期できないからこそ例外なのであり、予期できない例外において最も適切なのはプログラムを終了することであるのは明らかなので。

さて、これはカジュアルに例外を投げる言語で育つと例外を使いたがるし、例外を捕捉しないのは気持ち悪いと感じるようになる。 一方、大域脱出手段でしかない言語で育つと滅多なことでは例外を使わない。

本質的に例外は以下に集約される。

  • 例外は大域脱出手段であり、そこに関知するところまでコールスタックを遡上する
  • もしもルートコールスタックまで捕捉されない場合、プログラムは実行を終了するべきである

こんなに単純なことだが、これは例外というものについて幅広い言語で学ぶか、「例外そのもの」について学ばないと出てこない。

ちなみに、Rubyは純粋な大域脱出手段として、例外とは別にthrow/catchというものがある。

もっと悪い話

これを聞いて「うんうん、そうだな、なるほど」と思った人は(最終的に肯定的であるか否定的であるかによらず)良いのだ。 だってそのような人は、「いかにしてプログラミングを習得するか」について考えているのだから。

最悪なのは、プログラミングを単なる金づると考えている人である。

そんなヤツは少ないだろう、と思うかもしれないが、「プログラミングという行為が職能であると考えている」と言ったらどうだろう?

JavaScriptを習得しようといったら、「まずJQueryをロードする」とか「これからはVue.jsが流行りなのでVue.jsを勉強しましょう」とか言い出す者や、Rubyを習得しようといったら、「じゃあまずはRuby on Railsのセットアップから」とか言い出す者など枚挙に暇がないのではないか。

「プログラミングの習得」というのはあくまでもプログラミングの習得である。 プログラミングを知る上で言語は重要ではない。どんな言語を使おうが、プログラミングはプログラミングである。 Dでも、Nimでも、OCamlでも、Eiffelでも、どんな言語を習得してもいいし、それによってプログラムが書けるのなら、それはプログラミングなのだ。

ところが、なぜかやたらに職能として語りたがる。

これをすれば仕事になる、これをすれば稼げる、という視点に基づく学習行動は、職業訓練であってプログラミング学習ではない。 プログラミング学習とは当然ながらプログラミングを習得することが目的である。 一方、職業訓練というのは職能として機能するのであればプログラミングを習得する必要はない。

ここまで例示した内容というのは、確かに「教える側に当然備わっているべき知識」であるのだが、内容的には至って初歩的なもので、上記内容についてはプログラミング歴1年にも満たずその解説や実演までできても何ら驚くべきことではなく、少なくともプログラミングを習得している者にとって(大げさに言っても中級者以上を自称する者にとって)備わっていなければおかしいようなものだ。

だが、職能としてはこれらの知識を必要とされることはあまりない。 総合力を求められることを前提とするならば、プログラミングに対する広汎な知識を習得することもあるかもしれないが、実務上これらの知識が備わっていることを直接に求められることはまずない。

これは、「職能として行使するから備わっていない」ということではない。 プログラミングを習得するということがたとえ職能のためであれ、それを実現する方法としてプログラミングそのものを習得することが有益だと判断すればプログラミングを習得するだろう。

だが、プログラミングを習得するのは、丸覚えした知識を切り貼りすることではないし、流行りがどうとかいうことでもない。 プログラミングそのものはある程度以上に普遍的なものであり、それがどのような言語で習得するかによらず習得する内容は変わらない。困難性に違いはあれど、十全な機能を持つ限りどのようなプログラミング言語であれ実現可能なことは変わらない。また、同様にフレームワークを使わなければできないことも存在しない。

職業訓練が悪いという話ではなく、職業訓練とプログラミングの習得は別物であり、職業訓練をプログラミング学習と言うのは事実と異なるという話だ。

そして特定の言語にこだわるのは、特定の成果物を想定することを前提とする可能性が高く、それはプログラミングの習得ではなく、目的達成の方法論の話であると考えられる。 そして、大概にしてそれは「こうすれば仕事できまっせ」であろう。 これがフレームワーク前提ともなればなおさらである。

Enjoy

本質的に、プログラミングとは知的好奇心を満たす創造行為であり、楽しいものである。

実際、多くの人が楽しみ、様々なものを創造している。

プログラミングによって達成できるもの(プログラミングのみによって達成できるという意味ではなく)は枚挙に暇がなく、プログラミングを行う動機も、それを何のために行使するかも自由であり、また多彩である。

もちろん、プログラミング技能を持つ者の何割かは、その技能を職能として行使している。 同時に、プログラミング技能を職能として行使する者の何割かは、プログラミング技能を職能以外でも行使している。

ウェブで動作させるために。アプリケーションを作るために。研究のために。パソコンを便利に使うために。 電子工作のために。機械制御のために。ガジェットを改造するために。 あるいは、知的ゲームとして楽しむために。

様々な目的で、様々な理由で、プログラミングという技能は便利に行使できる。

今あなたが見ているこのChienomiは、私が開発したPureBuilder Simplyというプログラムによって構築されている。 それは、ウェブを見てもらうためでもある。自分の気に入らないアプリケーションを使わないためでもある。そして、自分の作業やメンテナンスの時間を削減するためでもある。

そして、PureBuilder Simplyにもまた、そうして誰かが力を注いだ、Pandocというソフトウェアが使われている。 そして、志を持って作られているRubyというソフトウェアが私のプログラムの実現を支えている。

プログラミングは、「コンピュータを使う」という行為に自由をもたらすものだ。 プログラミングができるならば、あなたのコンピュータはなんにでもなれる。少なくともその可能性を生み出すのだ。

プログラミングを習得することは、必ずしも目的を必要としない。 プログラミングを習得することでできることが増える。そうして増えたことの中から「したいこと」を拾い上げればいい。 あるいは、何らかの目的のためにプログラミングを習得してもいい。プログラミングを深く理解することは、少なくとも損失とはならないだろう。

だから、私達はこう結ぶのだ。

Happy hacking.

余談

実は、この記事というか、私の教え方自体に不足があって、これだと

  • 論理型言語 (e.g. Prolog)
  • ゴール指向言語 (e.g. Icon)
  • 関数型言語 (e.g. Haskell)

なんかに通用する知識や概念になっていないし、普遍的な知識を標榜しながら片手落ちだという指摘があることだろう(というか、実際あった)。

大変申し訳ないのだけれど、それらのことをなかったことにしているかのようなこの記事であるのは、 私がPrologもIconもHaskellも到底実用レベルでは使えない ことが理由であり、 概念的に差異が大きすぎて理解に至れていない (私がRubyを使えるのに時間がかかったという話とだいたい同根) ということなので、 プログラミングパラダイムが全く異なる言語に関しては、大変申し訳ないけれども除外させていただきたい。

また、イテレータについて内部イテレータについてしか説明していないことについても片手落ちを感じられるかもしれないが、 それは単純にこの記事の意図としてそれを解説することを目的としていないのでご容赦いただきたい。

また、この記事は記事として収めるために(あくまで、単一言語によるところでは理解として不足があることを示すために) 十分な説明となっていない箇所(言語によってはこの説明では適切とは言えないような箇所や、言及に不足がある箇所)もあるが、 既に約800行に到達している記事であるので、それも平にご容赦いただきたい。


  1. Rubyの場合、入出力は基本的にIOクラス(の子孫クラス)のオブジェクトになるのだが、IO#to_iがファイルディスクリプタ番号を返すため、「アクセス方法の明示されたIO」「抽象化されたIO(ファイルディスクリプタ)」「標準入出力とファイルディスクリプタ番号」の関係が理解しやすい。なお、/dev/fd//proc/$$/fd/があるとより理解しやすいため、これに関してはLinuxが良い。↩︎

  2. JavaScriptの配列は途中から追加されたものだし、配列といっても順序を持つコレクションであって配列としてアクセスできるようになったのは高速な実装が出てから、それも無理やりに「これは配列として使われる」という判断をした上で配列として扱ったからだ。↩︎