動的型付けと静的型付け


動的型付けと静的型付け

プログラム言語では、2種類の変数や関数を扱うタイプが存在します。それが動的型付けと静的型付けです。つまり、コードを書くときに、関数や変数について、ユーザーがこの変数や関数については、この「型」ですよー!と、手動で定義づけるか、プログラムが勝手にこの変数や関数はこの「型」ですな!と、自動で定義つけてくれるか、ということになります。

今回、この議題をあげたのは、開発時にデザイナーとのやりとりで、なぜ動的型付けと静的型付けがあるのか、という議論を行ったのが要因で、その場で上手な説明が出来なかったので、ちょっと整理してみました。


動的型付けとは

動的に型を付けるという読んでそのままの通り、プログラムを書くときに変数や関数に何が入ってくるかというのが特に決まっていない形を指します。特にインタプリタで実行される言語は、ほぼこの形式を採用しています。型を宣言する必要がないため、小さいプログラムや型の変動が激しい環境であれば簡単に実装できる反面、動的型付けは事前コンパイルができない=実行しないとバグがわからないという致命的な面を持っています。

例えば、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

このコーティングミスを防ぐためには、関数内で数値型以外はエラーや例外処理を書くという必要があります。でも、それだと本末転倒な気がします...。なお、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以降で出来る可能性が出てきました...)

このように、動的型付けの場合、考え無しに実装すると実行時に思った通り動作しなくなり、そのバグの究明にかなり時間がかかることとなってしまいます。小さなプログラムであれば特定は簡単ですが、膨大なコード量になってくると窓からパソコンごと捨てたくなります。

でも、動的型付け言語も利点はあります。JSON等のデータを扱う場合で、そしてそのデータの中身(型)が変化する場合です。型が必要ないので、予め型を宣言する必要がありませんから、どのようなのが来てもも万事解決です。この点は、動的型付けの揺るぎない利点でしょう。

なお、動的型というのは基本的に静的型よりも処理速度が遅くなる傾向にあります(機械語がわかっていると、理由はわかるはずです)


静的型付けとは

ここが課題です。デザイナーとのやりとりで難しいと行っていたのは、変数や関数に型がなぜ必要なのか、オブジェクト型ってなんやねん!という点です。

静的型付けは、簡単に説明すると変数や関数に型を予め定義しておき、その型以外のデータを変数では使えません。この変数は整数値を扱う、負の数はだめ、小数点で扱いますよ、数値の羅列(配列)で扱うのですよ!ということを予め定義するものです。

静的型付けは、ネイティブコードに直接コンパイルできる言語はほぼ持っています。というのも、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の入力補完機能)が型がわからないので、その変数に何が入れられるかとういのがわからないことと、動的型特有の実行時に判別するので実行時エラーにより思った通りの動作ができない(不具合の究明に死ねる)という弱点がありますが、適材適所で使うと利便性が高まります。





  1. 当初、6番目はエラーと書いていましたが、var_dumpで結果を確認せず、echo でしてしまっていました。失礼しました。また、ご指摘ありがとうございます。