プログミングにおけるデータの型と静的型付け言語、動的型付け言語
型理論(Type theory)とは、プログラミング・数学・言語学等に現れる型の概念及びそれらが成す型システムを研究対象とする数学・計算機科学の分野である。特定の型システムのことを型理論と呼ぶこともある。集合論の代替となる数学の基礎として役立てられる型理論(型システム)も存在する。そのような例としてアロンゾ・チャーチの型付きラムダ計算やマルティン・レーフの直観主義型理論が有名である。
20世紀初頭にバートランド・ラッセルが発見した、ラッセルのパラドックスによるフレーゲの素朴集合論の欠陥を説明する中で提起されたtheories of typeが型理論の起源であり、後年にAxiom of reducibilityが付随された型理論は、ホワイトヘッドとラッセルの 『プリンキピア・マテマティカ』に収録されている。
また”人工無脳が語る禅とブッダぼっど“ではラッセルの「プリンキア・マテマティカ」を読んでさらに考察を深めたウィトゲンシュタインによる言葉の型と意味との関係を禅の思想に絡めて述べている。
さらなる数学的アプローチとしては、”現代思想2020年7月号 特集=圏論の世界 ――現代数学の最前線 読書メモ“で述べているような圏論とも関連するものとなる。
前回は、ソフトウェアデザイン2020年5月号より、コンピューターにおける型の基本として、プログミングにおけるデータの型と静的型付け、動的型付けについて述べた。今回は静的型付け言語と動的型付け言語の違いについて述べる。静的型付け/動的型付け言語とはなにか、
静的型付け言語というのは、式の型がコンパイル時に決まっている言語となる。まず、静的型付け言語では、変数に型がある。Javaでa:と宣言したら、aはint型になる。そしてa+1というのは、int型の変数aにint型の1を加算したものなので、結果はint型となる。もしa+0.5なら、Javaではintはdouble型に拡張してから加算されるので、結果はdouble型となる。このように、静的型付け言語では、それぞれの式の型が部分式を含め、コンパイル時に決まっている。もちろん、メソッドの引数や戻り値の型も決まっている。そうでなければ式の形が決まらないからである。
a+1+0.5という式を木構造であらわしたものが下図となる。
変数aをint型とした場合の、各部分式における型を四角で表している。
それに対して、動的型付け言語では、そもそも変数に型はない。よってa+1の型も決まらない。動的型付け言語では、型は、実行時にオブジェクトの側に付加される。たとえば、変数aの値が1のときのイメージは下図のようになる。
静的型付けの図はコンパイル時のイメージとなるが、動的型付けの図は実行時のイメージとなる。
動的型付け言語では、変数の型や式の型は実行時まで決まらないので、例えば「文字列を文字列で除算する」といった意味のないコードを書いても実行時まではエラーとならない。
なお、動的型付け言語でも、”オブジェクト指向言語(1)“等で述べているオブジェクト指向言語であれば、スーパークラスの型の変数にサブクラスのオブジェクトへの参照を代入することは可能となる。そして、実際にメソッドを呼び出す際は、変数の型ではなくオブジェクトの型に従ったメソッドが呼び出される(“Clojureでのポリモーフィズム“等で述べているポリモーフィズムと呼ばれる)わけであるので、オブジェクトの側に型情報を持っていることがわかる。それこそ、JavaのObjectやScalaのAnyのような最上位のクラス型変数を使えば、どんな型のオブジェクトも指せてしまうので、動的型付け言語と実質同じようなものだ、とも言える。しかし、実際のプログラムでは、変数には適切な型を選ぶし、静的型付け言語では式の型(たとえそれがObjectであれ)がコンパイル時に決まっている、ということには変わりがない。
それぞれのメリット・デメリット – 性能について
結論から先に言うと、一般的に静的型付け言語の方が実行速度は速くなる。すでに述べたように、動的型付け言語では「文字列を文字列で割る」というような意味のない計算でも、それが意味のない計算であるとこが実行時までわからない。よって、エラーチェックを実行時に行う必要があるため、どうしても実行速度は遅くなる。足し算一つとっても、整数同士の足し算と、浮動小数点同士の足し算とでは実行する機械語命令は異なるので、動的型付け言語ではそういった判定も実行時にやらないといけなくなる。
またオブジェクトのフィールドの参照やメソッドの呼び出しも、静的型付け言語のほうが高速にできる要素がある。静的型付け言語なら、あるフィールドを参照したいとき、それがメモリ上で何番目にあるかがコンパイル(またはロード)の時点で決まっているので即座にアクセスできるが、動的型付け言語ではフィールドも実行時に決まるので、名前で検索する必要がある(ハッシュを使った高速化はなされているが)。メソッド呼び出しも同様で、静的型付け言語なら仮想関数テーブルから一発でメソッドが選択できるところ、動的型付け言語では実行時に検索する、という実装になっているのが普通である。
とはいえ、用途によっては、言語自体の速度が遅くても問題ないという場合もある。たとえばRubyはRuby on RailなどでWebアプリケーションの構築に使われるが、Webアプリケーションでは、大抵の場合、ボトルネックはDBアクセスかネットワークとなるため、Ruby部分が遅くても実際の性能には影響を与えない。
「速度が必要なところはCやC++で書く」という解決法もある。近年、機械学習をPythonで行うのが流行っているが、数値計算のライブラリNumPyやディープラーニングのライブラリTensorFlowはCやC++で記述されていて、Pythonからそれを呼び出して利用できるようにしている。
それぞれのメリット・デメリット – 記述の容易さ
静的型付け言語では、ソースコード上で型を指定しなければいけないが、動的型付け言語では不要なので、動的型付け言語のほうがソースコードが簡潔になる、という観点もある。
たとえば2つの数を足すという関数(メソッド)を考えた時、Javaなら
int add(int a, int b) {
return a + b;
}
となるのに対して、型のないJavascriptでは以下のようになる。
function add(a,b) {
return a + b;
}
確かにJavascriptではintと書かずに済んでいるが、この程度では大差ないということなる。これに対してある程度複雑な例になってくると、確かに静的型付け言語だと煩わしいというケースが出てくる。
以下の例は、”プログミングにおけるデータの型と静的型付け言語、動的型付け言語“で述べたPersonクラスを、文字列型のユーザーIDで検索できるようにHashMapに格納したいときのJavaコードとなる。
Map<String, Person> personMap
=new HashMap<String, Person>{}:
これはJavaのジェもリクスという機能を使用しているが、動的型付け言語では、そもそもジェネリクスは不要となる。どのみちコンパイル時の型チェックはないからである。
なお、JavaもJava10から、ローカル変数については型推論ができるようになったので、以下のように書ける。
ver personMap
= new HashMap<string,Person>{};
ところで、動的型付け言語のほうが簡潔に書けると言う場合、実際には動的型付けか静的型付けかどうかは関係ないケースもある。単に予約後が短いとか、行末にセミコロンがいらないとか、トップレベルに文が書ける(1行出力するためだけにクラス定義やメソッド定義を書かなくて良い)という特徴から、簡潔に見えるという要因も多くなる。
それぞれのメリット・デメリット – 可読性
一般にプログラムは短い方が読みやすくなる。よって、記述の簡潔な動的型付け言語のほうが読みやすい、と言う観点もある。十数年前「JavaからRubyへ」と言われていた頃には、そのようなことが広く言われていた。PCの画面は広くなってきたとはいう有限だし、記述が簡潔であれば一度に目に入る量が増える。ソースを書いているときなら、頭はそのソースに集中しているので、そういうときは、簡潔さが有効となることが多い。
しかし、しばらく経ってからソースを読み返す場合、簡潔であるということはヒントが少ないと言うことであり、ソースを書いている時ならたとえ1文字の変数名でも頭に入っていると思うが、あとで読み返すときには何が何だかわからない、と言うことになりがちである。動的型付け言語でも同様のことが起きる。
例えば以下のJavascriptコードは、引数に渡された「キャンパス」をクリアするメソッドになる。
clear(canvas) {
let context = canvas.getContext("2d");
… 以下略
上記のコードではこの引数のcanvasが何者かは分かりずらい。この例であればJavascriptの知識があれば、HTML5のCanvasだろうと検討もつくが、これがこのプログラムの独自クラスであれば、その定義をどのようにして探せば良いのか、このメソッドの呼び出し元を探すにしても、claer()というメソッド名は他のクラスにもありそうであり、このクラスのclear()であるかどうかは判断はつかない。
プログラムは、書くときより読む時のことを考えて書け、とよく言われる。これはプログラムを書いている時間よりも、他人が書いたプログラムを読んでいる時間のほうが圧倒的に長いからである。その点で、動的型付け言語の可読性の低さは、生産性に深刻な悪影響を及ぼす可能性がある。
それぞれのメリット・デメリット – 型は設計の指針になる
プログラミング言語において、実行されるコードよりもデータ構造がずっと大事だと広く言われている。その点で考えると静的型付け言語の方が、事前に全てのデータ構造を定義していて分かりやすいという利点がある。
静的型付け言語をメインに使っているプログラマは、プログラムを書く際に、まずデータ構造とインターフェース(メソッドシグニチャ)を決めて書き始める。それさえ決めて仕舞えば、あとはそれに従って粛々とコードを書くだけであり、チームでプログラムを書く際にも助けになる。
それぞれのメリットデメリット – 変更への強さ
動的型付け言語は「柔軟」だと言われる。静的型付け言語ではトップダウン的にデータ構造やメソッドシグニチャをまず決めてからコードを書くが、動的型付け言語ではアジャイル的にコードを書き、直すと言う形となる。そのため、動的型付け言語で書いたコードは、全体として整合性が崩れた状態でもある程度動くため、特にweb開発などの場面ではエラーを出しながらでも動作を確認していけるメリットがある。
これに対して、静的型付け言語では不整合なポイントがあるとすべて直さないとコンパイラが許してくれず動作しない。逆にいうと、コンパイラがすべて指摘してくれた箇所を直せば概ね安心して動作できるという利点がある。
これは、自動単体テストですべてのコードを通すことができれば動的型付け言語でもよいが、変化が激しくテストコードを十分に書けないケースなどでは修正が根拠をもってできると言う点で静的型付け言語にも利点がある。
それぞれのメリット・デメリット – JSONファイルの取り扱い
何らかのプログラムを書いて、そのプログラムの設定ファイルがJSONで記述されているとする。例えば以下のようなものを考える。
{
"defaultWidth":800,
"defaultHeight":600,
"defaultSavePath":"C:\\temp",
"users":{"taro","jiro"}
}
これを読み込んでプログラム中で使うとして、静的型付け言語を使っているプログラマであれば、何らかのJSONパーサーでJSONを読み込んでから、独自のクラスのフィールドに代入する、あるいはJSONをクラスにマッピングする機能を使うと言う方法をまず考える。しかし、この設定値を使うのが最初の一回きりなら、いちいちクラスを作るのも面倒になる。
このようなとき、動的型付け言語であれば、JSONパーサーがハッシュか何かに変換してくれれば、以下のRubyのプログラムのように設定値を取り出せる。
defaultWidth = conf["defaultWidth"]
これが静的型付け言語だと、例えばJSONパーサがHashMapに変換してくれたとしても、値を取り出すときに、型がわからないのでダウンキャストの必要がある。
int defaultWidth =
(int)conf.get("defaultWidth");
この例だと、JSONのdefaultWidthのところに整数でない値が記載されていると、実行時にエラーとなる(ClassCastException)。実行時にエラーとなるのは動的型付け言語と同じになるため、静的型付けのメリットが消えてしまう。
型が決まっていないJSONという表現形式を、静的型付け言語で扱うのは向いていないの当然だが、「中の型だかわからないデータを突っ込める」というのは動的型付け言語のメリットとなる。
型推論について – Javaの型推論
前述したようにJava10以降は以下のように書くかわりに
Map<String, Person> personMap
= new HashMap<String,Person>{};
以下のように書くこともできる
ver personMap
= new HashMap<String, Person>{};
これはコンパイラが型を推論してくれるためであり、これを型推論と呼ぶ。
この例は型推論の中でも「ローカル変数の型推論」と呼ばれるもので、代入の右辺の型を単純に左辺の型に適用すると言う最も単純なものとなる。
型推論について – Scalaの型推論
たとえばScalaなら以下のように戻り値の型を推論することができる。
//引数を引数を加算して戻す関数
def add(a:Int,b:Int) = a + b
これは以下のように書くのと同じとなる。戻り値の型のIntをコンパイラが推測してくれるものとなる。
def add(a:Int, b:Int): Int=a + b
型推論について – Haskelの型推論
Haskelでは以下のように引数の型も省略できる。
add a b = a + b
その上、Haskellでは、このadd関数は引数の型が整数型であろうと浮動小数点数であろうと使える。
print (add 3 5). --8と表示
print (add 3.2 5) --8.2と表示
こうしてみると、動的型付けのように見えるがHaskellはあくまでも静的片付け言語であり、例えばadd関数の引数に数値と文字列を渡すと、実行時ではなくコンパイル時にエラーとなる。
動的型付け言語の「簡潔に書ける」というメリットは、型推論のある言語を使えば静的型付け言語でも十分に達成できるが、静的型付け言語のメリットを「型が明示的に示されているおかげで可読性が高い」ということを挙げるのであれば、型推論に頼って型を描かないのはデメリットになる。
このように静的型付け言語と動的型付け言語ともに長短があり、最近ではPythonやClojure等の動的型付け言語でも部分的に型に対する対応が出来始めている。
次回は、動的型付け言語のPythonにおける型ヒントについて、mypyという型チェッカーについて述べる。
コメント
[…] 動的型付け: Schemeは”プログラミングにおける静的型付け/動的型付け言語の違い“でも述べているような動的型付けの言語であり、変数の型を宣言する必要がない。これにより、 […]