この記事では、静的に型付けされた言語と動的に型付けされた言語の違いについて説明し、「強い」型と「弱い」型の概念を調べ、異なる言語の型付けシステムの能力を比較します。 最近、プログラミングにおいてより厳格で強力なタイピングシステムへの明確な動きがあるため、タイプとタイピングについて話すときの意味を理解することが重要です。
タイプは、可能な値のコレクションです。 整数は、値0、1、2、3などを持つことができます。 ブール値はtrueまたはfalseです。 たとえば、「Give」と「5」の値が可能な「Give Five」タイプなど、独自のタイプを考えることができます。 これは文字列や数値ではなく、新しい別個のタイプです。
静的に型付けされた言語は変数の型を制限します。たとえば、プログラミング言語はxが整数であることを知っている場合があります。 この場合、プログラマーはx = true
を行うことを禁じられており、これは誤ったコードになります。 コンパイラはコンパイルを拒否するため、そのようなコードを実行することさえできません。 別の静的に型付けされた言語には他の表現能力があり、人気のある型システムはどれもDaiPyat型を表現できません(しかし、多くは他のより洗練されたアイデアを表現できます)。
動的に型付けされた言語は値を型でマークします。言語は1が整数、2が整数であることを知っていますが、変数xが常に整数を含むことを知ることはできません。
言語ランタイムは、これらのラベルをさまざまな時点でチェックします。 2つの値を追加しようとすると、彼女はそれらが数値、文字列、または配列であるかどうかを確認できます。 次に、タイプに応じて、これらの値を追加、接着、またはエラーを出します。
静的に型付けされた言語
静的言語は、プログラムの開始前であっても、コンパイル時にプログラムの型をチェックします。 型が言語の規則に違反するプログラムはすべて不正と見なされます。 たとえば、ほとんどの静的言語は、式"a" + 1
を拒否します(Cはこの規則の例外です)。 コンパイラは、 "a"が文字列であり、1が整数であり、 +
が左右の部分が同じ型である場合にのみ機能することを知っています。 そのため、問題があることを理解するためにプログラムを実行する必要はありません。 静的に型付けされた言語の各式は、コードを実行せずに定義できる特定の型を参照します。
多くの静的型付け言語では、型を指定する必要があります。 Java public int add(int x, int y)
の関数はpublic int add(int x, int y)
2つの整数を取り、3番目の整数を返します。 他の静的に型付けされた言語は、型を自動的に判別できます。 Haskellの同じ加算関数は次のようになります: add xy = x + y
。 言語の種類はわかりませんが、 +
が数値に対してのみ機能することがわかっているため、 x
とy
は数値である必要があるため、 add
関数は引数として2つの数値を受け取ります。
これは、「静的」型システムを減らすものではありません。 Haskellの型システムは、静的で、厳密で、強力な機能で有名であり、これらのすべての分野で、HaskellはJavaに先んじています。
動的に型付けされた言語
動的に型付けされた言語では、型を指定する必要はありませんが、それ自体で型を定義する必要もありません。 変数のタイプは、起動時に特定の値を持つまで不明です。 たとえば、Pythonの関数
def f(x, y): return x + y
2つの整数、接着線、リストなどを追加できますが、プログラムを実行するまで正確に何が起こっているのか理解できません。 おそらくある時点で、関数fは2行で呼び出され、別のときに2つの数値で呼び出されます。 この場合、xとyには、異なる時間に異なるタイプの値が含まれます。 したがって、動的言語の値には型がありますが、変数や関数には型がないと彼らは言います。 値1は間違いなく整数ですが、xとyは任意です。
比較
ほとんどの動的言語は、型が誤って使用されるとエラーをスローします(JavaScriptはよく知られている例外です。意味をなさない場合でも、式の値を返そうとします)。 動的に型付けされた言語を使用する場合、戦闘環境では"a" + 1
という形式の単純なエラーが発生する可能性があります。 静的言語はそのようなエラーを防ぎますが、もちろん、防止の程度は型システムの能力に依存します。
静的言語と動的言語は、プログラムの正確性に関する根本的に異なる考え方に基づいて構築されています。 動的言語"a" + 1
これが正しいプログラムです。コードが起動され、ランタイムにエラーが表示されます。 ただし、ほとんどの静的に型付けされた言語では、式"a" + 1
はプログラムではありません。コンパイルも実行もされません。 これは、ランダムな文字のセットのような無効なコード!&%^@*&%^@*
は無効なコードです。 正確さと不正確さというこの追加の概念は、動的言語では同等のものではありません。
強いタイプと弱いタイプ
「強い」と「弱い」の概念は非常にあいまいです。 それらの使用例をいくつか示します。
「強い」とは「静的」を意味する場合があります。
ここではすべてが単純ですが、「静的」という用語を使用することをお勧めします。ほとんどの用語が使用され、理解されているためです。
「強い」とは、「暗黙的な型変換を行わない」ことを意味する場合があります。
たとえば、JavaScriptでは"a" + 1
を記述できますが、これは「弱いタイピング」と呼ぶことができます。 しかし、ほとんどすべての言語は1つまたは別のレベルの暗黙的な変換を提供します。これにより、整数から1 + 1.1
などの浮動小数点数に自動的に切り替えることができます。 実際には、ほとんどの人は「強い」という言葉を使用して、許容可能な変換と許容できない変換の境界を定義します。 一般に受け入れられている境界線はありません;それらはすべて不正確であり、特定の人の意見に依存しています。
「強い」とは、言語の厳密なタイピング規則を回避できないことを意味する場合があります。
- 「強い」とは、メモリセーフを意味する場合があります。
Cは、メモリセーフでない言語の例です。xs
が4つの数値の配列の場合、Cはコードxs[5]
またはxs[1000]
を喜んで実行し、xs[5]
直後にメモリから値を返します。
やめましょう。 これは、いくつかの言語がこれらの定義を満たす方法です。 ご覧のとおり、Haskellだけがあらゆる点で一貫して「強力」です。 ほとんどの言語はそれほど明確ではありません。
言語 | 静的 | 暗黙的な変換? | 厳格なルール? | メモリにとって安全ですか? |
---|---|---|---|---|
C | 強い | どうやって | 弱い | 弱い |
Java | 強い | どうやって | 強い | 強い |
ハスケル | 強い | 強い | 強い | 強い |
Python | 弱い | どうやって | 弱い | 強い |
Javascript | 弱い | 弱い | 弱い | 強い |
(「暗黙的な変換」列の「いつ、どのように」とは、強いものと弱いものとの区別が、許容できると考えられる変換の種類によって異なることを意味します)。
多くの場合、「強い」および「弱い」という用語は、上記のさまざまな定義、およびここに示されていない他の定義の不定の組み合わせを指します。 この混乱により、「強い」と「弱い」という言葉はほとんど意味がなくなります。 これらの用語を使用する場合は、正確に何を意味するのかを説明することをお勧めします。 たとえば、「文字列に数字を追加するとJavaScriptは値を返しますが、Pythonはエラーを返します」と言うことができます。 この場合、「強い」という言葉の多くの意味について合意に達する努力を無駄にしないでしょう。 または、さらに悪いことに、用語が原因で未解決の誤解が生じます。
ほとんどの場合、インターネット上の「強い」および「弱い」という用語は不明確であり、特定の人々の意見が不十分に定義されています。 彼らは言語を「悪い」または「良い」と呼ぶために使用され、この意見は専門用語に変わります。
強い型付け:私が大好きで、私が快適に感じる型システム。
弱いタイピング:私を悩ませたり、気に入らない型システム。
段階的なタイピング
動的言語に静的型を追加することは可能ですか? ある場合には、はい。 他では、それは困難または不可能です。 最も明らかな問題は、動的言語のeval
およびその他の同様の機能です。 Pythonで1 + eval("2")
を実行すると3が得られますが、 1 + eval(read_from_the_network())
は何を与えますか? 実行時に何がオンラインになっているかによって異なります。 数値が得られれば、式は正しいです。 文字列の場合、いいえ。 開始する前に調べることは不可能であるため、型を静的に解析することはできません。
実際の不十分な解決策は、 eval()
式をAnyに設定することです。これは、一部のオブジェクト指向プログラミング言語のObjectまたはGoのインターフェースinterface {}
に似ています。これは、任意の値を満たすタイプです。
Any型の値は何によっても制限されないため、評価システムを支援する型システムの機能はなくなります。 eval
と型システムの両方を持つ言語は、 eval
使用されるたびに型安全性を放棄しなければなりません。
一部の言語には、オプションの入力または段階的な入力があります。デフォルトでは動的ですが、いくつかの静的注釈を追加できます。 Pythonは最近、オプションの型を追加しました。 TypeScriptは、オプションのタイプを持つJavaScriptアドインです。 フローは、古き良きJavaScriptコードの静的分析を実行します。
これらの言語は静的型付けの利点の一部を提供しますが、真の静的言語のように絶対的な保証を与えることはありません。 一部の関数は静的に型付けされ、一部は動的に型付けされます。 プログラマーは常に違いを知り、注意する必要があります。
静的に型指定されたコードのコンパイル
静的に型指定されたコードをコンパイルするとき、コンパイラと同様に、最初に構文がチェックされます。 次に、タイプがチェックされます。 これは、最初は静的言語が1つの構文エラーについて文句を言い、それを修正した後、100の入力エラーについて文句を言うかもしれないことを意味します。 構文エラーを修正しても、これらの100個の入力エラーは発生しませんでした。 コンパイラは、構文が修正されるまで型エラーを検出できませんでした。
通常、静的言語コンパイラは、動的言語コンパイラよりも高速なコードを生成できます。 例えば、コンパイラーがadd関数が整数を受け入れることを知っている場合、中央処理装置のネイティブADD命令を使用できます。 動的言語は、実行時に型をチェックし、型に応じて多くの追加関数のいずれかを選択します(整数または浮動小数点を追加するか、文字列またはリストを追加しますか?)または、エラーが発生し、型が一致しないことを決定する必要があります。 これらすべてのチェックには時間がかかります。 動的言語では、型に関する必要な情報をすべて受け取った後、実行時にコードが再コンパイルされるJITコンパイル(ジャストインタイム)など、最適化のためにさまざまなトリックが使用されます。 ただし、Rustなどの言語で適切に記述された静的コードの速度に匹敵する動的言語はありません。
静的および動的タイプの引数
静的型システムの支持者は、型システムがないと、単純なエラーが本番環境で問題を引き起こす可能性があることを指摘しています。 もちろんこれは事実です。 動的言語を使用したことがある人は、これを自分で経験しています。
動的言語の支持者は、そのような言語でコードを書く方が簡単だと指摘します。 これは、 eval
を使用したコードなど、私たちが時々書くある種のコードには間違いなく当てはまります。 これは、定期的な作業では物議を醸す決定であり、「簡単」という曖昧な言葉を思い出すのは理にかなっています。 リッチ・ヒッキーは 、「簡単」という言葉と、「簡単」という言葉とのつながりについてよく語りました 。 このレポートを見ると、「簡単」という言葉を正しく使用することは容易ではないことが理解できます。 「軽さ」に注意してください。
静的および動的タイピングシステムの長所と短所はまだ十分に理解されていませんが、言語と解決される特定の問題に確実に依存しています。
JavaScriptは、無意味な変換(「a1 "a" + 1
を与える「a」 "a" + 1
など)を意味する場合でも、動作を継続しようとします。 Pythonは、 "a" + 1
場合のように、保守的になり、エラーを返すことがよくあります。
セキュリティレベルが異なるさまざまなアプローチがありますが、PythonとJavaScriptはどちらも動的に型付けされた言語です。
Cは、プログラマーがメモリ内の任意の場所からデータを読み取れるようにするか、プログラムのクラッシュにつながる意味がなくても、あるタイプの値が別のタイプであると想像できるようにします。
Haskellでは、これの前に明示的な変換なしに整数と浮動小数点数を追加することはできません。 CとHaskellはどちらも静的に型付けされていますが、このような大きな違いはあります。
動的言語と静的言語には多くのバリエーションがあります。 「Xに関しては静的言語は動的言語よりも優れている」という形式の無条件のステートメントは、ほとんどナンセンスです。 これは特定の言語の場合に当てはまるかもしれませんが、「HaskellはXに関してはPythonよりも優れている」と言う方が良いでしょう。
さまざまな静的型付けシステム
静的型付け言語の2つの有名な例を見てみましょう:GoとHaskell。 Go型付けシステムには、他の型の「パラメーター」を持つ型は一般化されていません。 たとえば、MyListリスト用に独自のタイプを作成し、必要なデータを保存できます。 MyListのソースコードを変更せずに、MyList整数、MyList文字列などを作成できるようにしたいと考えています。 コンパイラーは、次の分類に従う必要があります。整数のMyListがあり、誤ってそこに行を追加した場合、コンパイラーはプログラムを拒否する必要があります。
Goは、MyListなどのタイプを設定できないように特別に設計されました。 できる最善のことは、MyListの「空のインターフェイス」を作成することです。MyListにはオブジェクトを含めることができますが、コンパイラはそのタイプを認識しません。 MyListからオブジェクトを取得するときは、コンパイラーに型を伝える必要があります。 「ラインを取得しました」と言っても、実際には値が数値である場合、動的言語の場合のように、パフォーマンスエラーが発生します。
Goには、最新の静的型付け言語(または1970年代の一部のシステム)に存在する他の多くの機能もありません。 Goの作成者には、これらの決定に対する独自の理由がありましたが、この問題に関する外部の人々の意見は、時には厳しく聞こえる場合があります。
それでは、非常に強力な型システムを持つHaskellと比較しましょう。 タイプをMyListに設定すると、「数字のリスト」のタイプはMyList Integer
ます。 Haskellは、誤ってリストに文字列を追加させたり、リストの項目を文字列変数に入れないようにしています。
Haskellは、より複雑なアイデアを型で直接表現できます。 たとえば、 Num a => MyList a
は、「同じタイプの数字のMyList値」を意味します。 整数、浮動小数点数、または固定精度の10進数のリストを指定できますが、コンパイル中にチェックされる文字列のリストになることはありません。
任意の数値タイプで機能する追加関数を作成できます。 この関数の型はNum a => (a -> a -> a)
です。 これは次のことを意味します。
-
a
は任意の数値タイプを指定できます(Num a =>
)。 - この関数は、タイプ
a
2つの引数を取りa
タイプa
(a-a -> a -> a
)を返します。
最後の例。 関数の型がString -> String
場合、文字列を受け取り、文字列を返します。 ただし、 String -> IO String
場合、何らかの入力/出力も行います。 これは、ディスク、ネットワークへのアクセス、端末からの読み取りなどです。
型の関数にIO がない場合、I / O操作を実行しないことがわかります。 たとえば、Webアプリケーションでは、関数の種類を調べるだけで、関数がデータベースを変更するかどうかを理解できます。 これを行うには、動的言語も静的言語もほとんどありません。 これは、最も強力なタイピングシステムを備えた言語の機能です。
ほとんどの言語では、関数とそこから呼び出されるすべての関数などを処理して、データベースを変更する何かを見つけようとする必要があります。 これは、間違いを犯しやすい退屈なプロセスです。 そして、Haskell型システムはこの質問に簡単かつ確実に答えることができます。
この力をGoと比較してください。GoはMyListの単純なアイデアを表現することができず、「2つの引数を取り、それらは両方とも数値で同じタイプで、入出力を行う関数」は言うまでもありません。
Goアプローチを使用すると、Goでプログラミングするためのツールを簡単に作成できます(特に、コンパイラの実装は簡単です)。 さらに、必要な概念が少なくなります。 これらの利点が重要な制限にどのように匹敵するかは、主観的な問題です。 ただし、HaskellはGoよりも学習が難しく、Haskellの型システムははるかに強力であり、Haskellはコンパイル時にさらに多くの種類のバグを防ぐことができると主張することはできません。
GoとHaskellは非常に異なる言語であるため、用語が正しく使用されていても、「静的言語」の1つのクラスにグループ化すると誤解を招く可能性があります。 実用的なセキュリティ上の利点を比較すると、GoはHaskellよりも動的言語に近いです。
一方、一部の動的言語は、一部の静的言語よりも安全です。 (Pythonは一般にCよりも安全であると考えられています)。 静的または動的言語をグループとして一般化する場合、言語間の膨大な数の違いを忘れないでください。
タイピングシステム機能の違いの具体例
より強力なタイピングシステムでは、より低いレベルで制限を指定できます。 以下にいくつか例を示しますが、構文があいまいな場合は、それらにこだわらないでください。
Goでは、「add関数は2つの整数を取り、整数を返します」と言うことができます。
func add(x int, y int) int { return x + y }
Haskellでは、「関数は任意の数値型を取り、同じ型の数値を返します」と言うことができます。
f :: Num a => a -> a -> a add xy = x + y
Idrisでは、「関数は2つの整数を取り、整数を返しますが、最初の引数は2番目の引数より小さくなければなりません」と言うことができます。
add : (x : Nat) -> (y : Nat) -> {auto smaller : LT xy} -> Nat add xy = x + y
最初の引数が2番目の引数よりも大きいadd 2 1
を呼び出そうとすると、コンパイラーはコンパイル時にプログラムを拒否します 。 最初の引数が2番目の引数より大きいプログラムを作成することはできません。 まれな言語にはこの機能があります。 ほとんどの言語では、このチェックは実行時に行われますif x >= y: raise SomeError()
ように記述します。
Haskellには、上記のIdrisの例のような型に相当するものはなく、GoにはHaskellの例またはIdrisの例に相当するものはありません。 , Idris , Haskell, Haskell , Go. , .
. , . . , .
- C (1972), Go (2009) : , . MyList, " ", " " .. " ". " " , , .
- Java (1995), C# (2000) : ,
MyList<String>
, . String, , . - Haskell (1990), Rust (2010), Swift (2014) : , , (ADTs), - ( , (traits) , ). Rust Swift , Haskell, (Mozilla Apple, ).
- Agda (2007), Idris (2011) : , ", y, y , x". "y , x" . y x, . , . , .
, , . — Go, , .
(Java C#) — , .
, Mozilla (Rust) Apple (Swift).
(Idris and Agda) , . .