(学びの方法)
言語の共通部分と独自部分を学ぶ。
制作者の意図から設計思想を学ぶ。
言語を自分で作ってみる。
第1章
(比較して学ぶ)
一つの言語をしっかりと学んでから、他の言語を学ぶ方がいいというように言われるが、
他の言語と比較することで、言語間で普遍的な部分と、言語独自の部分を知ることができる。
言語は時代とともに流行や需要が変わってくるので、一つの言語に固執するより、
その時々に必要な言語を使えるようにしておいた方がいいし、一つの言語では不便だ。
普遍的な部分を学んでおけば、他の言語を学ぶ時には、独自部分だけを学べば良くなり、
学習効率が上がる。
(なぜ=解決したかった問題を知る)
何かしらの問題があって、それを解決したくて、その言語ができたという、
なぜのその言語ができたのかということを明らかにすることで、
普遍的な知識を得ることができる。
そして、その設計思想が、その言語の書き方=構文を作っているのだから、
なんでこの書き方をするのかという問題に納得がいく。
構文にしても、問題を解決するためにあるので、
どんな問題を解決するために、なぜ必要なのかを知ることで、
学習効率も、理解度も上がる。
第2章
(プログラミングの設計思想)
プログラミングは人間が楽をするために作られた。
今のプログラミングはすごく複雑に見えるが、歴史の積み上げの上にこうなっている。
歴史の中で解決したい問題が出てきて、その度に解決策が追加されてきた積み上げが今の言語になっている。
歴史の流れから紐解くことで、言語の理解が早くなる。
(言語の作られた目的)
何を目的に作られた言語なのかによって、言語の特注がわかる。=>何語を使えばいいかわかる。
人がケーブルを繋ぐところ→紙→いや、機械語は難しいから、人が読めるようにしよう=コンパイラ言語
「私の成果の大部分は怠惰から来ている。」by FORTRANの設計者
Perl=データ取得&レポート作成言語 つまり、レポート作成を楽にしようと考えられた言語。
C+ → Cより遅くならない、処理の速度を目指して作られた言語。
Schema → 言語把握が簡単になることが目的で、仕様書が超薄い
Python → 他人が書いたコードを読むのが楽になることが目的。だから、早くも、仕様書が薄くもない。
PHP → WEBサービスを書くことが楽になる。
Haskell、OCaml → 言語処理が楽になる。
各言語の設計目的によって、設計思想が違うから、得意な分野が違ってきて、用途も異なるということ。
だからどの言語がいいとか、悪いとかじゃなくて、どんな問題を解決したいのかによって、使う言語は違う。
楽とか、生産性が高いとか、問題解決能力が高いってのがキーワーど。
何を使うかではなく、それで何ができるかの方に、私は興味があります。
第3章
(文法とは)
文法とは設計者の意図によって決められた、言語上のルール、定義。
例えば、pythonでは=は代入の構文だが、Cでは=は比較演算子として定義されている。
構文との違いは、、、if文の書き方は構文と言えるが、定義とは言わない。そういうこと。
(コラム:構文の種類)
(ifとかforとかは、人間のやりたいことを機械にやらせるための構文って側面が強いが、
それ以外にも、機械が機械にやらせるための、機械間でのやり取りのための仕組みもある。
ORMとかコンパイルとかって考え方は、機械から機械への命令になっている。
人間から機械への命令はわかりやすいが、機械から機械への命令は、機械のことを理解してないとわかりにくい。)
(forthとスタック)
可読性を追求した、文法のほとんどない言語がForth。
Forth の計算にはスタック(先入れ後出し)が使用されていて(スタックベース言語)、
演算子がスタックに入った時点で、後から入ってきた二つの数値を演算する仕組みになっている。
Java, python,rubyも内部ではforthのような仕組みに変換されている。
(LISPと構文木)
Abstract syntax tree = 抽象構文木
Forthではひとかたまりの計算がわかりにくかったから、わかりやすくした言語、
LISPでの処理は抽象構文木で表現できる。
Pythonの内部での処理も抽象構文木で表現できる。
(中置記法とFortran)
Fortranは人に優しい言語だから、演算子も使い慣れた中間に置くことにした。
そうした文法を機械語に翻訳する機械を構文解析器(パーサ)という。
各言語がそれぞれの目的に合わせて文法を考えたが、最近まで残っているのは、
人が使いやすい、fortranの設計思想を受け継いだ言語が多い。
また、機能を追加する際は、文法の被りがあるため、特殊な表現をすることも仕方ない。
第4章
(構造化プログラミング)
コンパイル = プログラミング言語(高水準言語)をコンパイルして、アセンブリ言語にし、
アセンブルして、機械語にして、リンクして、つなぎ合わせて一つの実行可能なファイルにする。
goto → If → while → for → foreach
Ifのないアセンブリ言語では、毎回Xと何かを比較して、〇〇でないのなら、××へジャンプを繰り返す。
非常に面倒くさい書き方をする。
C言語にはgotoという、jumpみたいな書き方もできるが、確実にif文の方が読みやすい。
Whileも同じように、gotoとloopとbreakで表現できるけど、それなら、同じ機能ができるなら、
Whileを使ったほうが、書きやすいし、読みやすい。
パッケージやフレームワークも、同じこと。ベタガキでもできるけど、面倒くさいから、
ひとまとまりにして、呼び出せば使えるようにしてくれているだけのこと。
新しい=便利になっているということ。
古いものを使いまわして、煩雑にするか、楽するために新しいものを習得するかということ。
For文も、whileで表現できるけど、汚いし、可読性が低いから、for文を作って、可読性をあげた。
For文よりもforeachの方が、配列とかの繰り返しには向いているから、作った。pythonのforはforeach。
(コラム:アセンブラと機械語)
アセンブリ言語の単語をニーモニックという。C言語をコンパイルすると、の字数が増えるが、アセンブリ言語をアセンブラしても増えない。アセンブルと機械語は1対1。だから、アセンブリ言語をかけることは、マシン語を使えることに等しい。機械語はCPUに命令を送ることになる。どんどんハードウエアに近ずいていく。
アセンブリ言語は、CPU・I/O・メモリーに命令を与える。
CPU:PC(program counterプログラムの流れを制御)、SP(stack pointer メモリ内の領域を利用する)、FR(flag register 演算結果を記録)、GR(general register 汎用レジスタで演算する)0〜7、合計11個のレジスタで演算と記録を行う。
アセンブリの命令から、OSのサブルーチンを呼び出す(スーパーバイザ・コール)して、I/Oに命令する。
スーパーバイザはOSのこと。
コンピュータにできることは、データの入出力と、演算、流れの制御の3つだけ。
アセンブラの文法は、命令語+目的語だけ。超簡単。
SPで命令文が格納されている番地を示し、FRでプラスとマイナスと0を区別し、PCで読み込む行を示していく
それらと、数学の基本選択法を組み合わせて、ソート関数ができる。線形探索と組み合わせると、サーチができる。
第5章
(関数)
システムが大きくなると、可読性を高めるために、コードを分散させたり、ひとまとまりにしておきたくなる。
また、同じような機能はまとめて、再利用を可能にした方が使いやすいし、読みやすい。
最初はサブルーチン(戻り値を持たない関数)だった。
jumpでサブルーチンへ行くことはできるが、戻ってこないと、行きっぱなしになってしまう。
戻るために、毎回サブの戻り先をメインルーチンの次の処理が実行されるアドレスに変えていた。
これは面倒なので、戻り先を記憶する専用メモリを作ったが、これだと途中で他のサブを実行した時に、
戻り先が変わってします。
そこで、プログラムの1文1文はアドレスに記憶されていることを使って、
スタックにアドレスの番号を持たせる領域を作り、指定アドレスにgotoさせることでサブルーチンを行わせ、
持たせたアドレスを一つ進ませることで、次に読み込む文を指定することにした。
(再帰関数)
HTMLのように、何重にも入れ子になっている構造では、forで回していたのでは、なかなか最下層の値にたどり着けない。
そこで、それを解決する構造を持った関数を、関数の中で使うことで、何重にもなった入れ子構造でも処理ができるようになっている。
入れ子構造に対して、forを使う時に、使えるってこと。
第6章
(例外処理)
ファイルの容量がいっぱいで書き込みできないとかっていうエラーが発生する場合、それを知らせる仕組みが必要。
C言語にはない処理だから、C言語ではファイルがぶっ壊れる可能性があるってこと。
そこで、Cでは、返値を判断する処理を書かないといけないけど、これを忘れたり、エラーが発生しないと思って書かなかったりすることがあって、あとで問題が発生しても見つけられないことがある。また、失敗した時の処理は同じ処理の場合、一つにまとめてしまった方が、可読性が上がる。Cではgotoで失敗が起きた時の処理をまとめている。
初期の言語には例外処理がなかったり、例外が2つしかなかった。
PL/Iでは、例外処理のための機能が追加され、新しく例外を定義できることと、例外を意図的に発生させる機能が追加された。
CLUで失敗しそうな処理を囲むという発想が実現化され、C++でtry〜catchが搭載された。
例外を発生させる命令にthrowとつけたのは、signalがすでに使われていたから。
各言語について言葉が違うのは、言語の設計思想やすでに使われていたという事情とかもあったりする。
SEH(構造化例外処理)でfinalyが登場する。これは対になる操作を確実に実行するため。開いたら、閉じるということ。
C++にはfinalyはないため、関数の初期化と後片付け処理でそれを行なっている。
例外の定義は言語によって違う。引数の不足や、配列の範囲外指定が必ずしも例外を発生させるわけではない。
よって、プログラマは言語の例外発生だけに頼るのではなく、自力で例外となる可能性を潰していかなければならない。
また、自作の関数であれば、プログラム的には正解でも、動作的にアウトの場合があり、
そのような場合は自作の例外を作成し、例外処理を行う必要がある。プログラミング言語は万全ではないのである。
Javaの例外は3通りあって、error(どうしようもない例外、メモリー不足とか)、exception(これが検査例外、ちゃんと書いていても起きる例外。try~catchが必要。)、runtimeexception(書き間違いとかで起きる例外、気をつければいいだけ、配列外のインデックス指定とか)
第7章
(名前)
名前をつける前は、番地の指定によって区別していた。DNSと同じように、数字より名前の方がわかりやすい。
PCの中で番地と名前を対応させる表が存在している。しかし、当初のPCはシステム全体の番地を使っていたので、
要するに名前領域がなかったので、名前の衝突(同じ変数を使う)が起こった。
特に規模が大きくなればなるほど、名前の衝突が起きる。
その問題を解決するために、一旦変数を脇にどけておいて、処理の終わりで元に戻す、local変数ができる。
これが動的スコープ。
しかし、動的スコープは呼び出し先の関数にまで影響を及ぼすので、静的スコープとして、
関数の呼び出しごとに、名前と番地の対応表を持つようにした。
グローバル → ローカル → 静的スコープ と影響範囲を狭くすることができるようになった。
この変化は時代とともに付加されていった機能で、perlではバージョンアップのたびに機能が追加されていった。
Pythonは新しいため、最初からグローバル、ファイル、ローカル変数という扱いになっている。
ただし、人が思っている挙動と違う動きをする場合は、pythonのように静的から動的に変更されることがある。
言語仕様は言語はもちろん、バージョンによっても変わってくるので、細かい仕様ではなく、概念を理解する必要がある。
第8章
(型)
数値は10進法に慣れてしまっているが、2進法や8進法、16進法も表現できる。
PCは小数点を認識できないので、仮数x基数^x指数で表現している。指数を固定する(固定小数点数)では表現の範囲が決まってしまうので、指数をデータの範囲内で表現する浮動小数点数を使用している。
PCの中では整数と浮動小数点数では、ビット列の配置が全く違う。これを区別するために型が使われている。
つまり、使うビット数を節約するために、無駄な容量をしようしないためと、計算間違いをしないために型を指定する。
また、浮動小数点数と整数を計算すると、自動的に浮動週数点数に変換して、計算される。
言語によって、その挙動はまちまちなので、注意が必要。
プログラミング言語は、コンピュータっていうハードに対しての命令を出すものだから、
その成り立ちには、絶対的にハードの制約がついてくる。
それプラス、人間にいかに使いやすいようにするかという設計思想が入っている。
ユーザー定義のコンストラクタ(構造体)もある。pythonの辞書みたいなもの。
入れられるものが決まっている変数を型と呼んでいる。
定義時に型を宣言するのが、静的型付け言語。コンパイル時に型が決まるのが、動的型付け言語。
第9章
(コンテナ=配列とか辞書とか、物を入れる物)
配列と連結リストの違いは、メモリへの保存方法で、配列はメモリの連番を使って記憶されているが、連結リストは要素と次の要素の格納アドレスがセットで入っている。配列は要素の取り出しは簡単だが、途中に要素を挿入するのには、仕事量が多くなる。逆に、連結リストは挿入は簡単だが、取り出しは不得意。
配列、ハッシュ、連想配列、木、それぞれ必要とする容量、検索の速さ、に違いが出てくる。
ユーザーが求めていることにベストの配列を選択するのが重要。
(文字)
モールス信号から、文字の符号化は始まった。アメリカ標準やIBM独自の文字コードなど至る所で文字コードが生まれた。
文字コードによって、符号化の方法が違うため、コメントの挿入時などに思いもよらない文字変換されることになる。
だから、プログラムを書く際に、文字コードの宣言が必要になる。
各国では独自の文字コードと符号化ができていたので、ユニコードによって統一された。
UTF-8はユニコードという文字セットの符号化の方式。
C言語は1文字8バイトで、asciiとかebcdecを使ってる、pythonはそれとunicodeの両方を使う。
第10章
(並行処理)
同時に複数の処理を実行する際に、同じアドレスを共有してしまい、処理がダブってしまう。
プリエンティブ(強制的に並行処理する)だと、処理の途中で他の処理が行われる。
それを避けるために、共有しないとか、共有するけど変更できない(イミュータブル)とか。
また、協調的に処理を実行する(処理の区切りで割り込ませる)方法として、ジェネレータがある。
割り込み禁止の札をつける方法として、ロックやミューテックスがある。
お互いにロックをかけて、割り込みOKをお互いに待っている状態をデッドロックという。
トランザクションによる解決も考えらレル。
第11章
(オブジェクト指向)
全てのものをオブジェクトとして現実世界では扱う、そして、
そのオブジェクトを模したものモデルを作ってPC場でオブジェクトを扱うようにしていた。
モジュールとパッケージは言語によって言い方の違う、同じもの。
カウンターというモジュールを作ることは、鳥を数えるかうんたーを作るのと同じ。クラスは設計書で、インスタンス化によって、カウンターがものとなり、実際に動き出すと考えると納得がいく。
クラス、モジュール、パッケージ、クロージャといろんな呼び方がある。
第12章
(継承)
あるクラスに統一的な属性を持たせて、親クラスとし、それを元にして、違う子クラスを作ること。
特殊化としての使い方、一般化としての使い方、差分としての使い方など、使い方は3種類あるが、
継承は影響範囲が広くなるので、実用的な使い方をしたほうがいい。
多重継承により、複数のクラスを継承することができる反面、名前の重複が起きる可能性が出てきた。
そこで、委譲という形でクラスの関数を利用することを推奨している。
多重継承の名前重複問題には様々なmix-inやトレイとといった考え方で解決がはからレテいる。
モジュールとして、読み込む最小限の単位を作成する方法を模索している。