Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
391
Help us understand the problem. What are the problem?

動的型付けと静的型付け

はじめに

世の中には、多数のプログラミング言語が存在しますが、ほとんどの言語において必須な言語機能として用意されている「変数」、言語によっては多少、名称や扱い方が異なりますが「関数(サブルーチン、メソッド等)」があります。
これら変数や関数でデータを扱うとき、大雑把に分けると2つのパターンでデータの扱い方が分かれており、それが今回の話題として扱う「動的型付け」と「静的型付け」になります。
単純な考え方として、コードを書く場合に、予めプログラマが変数や関数の引数、返り値に対してどのようにデータを扱うかを指定する、つまり「型」を決めて使うことを「静的型付け」と言い、コンパイラやインタプリタがプログラマによって書かれたプログラムのコードを実行・解釈し、変数や関数の引数、返り値について、実行時に型を想定して扱うことを「動的型付け」と言います。

今回、この議題を上げたのは、開発時のデザイナーとの連携でのやり取りにおいて、なぜ、動的型付けと静的型付けというものがあるのか?動的型付けだけあればいいのではないか?という議論を行ったのが要因で、その場で上手な説明をすることが出来なかったので、改めて動的型付け・静的型付けについて整理してみました。

そもそも型とは何か

データというのは抽象的な物であり、コード上では文字列や数値、論理値であったとしても機械語・物理メモリ上ではただの数値の羅列でしかありません。また、コンピュータにおいて、データは有限数であり、必ず上限があります。それら、機械語・物理メモリ上における抽象的な数値の羅列や数値の有限範囲に意味を持たせることが「型」の定義・意義になります。

なお、インタプリタで良く採用される動的型付けの言語では型が存在しない、また、事前コンパイラ(AOTや中間言語等)を通す静的型付けの言語は、型は静的型付けのみであり、動的型付けとして型を定義することは出来ない、と思われる方が意外に多いのだが、インタプリタで扱われる言語であっても変数や関数(引数や戻り値)などにおいて「型」は存在しており、標準関数や標準クラスとして用意されている言語ライブラリは、正しい「型」で扱わないと実行時エラーとなる場合があります。また、静的型付けの言語であっても、RTTI(RunTimeTypeInformation)と呼ばれる実行時型情報という技術により、動的型付けとして型を扱うことが可能な場合があります。

コンパイルとインタプリタ

まず、前提知識として「型」を押さえる前にプログラムの実行形態について簡単に説明します。

コンパイラと呼ばれる実行ファイルにソースコードを読ませることで、別の言語形態(機械語に変換、別の高級言語に変換など)に変換(翻訳)を主とすることを「コンパイル(翻訳の意)」といい、インタプリタと呼ばれる実行ファイルにソースコードを読み込ませることで翻訳・実行を同時に行うことを主とする形態を「インタプリタ(通訳者の意)」といいます。
ちなみに、主にコンパイル型の言語として扱われているが、インタプリタとしても利用することが出来る場合(例えばC言語やC#言語等)もあるため、「コンパイル型言語」や「インタプリタ型言語」と「言語として分類」することは難しいので、ここでは「コンパイル」と「インタプリタ」については言語にとらわれない考え方で進めます。

「コンパイル」とは、主に「別の言語に翻訳・変換」することが主で、翻訳や変換までしかしません。そのため、基本的にはコンパイルしただけでは実行は出来ません。リンカ等を通してライブラリやフレームワークなど必要なコードをリンクを行い、実行ファイルを生成する(これらコンパイル・リンク等を行い、パッケージなどを生成することを「ビルド」と言う)、または別の変換した言語を再度コンパイルする・インタプリタを通すなどが必要になります。現実的な考え方は、「海外の方が作った書物を、予め現地語に翻訳しておく」というイメージになります。

「インタプリタ」は、主に「インタプリタに渡されたソースコードを翻訳・実行までさせる」ことが主になります。現実的な考え方は、「海外の方が話している内容を、その場で通訳者が現地語に翻訳して伝えている」というイメージになります。

このように、コンパイルとインタプリタは、「翻訳する」という考え方までは同じで、その後にどうするのか?という部分が異なるという点です。
インタプリタの場合、「翻訳」をしてもソースコードは残ります(翻訳者が覚えてる)。これは実行が完了する(翻訳者がその仕事を終えるまで)まで携わるためです。そのため、コードの実行時に、型の意味を考える必要があっても、ソースコード(元のコード)から辿ることが可能です。
しかし、コンパイルされた場合では「ソースコード」が翻訳されると現地語に翻訳されたコードのみが残ります(翻訳された書類しか残らない)。そして、ソースコード(元となったコード)は基本なくなります。つまり「元のコード」が存在しなくなり、もしその時に「型」の意味が不明であれば、辿ることが出来なくなり、実行させられなくなる、ということになります。

動的型付けとは

『動的に型を付ける』という読んでそのままの通り、プログラムを書くときに変数や関数に何が入ってくるかというのが特に決まっていない形を指します。要は、プログラマーは、変数や関数になんとなく、こんな感じのデータが入るであろうということを予測するだけで、どんな値が入るかはコーティングには影響なく、実行時に多少影響するぐらいで、型の管理はインタプリタやコンパイラが実行時にしてくれるという形態になります。
特にインタプリタで実行される言語は、ほぼこの形式を採用しています。型を宣言する必要がないため、小さいプログラムや型の変動が激しい環境であれば簡単に実装できる反面、動的型付けは事前にコンパイルしてネイティブで最適化されたコードを出力することは出来ません。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の入力補完機能)が型がわからないので、その変数に何が入れられるかとういのがわからないことと、動的型特有の実行時に判別するので実行時エラーにより思った通りの動作ができない(不具合の究明に死ねる)という弱点がありますが、適材適所で使うと利便性が高まります。


  1. 当初、動的型付けではネイティブにコンパイルできないと書いていましたが、RTTI(RunTimeTypeIdentification:実行時型情報)などの仕組みを使うことで、動的型的な情報をネイティブにはコンパイルは可能です。この記事内では、ネイティブにおけるプリミティブなデータ関係でコンパイルできないと意味合いで書いていました。 

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
391
Help us understand the problem. What are the problem?