私は数日前からPythonを使用していますが、動的型付けと静的型付けの違いを理解していると思います。私が理解していないのは、どのような状況下でそれが好ましいかということです。柔軟性があり読みやすいですが、実行時のチェックが多くなり、ユニットテストが必要になります。
柔軟性や読みやすさなどの非機能的な基準とは別に、動的型付けを選択する理由は何ですか?そうでなければ不可能な動的型付けで何ができますか?動的型付けの具体的な利点を示す具体的なコード例は何ですか?
私は数日前からPythonを使用していますが、動的型付けと静的型付けの違いを理解していると思います。私が理解していないのは、どのような状況下でそれが好ましいかということです。柔軟性があり読みやすいですが、実行時のチェックが多くなり、ユニットテストが必要になります。
柔軟性や読みやすさなどの非機能的な基準とは別に、動的型付けを選択する理由は何ですか?そうでなければ不可能な動的型付けで何ができますか?動的型付けの具体的な利点を示す具体的なコード例は何ですか?
回答:
あなたは特定の例を求めたので、私はあなたにそれをあげます。
Rob ConeryのMassive ORMは400行のコードです。Robは、SQLテーブルをミラーリングするために多くの静的型を必要とせずに、SQLテーブルをマップし、オブジェクトの結果を提供できるため、これほど小さいです。これはdynamic
、C#のデータ型を使用して実現されます。RobのWebページでは、このプロセスについて詳しく説明していますが、この特定のユースケースでは、動的型付けがコードの簡潔さの大部分を担っていることは明らかです。
静的型を使用するSam SaffronのDapperと比較してください。SQLMapper
単独クラスは、コードの3000行です。
通常の免責事項が適用されることに注意してください。また、走行距離は異なる場合があります。DapperにはMassiveとは異なる目標があります。これは、おそらく動的なタイピングなしでは不可能な400行のコードでできることの例として指摘しています。
動的型付けを使用すると、型の決定を実行時まで延期できます。 それで全部です。
動的に型付けされた言語を使用する場合も、静的に型付けされた言語を使用する場合も、型の選択は賢明でなければなりません。文字列に数値データが含まれていない限り、2つの文字列を一緒に追加して数値の回答を期待することはありません。含まれない場合は、予期しない結果が得られます。静的に型付けされた言語では、そもそもこれを行うことはできません。
静的型言語の支持者は、コンパイラーが、コンパイル時に、単一行が実行される前に、かなりの量のコードを「健全性検査」できると指摘しています。これはGood Thing™です。
C#にはdynamic
キーワードがあり、これにより、コードの残りの部分で静的型安全性の利点を失うことなく、型決定を実行時まで延期できます。型推論(var
)は、型を常に明示的に宣言する必要性をなくすことにより、静的に型付けされた言語で書く苦労の多くを取り除きます。
動的言語は、プログラミングに対するよりインタラクティブで即時的なアプローチを好むようです。クラスを作成し、コンパイルサイクルを経てLispコードを少し入力して実行を監視する必要があるとは誰も期待していません。しかし、それはまさに私がC#で行うことを期待していることです。
「静的な型付け」や「動的な型付け」などのフレーズが頻繁に使われ、人々は微妙に異なる定義を使用する傾向があるため、まずは意味を明確にすることから始めましょう。
コンパイル時にチェックされる静的型を持つ言語を考えてください。しかし、型エラーは致命的でない警告のみを生成し、実行時にはすべてがダック型であると言います。これらの静的型はプログラマーの便宜のためだけのものであり、コード生成には影響しません。これは、静的型付け自体が制限を課すものではなく、動的型付けと相互排他的ではないことを示しています。(Objective-Cはこのようなものです。)
しかし、ほとんどの静的型システムはこのように動作しません。制限を課すことができる静的型システムの2つの一般的なプロパティがあります。
多くのタイプセーフプログラムには必ず静的タイプエラーが含まれるため、これは制限です。
たとえば、Python 2とPython 3の両方として実行する必要があるPythonスクリプトがあります。一部の関数は、Python 2と3の間でパラメータータイプを変更したため、次のようなコードがあります。
if sys.version_info[0] == 2:
wfile.write(txt)
else:
wfile.write(bytes(txt, 'utf-8'))
Python 2静的型チェッカーは、実行されない場合でもPython 3コードを拒否します(逆も同様です)。タイプセーフプログラムに静的タイプエラーが含まれています。
別の例として、OS X 10.6で実行したいが、10.7の新機能を利用したいMacプログラムを考えてみましょう。10.7メソッドは実行時に存在する場合と存在しない場合があり、それらを検出するのはプログラマーです。静的型チェッカーは、型安全性を確保するためにプログラムを拒否するか、実行時に型エラー(関数の欠落)を生成する可能性があるプログラムを受け入れるように強制されます。
静的型チェックは、ランタイム環境がコンパイル時情報によって適切に記述されていることを前提としています。しかし、未来を予測することは危険です!
もう1つの制限があります。
静的型が「正しい」と仮定すると、最適化の機会が多く提供されますが、これらの最適化は制限される場合があります。良い例は、リモートオブジェクトなどのプロキシオブジェクトです。メソッド呼び出しを別のプロセスの実際のオブジェクトに転送するローカルプロキシオブジェクトが必要だとします。プロキシが汎用(オブジェクトのようにマスカレードできる)で透過的(既存のコードがプロキシと通信していることを知る必要がないように)できれば便利です。しかし、これを行うには、オブジェクトが実際にプロキシである場合に失敗するため、メソッド呼び出しを静的にインライン化するなどして、静的型が正しいと仮定するコードをコンパイラが生成できません。
そのようなリモート処理の例には、ObjCのNSXPCConnectionまたはC#のTransparentProxy(ランタイムでいくつかのペシミゼーションが必要でした- 議論についてはこちらを参照)が含まれます。
codegenが静的タイプに依存せず、メッセージ転送などの機能がある場合、プロキシオブジェクトやデバッグなどを使用して多くのクールなことができます。
したがって、これは、型チェッカーを満たす必要がない場合にできることの一部のサンプリングです。制限は静的型によって課せられるのではなく、強制的な静的型チェックによって課されます。
A Python 2 static type checker would reject the Python 3 code (and vice versa), even though it would never be executed. My type safe program contains a static type error.
妥当な静的言語であればIFDEF
、どちらの場合も型の安全性を維持しながら、型プリプロセッサステートメントでこれを行うことができます。
ダック型変数は誰もが最初に考えるものですが、ほとんどの場合、静的型推論を通じて同じ利点を得ることができます。
しかし、動的に作成されたコレクションでのダックタイピングは、他の方法では実現が困難です。
>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'
では、どの型がJSON.parse
返されますか?整数の配列または文字列の辞書の辞書?いいえ、それでも十分ではありません。
JSON.parse
null、bool、float、string、これらの型の配列を再帰的に、または文字列からこれらの型のいずれかを再帰的に辞書に格納できる「バリアント値」を返す必要があります。動的型付けの主な長所は、そのようなバリアント型を持つことです。
これまでのところ、これは動的型言語ではなく、動的型の利点です。適切な静的言語は、そのような型を完全にシミュレートできます。(そして、「悪い」言語でさえ、ボンネットの下で型安全性を破る、および/または不器用なアクセス構文を要求することによって、しばしばそれらをシミュレートできます。)
動的に型付けされた言語の利点は、静的型推論システムによってそのような型を推論できないことです。型を明示的に記述する必要があります。しかし、このような1回を含む多くの場合、型を記述するコードは、型を記述せずにオブジェクトを解析/構築するコードとまったく同じくらい複雑であるため、必ずしも利点ではありません。
すべてのリモートで実用的な静的型システムは、関係するプログラミング言語に比べて非常に制限されているため、実行時にコードがチェックできるすべての不変式を表現することはできません。型システムが提供しようとする保証を回避しないために、保守的なものを選択し、これらのチェックに合格するが、型システムでは証明できないユースケースを許可しないことを選択します。
例を挙げましょう。モデルがx
Foo型のオブジェクトの属性が整数を保持していると言う場合、モデルは常に整数を保持する必要があるという意味で静的に型指定されたデータオブジェクト、それらのコレクションなどを記述する単純なデータモデルを実装するとします。これはランタイム構造であるため、静的に入力することはできません。YAMLファイルに記述されたデータを保存するとします。ハッシュマップ(後でYAMLライブラリに渡される)をx
作成し、属性を取得し、マップに保存し、偶然にも文字列である他の属性を取得します。今のタイプは何the_map[some_key]
ですか?よく、私たちはそれsome_key
がわかっている'x'
ので、結果は整数でなければなりませんが、型システムはこれについて推論することさえできません。
活発に研究されている型システムの中には、この特定の例で機能するものもありますが、これらは非常に複雑です(コンパイラライターの実装とプログラマの推論の両方)、特にこの「単純な」何か(つまり、段落)。
もちろん、今日の解決策はすべてをボックス化してからキャストすることです(または、多数のオーバーライドされたメソッドを持ち、そのほとんどが「実装されていない」例外を発生させます)。しかし、これは静的に型付けされるのではなく、実行時に型チェックを行う型システムのハックです。
静的に型付けされた言語の上に動的な型付けを実装できるため、静的な型付けではできない動的な型付けでできることは何もありません。
Haskellの短い例:
data Data = DString String | DInt Int | DDouble Double
-- defining a '+' operator here, with explicit promotion behavior
DString a + DString b = DString (a ++ b)
DString a + DInt b = DString (a ++ show b)
DString a + DDouble b = DString (a ++ show b)
DInt a + DString b = DString (show a ++ b)
DInt a + DInt b = DInt (a + b)
DInt a + DDouble b = DDouble (fromIntegral a + b)
DDouble a + DString b = DString (show a ++ b)
DDouble a + DInt b = DDouble (a + fromIntegral b)
DDouble a + DDouble b = DDouble (a + b)
十分なケースがあれば、任意の動的型システムを実装できます。
逆に、静的に型付けされたプログラムを同等の動的なプログラムに変換することもできます。もちろん、静的に型付けされた言語が提供する正確性のコンパイル時の保証はすべて失われます。
編集:これをシンプルにしたかったのですが、オブジェクトモデルの詳細を以下に示します
関数はデータのリストを引数として受け取り、ImplMonadで副作用を伴う計算を実行し、データを返します。
type Function = [Data] -> ImplMonad Data
DMember
メンバー値または関数のいずれかです。
data DMember = DMemValue Data | DMemFunction Function
Data
オブジェクトと関数を含めるように拡張します。オブジェクトは、名前付きメンバーのリストです。
data Data = .... | DObject [(String, DMember)] | DFunction Function
これらの静的な型は、私がよく知っているすべての動的に型付けされたオブジェクトシステムを実装するのに十分です。
Data
。
+
、2つのData
値を別のData
値に結合する新しい演算子を定義しました。 Data
動的型システムの標準値を表します。
膜:
膜は、単一のオブジェクトのラッパーではなく、オブジェクトグラフ全体のラッパーです。通常、膜の作成者は、膜内の単一のオブジェクトのみをラップすることから始めます。重要な考え方は、膜を通過するオブジェクト参照自体が同じ膜に推移的にラップされるということです。
各タイプは、同じインターフェースを持つタイプでラップされますが、メッセージをインターセプトし、膜を通過するときに値をラップおよびアンラップします。お気に入りの静的型付け言語のラップ関数の型は何ですか?Haskellにはその関数の型があるかもしれませんが、ほとんどの静的に型付けされた言語にはないか、Object→Objectを使用することになり、型チェッカーとしての責任を事実上放棄します。
class Foo a where ...
data Wrapper = forall a. Foo a => Wrapper a
String
、Javaの具象型であるため、ラップできません。Smalltalkには入力を試みないため、この問題はありません#doesNotUnderstand
。
誰かが言ったように、理論的には、特定のメカニズムを独自に実装する場合、静的型付けではできなかった動的型付けでできることはほとんどありません。ほとんどの言語は、ボイドポインター、ルートオブジェクト型、または空のインターフェイスなどの型の柔軟性をサポートする型緩和メカニズムを提供します。
より良い質問は、特定の状況や問題で動的型付けがより適切で適切な理由です。
まず、定義します
エンティティ -コード内のエンティティの一般的な概念が必要です。原始数から複雑なデータまで何でもかまいません。
振る舞い -エンティティに何らかの状態と、外部の世界が特定の反応をエンティティに指示できる一連のメソッドがあるとしましょう。このエンティティの状態+インターフェースをその動作と呼びましょう。1つのエンティティは、ツール言語が提供する特定の方法で組み合わされた複数の動作を持つことができます。
エンティティとその動作の定義 -すべての言語は、プログラム内の特定のエンティティの動作(メソッドのセット+内部状態)を定義するのに役立つ抽象化の手段を提供します。これらの動作に名前を割り当てて、この動作を持つすべてのインスタンスが特定のタイプであると言うことができます。
これはおそらくそれほど馴染みのないものではないでしょう。あなたが言ったように、あなたは違いを理解しましたが、それでもです。おそらく完全ではなく最も正確な説明ではありませんが、何らかの価値をもたらすのに十分な楽しみを願っています:)
静的型付け -プログラム内のすべてのエンティティの動作は、コードの実行開始前にコンパイル時に検査されます。これは、たとえばPerson型のエンティティにMagicianのような動作をさせる場合(動作させる)、MagicianPersonエンティティを定義し、throwMagic()のようなマジシャンの動作を与える必要があることを意味します。あなたのコードの中で、誤って普通のPerson.throwMagic()コンパイラに伝えると、"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".
動的な型付け -動的な型付け環境では、特定のエンティティで何かを実際に試行するまで、エンティティの使用可能な動作はチェックされません。Person.throwMagic()を要求するRubyコードの実行は、コードが実際に来るまでキャッチされません。これはいらいらしますね。しかし、それは啓示にも聞こえます。このプロパティに基づいて、面白いことができます。たとえば、何でもマジシャンに頼ることができるゲームを設計し、コードの特定のポイントに到達するまで誰がそれをするのか本当にわからない、としましょう。そして、カエルが来て、あなたが言うHeyYouConcreteInstanceOfFrog.include Magic
その後、このカエルは魔法の力を持つ特定のカエルになります。他のカエル、まだありません。静的型付け言語では、動作の組み合わせの標準的な平均(インターフェイスの実装など)によってこの関係を定義する必要があります。動的型付け言語では、実行時にそれを行うことができ、誰も気にしません。
ほとんどの動的型付け言語には、インターフェイスに渡されるメッセージをキャッチする一般的な動作を提供するメカニズムがあります。たとえば、Ruby method_missing
とPHP __call
を思い出すといいでしょう。つまり、プログラムの実行時にあらゆる種類の興味深いことを実行し、現在のプログラムの状態に基づいてタイプを決定できるということです。これにより、たとえばJavaのような保守的な静的プログラミング言語よりもはるかに柔軟な問題のモデリング用のツールが提供されます。