はじめに
世の中には、多数のプログラミング言語が存在しますが、ほとんどのプログラム言語において必須の言語機能として用意されているのが「変数」と、言語によって多少名称や扱い方が異なりますが「関数(サブルーチン、クラスにおけるメソッド等)」があります。これら変数や関数では、プログラマーがデータを扱うとき、大まかに2つのパターンで扱い方を分けることが出来ます。それが、今回の話題として扱う「動的型付け」と「静的型付け」です。
単純な考え方とし、「プログラマー」が「コード」を書く場合に、予めプログラマー自身が変数や関数の引数、返り値に対してどのように「データ」を扱うのかをプログラマー自らが指定する、つまり「型」を決めて使う方法を「静的型付け」と言い、プログラマーによって書かれたコードを元に、変数や関数の引数、返り値の型を、コンパイラーやインタープリターが実行時にデータの型を解釈・決定して扱う方法を「動的型付け」と言います。
今回この議題を取り上げたのは、開発時にデザイナーとのやり取りで、なぜ動的型付けと静的型付けというものがあるのか?動的型付けだけあればいいのではないか?という議論を行ったのが要因で、その場で上手な説明が出来なかったので、改めて整理をしてみることにしました。
そもそも型とは何か
データというのは抽象的な物であり、プログラムにおけるコード上では、論理値、数値、文字列、配列、オブジェクト等であったとしても、コンピューター上(物理メモリ上)では、ただの数値の羅列でしかありません。また、コンピューターにおいて、データは有限数であり、必ず上限があります。それら、抽象的な数値の羅列や数値の有限範囲において、コンピューター(CPU)が理解できるネイティブな命令に置き換えるとき、曖昧な数値の羅列であるデータに意味を持たせるのが「型」定義の意義になります。
つまり、抽象的なものであるデータに対して、プログラマーがその型の定義を行い機械語への変換時の指示をインタープリターやコンパイラーに対して予め指定するか、プログラマーが書いたコードを元に、インタープリターやコンパイラーが実行時に型の種類を解釈し、実行するかどうかの違いだけです。
よくある勘違い
静的型付けと動的型付けを説明するときによくある勘違いが、コンパイラー型は静的型付けの言語である。や、インタープリターの言語は動的型付けの言語である。と説明される方が時々見受けられますが、これは間違いです。静的型付けである言語であっても、RTTI(RunTimeTypeInformation:実行時型情報)と呼ばれる手法を用いて動的型を扱えるようにしている言語があります。機械語(中間言語)にコンパイルできる代表格の言語の Delphi(ObjectPascal)やC#言語、C++言語において、動的な値を扱えるための型が用意されています。
また、インタープリターを主とした言語でも最近は型情報を予め定義できるような言語が増えています。主に部分的な箇所の取り扱いが多いです(PHPのように変数には対応せず、引数や返り値、クラスのプロパティなどに対応するなど)。また、言語そのものの欠点を補うために、基本的な言語構造は同じでも別言語として用意された言語というのもあります(Javascriptに対するTypeScriptのような関係です)。
このように、インタープリターで実行する形態であるやコンパイルしてネイティブに変換する(AOTコンパイル、中間言語方式コンパイル限らず)する形態であっても、動的型付け・静的型付けのどちらもが扱えるパターンが有りえるということです。
コンパイルとインタプリタ
まず、前提知識として「型」を押さえる前にプログラムの実行形態について簡単に説明します。
コンパイラと呼ばれる実行ファイルにソースコードを読ませることで、別の言語形態(機械語に変換、別の高級言語に変換など)に変換(翻訳)を主とすることを「コンパイル(翻訳の意)」といい、インタプリタと呼ばれる実行ファイルにソースコードを読み込ませることで翻訳・実行を同時に行うことを主とする形態を「インタプリタ(通訳者の意)」といいます。
ちなみに、主にコンパイル型の言語として扱われているが、インタプリタとしても利用することが出来る場合(例えばC言語やC#言語等)もあるため、「コンパイル型言語」や「インタプリタ型言語」と「言語として分類」することは難しいので、ここでは「コンパイル」と「インタプリタ」については言語にとらわれない考え方で進めます。
「コンパイル(Compile/Compiler)」とは、主に「別の言語に翻訳・変換」することが主で、翻訳や変換までしかしません。そのため、基本的にはコンパイルしただけでは実行は出来ません。リンカ等を通してライブラリやフレームワークなど必要なコードをリンクを行い、実行ファイルを生成する(これらコンパイル・リンク等を行い、パッケージなどを生成することを「ビルド」と言う)、または別の変換した言語を再度コンパイルする・インタプリタを通すなどが必要になります。現実的な考え方は、「海外の方が作った書物を、予め現地語に翻訳しておく」というイメージになります。
「インタプリタ(Interpret/Interpriter)」は、主に「インタプリタに渡されたソースコードを翻訳・実行までさせる」ことが主になります。現実的な考え方は、「海外の方が話している内容を、その場で通訳者が現地語に翻訳して伝えている」というイメージになります。
このように、コンパイルとインタプリタは、「翻訳する」という考え方までは同じで、その後にどうするのか?という部分が異なるという点です。
インタプリタの場合、「翻訳」をしてもソースコードは残ります(翻訳者が覚えてる)。これは実行が完了する(翻訳者がその仕事を終えるまで)まで携わるためです。そのため、コードの実行時に、型の意味を考える必要があっても、ソースコード(元のコード)から辿ることが可能です。
しかし、コンパイルされた場合では「ソースコード」が翻訳されると現地語に翻訳されたコードのみが残ります(翻訳された書類しか残らない)。そして、ソースコード(元となったコード)は基本なくなります。つまり「元のコード」が存在しなくなり、もしその時に「型」の意味が不明であれば、辿ることが出来なくなり、実行させられなくなる、ということになります。
動的型付けとは
『動的に型を付ける』という読んでそのままの通り、プログラマーがプログラムを書くときに変数や関数に何が入ってくるかというのが特に決まっていないが、実行時に型の決定を行うことが出来る形態を示します。要は、プログラマーは、変数や関数へ入るデータは、たぶんこうであろうということを予測しておくだけ、または考えなくプログラミングしていたとしても、その変数や関数の引数、返り値にどんな値が入るかはコーティングに影響なく、型の管理はインタプリタやコンパイラが実行時にしてくれるという形態になります。
特にインタプリタで実行される言語は、ほぼこの形式を採用しています。型を宣言する必要がないため、小さいプログラムや型の変動が激しい環境であれば変数の値に何が入るのか、変数名の使い回しなどを考える必要なく簡単に実装できる反面、変数名を間違えてundefined/nullとして出力させていてデバッグに時間がかかったり、文字列から数値型へ、数値型から文字列へ等の変換に時間を要したり、型チェックが必要となり、型チェックのためのif分が大量に増えてしまうという欠点もあります。また、基本的に前述の理由があり動的型付けは事前にコンパイルしてネイティブで最適化されたコードを基本的には出力することが出来ません。1 これは、プログラムを実行しないと型がどんな形で、どんな大きさなのかを判別することができないため、実際にプログラムを動作させないとわからないということが起因します。
では、動的型付けの利点と、現実的に動的に型が決まるということがどういうことなのか、考えてみます。
まず、動的型付けの利点は、データの型を気にしなくても良い点。もう一つはデータそのものの大きさを気にする必要が無い点です。一つ目のデータの型とは、「はい(true)、いいえ(false)」を扱う論理値、数値、小数点、正の数、負の数、文字、文字列、オブジェクト等など、様々な型が存在します。これらを変数や関数に入れたり渡したり拾ったりするとき、基本、気にする必要が無いです。もう一つはデータそのものの大きさ。数値ならば、0~10億や10兆やら、0~-10億や-10兆、0.000001 や、「あいうえおかきくけこさし...ん」という文字の長さまで、気にしなくても大丈夫です。最大限の数値または長さは当然、有限なのでそこだけを気にすれば...。
そのことを踏まえて、現実的な動的型の考え方。
Aさん「今から、カレー作る材料を持って行くから、スペースと包丁、まな板、ボール、鍋、皿を用意しておいて!」
Bさん「わかりました」
Bさん:あれ?どれだけの量持ってくるのかな?
Bさん:うーん、量は、Aさんが来るまでわからないので、一番家にある大きいのを用意しておこう。
ということで、カレーの分量がこの時点ではわかりません。特に、スペース、まな板、ボール、鍋、皿については来ない限りどれぐらいの大きさを用意すればわからないことになります。まぁ、Aさんも良く来るのであれば、Bさんの家に置いてある器具の大きさの最大値はわかっているでしょうし、スペースもどれぐらいが限界かわかっているとすると、最大値は固定化されます。このように、指示されるデータ容量がわからなくても柔軟に対応できるのが動的型の利点です。
プログラムに置き換えても、器具とかはわかっていても量がわからない(入力されるまでわからない)というのは結構あります。そもそも、カレーの作る材料というのも、この時点で「カレーの材料」であって、シーフード?野菜カレー?普通のカレー?なのかもわからないわけで(例えば、こういうのはWebAPIなどのJSONで返される仕組みを思い浮かべるとわかりやすいかも)、この時点でどういう素材が持ち込まれるのかわからないこともあって、その当たり実行時(持ってきてから確認して遂行する)しか出来ない感じになります。この、何が入るかわからない、設計が大変だと言うときは動的型付けが凄く利点になります。
さて、現在のコンピュータは64bitが主流になってます。PHPやJavascript等を初めとした言語では、整数値、浮動小数点などは64bitで扱うことになります。メモリ使用量は8byte、最大「18,446,744,073,709,551,616」の数値の範囲で扱えます。(符号無しの場合)。動的型付けの言語では、数値を扱う場合、ほぼ8byteのメモリを使用します。
では、インタプリタは型を決めるときに、最小に抑えることは出来ないのでしょうか?でも、この「最小に抑えたいな、こういう風にして欲しいな」というのは、その考え方が既に型を指定する(静的型付け)になるので、動的型付けではできませんね。また、動的型付けの型を付けるタイミングというのは、必ずプログラムを実行している時になります。なので、既にそのデータが小さいか、大きいかを確認してからサイズを決めているのでは効率が悪すぎます。なので、値やデータを扱う場合は、大きめに確保されて実行されます。
動的型付けでのプログラミング上の欠点
動的型付けのプログラミングの一番の欠点、それは、プログラムのコードが設計書にならないという点です。
どういうことかと言いますと...
Aさんは、javascriptで加算をする関数を書きました。
加算する関数として書いてます。
function add( a, b )
{
return a + b;
}
この関数は、aとbを加算し、計算結果を返します。プログラマーの意図しているのは「整数?小数点?等の数値を入れると加算してくれる関数ですよ」という意図として作っている点です。そして、以下はすべて動作します。
1. add( 1, 5 ); // 6
2. add( 1.5, 3.2 ); // 4.7
3. add( 3, 3.5 ); // 6.5
4. add( "1", "5" ); // "15"
5. add( "hoge", "hoge" ); // "hogehoge"
6. add( [1, 3, 5], [6, 8, 9] ); // 1,3,56,8,9
1,2,3はともかくとして、4,5,6 は想定外な書き方をされたパターンです(結果はコメント分(//の後ろ)です。なお環境によって異なるかもしれません)。プログラマーが自分で作成して自分でこれを書いてしまった場合は、アホとしか言いようがないので避けられるといえば避けられます。でも他人が関わってくると、このようなパターンはあり得ます。というより、大人数になると本当にこれが良く発生します。特に、設計書がない!というパターン。
ちなみに、これをPHPで書くと
function add( $a, $b )
{
return $a + $b;
}
変数に $ が付くだけですね。
1. add( 1, 5 ); // 6
2. add( 1.5, 3.2 ); // 4.7
3. add( 3, 3.5 ); // 6.5
4. add( "1", "5" ); // 6
5. add( "hoge", "hoge" ); // error
6. add( [1, 3, 5], [6, 8, 9] ); // 1,3,5
1,2,3は想定内です。4が「6」になるのは暗黙の型変換という言語機能による結果だと思います。PHPの場合、文字列結合はドット演算子「.」になりますので、ここでは数値型へ強制変換されたのかと。5については、当然エラーとなってしまいます。6については、結果が[1,3,5,6,8,9]とならず、[1,3,5] となるのは、PHPでは、配列の+演算子は右側の配列を左側の配列に追加し、また両方の配列に存在するキーについては左側の配列の要素が優先され、右側の配列にあった同じキーの要素は無視されてしまうからです。(上記では [ 0 => 1, 1=> 3, 2 => 5 ], [ 0 => 6, 1 => 8, 2 => 9 ] という感じで値が格納されていると思ってください)2
このコーティングミスを防ぐためには、関数内で数値型以外はエラーや例外処理を書くという必要があります。でも、それだと本末転倒な気がします...。なお、PHP7より、プリミティブに対しての静的型付けができるようになっているので、PHPでは
function add( int $a, int $b ) : int
{
return $a + $b;
}
とすることで、
1. add( 1, 5 ); // 6
2. add( 1.5, 3.2 ); // 4
3. add( 3, 3.5 ); // 6
4. add( "1", "5" ); // 6
5. add( "hoge", "hoge" ); // error
6. add( [1, 3, 5], [6, 8, 9] ); // error
という、結果が得られます。浮動小数点型、文字列型は暗黙の型変換により int にそろえてくれたようです。エラーにしてほしいです。(しかも、PHPは関数のオーバーロードが無理なので(当たり前ですが)、int/float/string 用に作ろうとすると、関数名を変える必要があります。(PHP7.4以降で出来る可能性が出てきました...)
このように、動的型付けの場合、考え無しに実装すると実行時に思った通り動作しなくなり、そのバグの究明にかなり時間がかかることとなってしまいます。小さなプログラムであれば特定は簡単ですが、膨大なコード量になってくると窓からパソコンごと捨てたくなります。コードにコメントを書くことによってある程度、その命令の意図を伝えることは出来ますが、そのコメントを読まなかったりものすごく長いコードファイルの中にあったりすれば、辛いと思います。
なお、動的型のもう一つの欠点は、基本的に静的型よりも処理速度が遅くなる傾向にあります(機械語がわかっていると、理由はわかるはずです)
最後に、動的型付けに発生するコーティングミス(欠点)が、「undefined variable」です。
$name = "飛竜猫";
$yomi = "toryuneko";
// ()
echo "{$nama}({$yome})";
このコードは、飛竜猫(toryuneko)
という結果を期待したのですが、空白しか得られません。理由は、定義している変数と違う変数名で記述した(つまり、間違えてる)からです。PHPの場合、基本はこのコードは無視されます(つまり空)。デバッグモードを有効にしていたすると、一応エラーとして表示されますが、リリースしたあとに結構気がつくことが多いです(ウェブの仕組みなので)。これは、echo で、変数の名前を間違っているだけに過ぎないのですが、結構、動的型付けの言語ではこれが多くなります。そして、実行時に気がつくと言うことです。このコードはこの量なので直ぐにわかりますが、大量のコードに埋もれている場合は、これが結構気づきません。
静的型付けでは、この変数の指示ミスはあっても、変数が存在しないものを指定するミスはありません。ミスをしていても、コンパイル時に存在しない(undefined)となります。
これが、動的型付けにおいて、一番面倒でやっかいなバグ(すごく気が付きにくい)になります。
ちなみに、javascript には、undefined というのがあります。これ、予約語とかではないようです。まぁ、C言語のNULLもただの定数ですが。
ちなみに、undefinedは、null とは違います。nullは、「空」を示しますが、undefinedは「存在しない」です。
静的型付けとは
ここが課題です。デザイナーとのやりとりで難しいと言っていたのは、変数や関数に型がなぜ必要なのか、オブジェクト型って何!といわれる点です(プログラマーでもオブジェクトという概念は、結構捉えどころが人それぞれで難しいです。あくまで、オブジェクトとはプログラム言語における概念であって、機械語としてのレベルでの話になると、スタックかヒープに置かれた大きなデータへの参照・ポインタであるというだけなので。)
静的型付けは、簡単に説明すると変数や関数に型を予め定義しておき、その型以外のデータを変数では使えません。この変数は整数値を扱う、負の数はだめ、小数点で扱いますよ、数値の羅列(配列)で扱うのですよ!ということを予め定義するものです。
動的型付けで例として述べた、カレーを作る作業は、動的型付けでは結局、何のカレーをどの量作るのか?というのがわからない状況で持ち込まるため、それを想定して用意をしなければなりませんでした。
今回の、静的型付けでの考え方では、カレーの材料から必要な分量(じゃがいも 何g n個数、にんじん 何g n個数等、ボールの大きさ、鍋の大きさ、個数)など、細かい情報が入るわけです。つまり、レシピが用意された形(設計書)で、こちらは用意(コーティング)を行うわけです。
静的型付けは、ネイティブコードに直接コンパイルできる言語はほぼ持っています。というのも、CPUが理解できる機械語というものに変換するには、型という情報がなければコンパイラは機械語に変換できないからです。機械語に型という情報は厳密にはありません。メモリ上で、その変数の情報にどのぐらいのメモリを確保し、そのデータはどのように扱うものなのかという判別が出来るぐらいです。コンパイラは、これを考慮して機械語を出力します。
int a; // (4byte)
double b; // (8byte)
char c[6]; // (6byte)
型があるコードをコンパイルすると、変数順に、4, 8, 6 byteの容量をメモリ上のスタック領域またはヒープ領域に確保するコードが出力されます。
コンパイラが出力した機械語には、そのデータはどのメモリ範囲にデータがあるか、という情報のみが書かれており、4,8,6byteのデータがどのような型(int,double,char[])であるのか、という情報はなくなっています。そこにあるのは、あくまで数値的なデータだけです。なお、データそのものはただの数値の羅列として扱われますが、各機械語の命令を通すことで、それが数値・浮動小数点・バイナリ配列・文字列だったのかというのがわかるようになっています。
さて、これを動的型付けに置き換えて
var a;
var b;
var c;
としたところで、これは何が入るのでしょうか。そして、どのようにメモリを配置すればいいのでしょうか。この情報だけでは、コンパイルしてもメモリでの操作を行うコードはおろか、メモリの配置容量や先をも決めることは出来ません。なので、動的型付け言語では、直にネイティブコンパイルができないのです。そのため、インタプリタで逐次実行しながら、動作の過程でわかる型を確認し、JITコンパイルを利用してコンパイル(機械語に出力)を行う形が主流になっているのです。
理由はわかったけど
静的型付けが必要な理由は、事前にネイティブにコンパイルするには必須であるということです。特にiOSのネイティブアプリを開発するには、動的言語(インタプリタ専用の言語)が利用できないということで、覚えてください...ということになったのですが、この静的型付けも種類が多く、また言語によって特徴もあってなかなかややこしいです。
特に、オブジェクト型(インスタンスを生成するのに)に対して、なぜ型が必要なのかと聞かれたとき、どのように答えればいいか困ったものです。概念的なことはプログラマーですので頭の中でこういう理由なのだが、というのはわかるのですが、プログラマーではない一般の人?というのにそれを理解してもらうのは結構難しいです。機械語(物理メモリに関すること)に影響するので(そもそも機械語に型はない。厳密には全くないわけではなく、値を扱うときにAMD64(x86_64)の場合、機械語レベルで32bit値は eax レジスタを、64bitは rax を使うなど、レジスタに応じた命令が用意されているため。
このあたりは、いかに機械語・CPUやコンピュータの仕組みを理解できているかによるため、なかなか難しい問題ではあります。
まとめ
猫は動的型付け言語もPerl/PHP/Ruby/Javascript/VBScriptと触ってきていますが、やはり静的型付け言語の方が好きです。変数名をしっかりと名付けることで、その変数は文字列なのか数値なのか、配列なのか、オブジェクトなのかというのがわかる...という仕様にしてしまえばいいのですが、結局他人と共同でやったり、時がたてば、そのコードの意図など忘れてしまいます。そういう点では、静的型付け言語で書いておけば、その意図が型として定義づけられているので、時がたっても思い出せるのではないか...思います。
とはいえ、最近流行のJSONを静的型付け言語で処理するのは結構大変です。特に、自分で作るシステムならば問題ないですが、他人が作ったシステムを利用とした場合、その構造が多岐にわたってくるとその分の構造を予め作っておく必要があります。それはそれで結構大変です。動的型付けの場合は、一度受け取った後で分解させる処理を書いていけば済むわけです。なので、どちらが優れている、欠陥だというよりは、適材適所に合わせて使うのが一番いいのでしょう。
ちなみに、猫の好きなC#言語は、静的型付け言語なのですが、dynamicという、動的型付けできる型が存在しています。C++言語にもRTTIとよばれるものもありましたので、静的言語に動的言語を扱えないというわけではありません。
ただ、動的な型は、実行時にサイズがわからないため、メモリ容量を多めにとったり、実行時にどの程度のサイズになるかを計測する命令を通り、当然ながら機械語にあるようなレベルの型の扱い(2byteをメモリから読み込む、4byteをメモリから読み込む)という単純命令ができないため、処理はかなり遅くなります。
また、動的型はインテリセンス(IDEの入力補完機能)が型がわからないので、その変数に何が入れられるかとういのがわからないことと、動的型特有の実行時に判別するので実行時エラーにより思った通りの動作ができない(不具合の究明に死ねる)という弱点がありますが、適材適所で使うと利便性が高まります。