pythonを数日間使用していますが、動的型と静的型の違いを理解していると思います。私が理解していないのは、どのような状況が望ましいかということです。それは柔軟性と可読性は向上しますが、実行時チェックと追加の必要な単体テストが増加します。
柔軟性や読みやすさなどの機能以外の基準以外に、動的型付けを選択する理由は何ですか?他の方法では不可能な動的タイピングで何ができますか?動的型付けの具体的な利点を示す具体的なコード例はどれですか。
特定の例を求めたので、それをあげます。
Rob Coneryの Massive ORMは400行のコードです。 RobはSQLテーブルをミラーリングするために多くの静的型を必要とせずにSQLテーブルをマップし、オブジェクトの結果を提供できるため、それは非常に小さいです。これは、C#でdynamic
データ型を使用して実現されます。 RobのWebページはこのプロセスを詳細に説明していますが、この特定の使用例では、動的型付けがコードの簡潔さの主な原因であることは明らかです。
静的型を使用するSam Saffronの Dapper と比較してください。 SQLMapper
クラスだけでも3000行のコードです。
通常の免責事項が適用され、マイレージは異なる場合があることに注意してください。 DapperにはMassiveとは異なる目標があります。私はこれを、たぶんダイナミックなタイピングなしでは不可能だろう400行のコードでできることの例として指摘するだけです。
動的型付けを使用すると、型決定をランタイムに延期できます。以上です。
動的に型付けされた言語を使用する場合でも、静的に型付けされた言語を使用する場合でも、型の選択は賢明でなければなりません。文字列に数値データが含まれていない限り、2つの文字列を一緒に追加して数値の回答を期待することはありません。そうでない場合は、予期しない結果が得られます。静的に型付けされた言語では、最初からこれを行うことはできません。
静的型言語の擁護者は、コンパイラが1行を実行する前に、コンパイル時にコードのかなりの量の「健全性チェック」を実行できることを指摘しています。これはGood Thing™です。
C#にはdynamic
キーワードがあります。これにより、残りのコードで静的型の安全性の利点を失うことなく、型の決定を実行時まで延期できます。型推論(var
)は、常に型を明示的に宣言する必要をなくすことにより、静的型付き言語で書くことの多くの苦痛を取り除きます。
動的言語は、プログラミングへのよりインタラクティブで即時のアプローチを好むようです。クラスを作成してコンパイルサイクルを実行し、LISPコードを少し入力して実行されるのを観察する必要があるとは誰も予想していません。それでも、それはまさにC#で行うことが期待されていることです。
「静的型付け」や「動的型付け」などのフレーズがよく使われ、人々は微妙に異なる定義を使用する傾向があるので、私たちの意味を明確にすることから始めましょう。
コンパイル時にチェックされる静的型を持つ言語を考えてみましょう。しかし、型エラーは致命的でない警告のみを生成し、実行時にはすべてがダック型であると言います。これらの静的型は、プログラマーの便宜のためだけであり、codegenには影響しません。これは、静的型付け自体は制限を課さず、動的型付けと相互に排他的ではないことを示しています。 (Objective-Cはこのようなものです。)
しかし、ほとんどの静的型システムはこのように動作しません。制限を課す可能性のある静的型システムの2つの一般的なプロパティがあります。
多くのタイプセーフプログラムには必ず静的タイプエラーが含まれるため、これは制限事項です。
たとえば、Pythonスクリプトは、Python 2とPython 3の両方として実行する必要があります。 Python 2と3の間のパラメータータイプなので、次のようなコードがあります。
if sys.version_info[0] == 2:
wfile.write(txt)
else:
wfile.write(bytes(txt, 'utf-8'))
A Python 2 static type checkerは、実行されない場合でも、Python 3コード(およびその逆)を拒否します。タイプセーフプログラムには、静的型エラー。
別の例として、OS X 10.6で実行したいが10.7の新機能を利用したいMacプログラムを考えてみましょう。 10.7メソッドは実行時に存在する場合と存在しない場合があり、それらを検出するのはプログラマーの責任です。静的型チェッカーは、型の安全性を確保するためにプログラムを拒否するか、実行時に型エラー(関数がない)が発生する可能性があるプログラムを受け入れるように強制されます。
静的型チェックは、ランタイム環境がコンパイル時情報によって適切に記述されていることを前提としています。しかし、将来を予測することは危険です。
もう1つ制限があります。
静的タイプが「正しい」と仮定すると、最適化の機会が多く提供されますが、これらの最適化には制限がある場合があります。良い例は、プロキシオブジェクトです。リモート。メソッドの呼び出しを別のプロセスの実際のオブジェクトに転送するローカルプロキシオブジェクトが必要だとします。プロキシが汎用的で(オブジェクトとして偽装できる)かつ透過的である(既存のコードがプロキシと通信していることを既存のコードが知る必要がないようにする)なら、いいでしょう。しかし、これを行うために、コンパイラーは静的型が正しいと想定するコードを生成できません。オブジェクトが実際にプロキシである場合は失敗するため、静的にメソッド呼び出しをインライン化する。
実際のそのようなリモート処理の例には、ObjCの NSXPCConnection またはC#のTransparentProxy(実装にランタイムでいくつかの悲観化が必要でした-詳細は こちら を参照)が含まれます。
Codegenが静的型に依存しておらず、メッセージ転送などの機能がある場合、プロキシオブジェクトやデバッグなどを使用して多くの優れた機能を実行できます。
これは、型チェッカーを満たす必要がない場合に実行できるいくつかのサンプルです。制限は静的型によって課せられるのではなく、強制静的型チェックによって課せられます。
ダック型変数は誰もが最初に考えるものですが、ほとんどの場合、静的型推論によって同じ利点を得ることができます。
しかし、動的に作成されたコレクションをダックタイピングすることは、他の方法では実現が困難です。
>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'
したがって、どのタイプがJSON.parse
戻る?整数の配列または文字列の辞書の辞書?いいえ、それでも十分ではありません。
JSON.parse
は、null、bool、float、string、これらのタイプの配列のいずれかを再帰的に、または文字列からこれらのタイプのいずれかに辞書を再帰的に返すことができる、ある種の「バリアント値」を返す必要があります。動的型付けの主な長所は、このようなバリアント型があることです。
これまでのところ、これは動的型付け言語ではなく、動的typesの利点です。まともな静的言語は、そのようなタイプを完全にシミュレートできます。 (そして、「悪い」言語でさえ、内部でタイプセーフを壊したり、不格好なアクセス構文を要求したりすることで、それらをしばしばシミュレートできます。)
動的型付け言語の利点は、そのような型を静的型推論システムで推論できないことです。タイプを明示的に記述する必要があります。ただし、このような多くの場合(これを含む)、型を記述するコードは、型を記述せずにオブジェクトを解析/構築するコードとまったく同じくらい複雑なので、必ずしも利点とは限りません。
すべてのリモートで実用的な静的型システムは、関係するプログラミング言語と比較して厳しく制限されているため、コードが実行時にチェックできるすべての不変条件を表現することはできません。型システムが提供しようとする保証を回避しないために、それは保守的であり、これらのチェックに合格するが(型システムでは)証明できないユースケースを許可しないことを選択します。
例を挙げましょう。モデルがFoo型のオブジェクトの属性x
が整数を保持していると言う場合、静的に型指定されているデータオブジェクトやそのコレクションなどを記述する単純なデータモデルを実装するとします。常に整数を保持します。これはランタイム構成なので、静的に入力することはできません。 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)
十分なケースがあれば、特定の動的型システムを実装できます。
逆に、静的に型付けされたプログラムを同等の動的なプログラムに変換することもできます。もちろん、静的型付け言語が提供する正確さのコンパイル時の保証はすべて失われます。
編集:これをシンプルにしたかったのですが、ここにオブジェクトモデルの詳細があります
関数はDataのリストを引数として取り、ImplMonadで副作用のある計算を実行し、Dataを返します。
type Function = [Data] -> ImplMonad Data
DMember
は、メンバー値または関数です。
data DMember = DMemValue Data | DMemFunction Function
Data
を拡張して、オブジェクトと関数を含めます。オブジェクトは名前付きメンバーのリストです。
data Data = .... | DObject [(String, DMember)] | DFunction Function
これらの静的型は、私がよく知っている動的に型付けされたすべてのオブジェクトシステムを実装するのに十分です。
メンブレン :
メンブレンは、単一オブジェクトのラッパーとは対照的に、オブジェクトグラフ全体のラッパーです。通常、メンブレンの作成者は、1つのオブジェクトだけをメンブレンに包み始めます。重要なアイデアは、膜を横切るオブジェクト参照自体が、同じ膜に推移的に包まれるということです。
各タイプは、同じインターフェースを持つタイプでラップされますが、メッセージをインターセプトし、値がメンブレンを通過するときにラップおよびアンラップします。お気に入りの静的型付け言語のラップ関数の型は何ですか?たぶんHaskellはその関数のための型を持っているかもしれませんが、ほとんどの静的型付けされた言語はそうではないか、オブジェクト→オブジェクトを使用して、型チェッカーとしての彼らの責任を事実上放棄します。
誰かが言ったように、理論的には、自分で特定のメカニズムを実装する場合、静的型付けではできなかった動的型付けでできることはほとんどありません。ほとんどの言語は、voidポインターのような型の柔軟性、およびルートオブジェクト型または空のインターフェイスをサポートする型緩和メカニズムを提供します。
より良い質問は、動的タイピングが特定の状況や問題でより適切で適切である理由です。
まず、定義しましょう
Entity-コードにあるエンティティの一般的な概念が必要です。プリミティブ番号から複雑なデータまで何でもかまいません。
Behavior-エンティティに、外部の世界がエンティティに特定の反応を指示できるようにするいくつかの状態と一連のメソッドがあるとします。このエンティティの状態+インターフェースをその動作と呼びましょう。 1つのエンティティは、複数の動作を、言語が提供するツールによって特定の方法で組み合わせることができます。
エンティティとその動作の定義-すべての言語は、プログラム内の特定のエンティティの動作(メソッドのセット+内部状態)を定義するのに役立つ抽象化の手段を提供します。 これらのビヘイビアーに名前を割り当て、このビヘイビアーを持つすべてのインスタンスが特定のタイプであると言うことができます。
これはおそらく、あまり知られていないことではありません。そして、あなたが言ったように、あなたは違いを理解しました、しかしそれでも。おそらく完全で最も正確な説明ではありませんが、いくつかの価値をもたらすのに十分楽しいと思います:)
静的型付け-コードの実行が開始される前に、プログラム内のすべてのエンティティの動作がコンパイル時に検査されます。つまり、たとえばPersonタイプのエンティティにMagicianの動作(動作)を持たせたい場合は、エンティティMagicianPersonを定義して、throwMagic()のような魔術師の動作を与える必要があります。コード内にいる場合、通常のPerson.throwMagic()コンパイラに誤って"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".
動的型付け-動的型付け環境では、特定のエンティティで実際に何かをしようとするまで、エンティティの利用可能な動作はチェックされません。 Ruby Person.throwMagic()を要求するコードを実行することは、コードが実際にそこに来るまでキャッチされません。これはイライラするように聞こえますね。しかし、啓示のようにも聞こえます。これに基づいてプロパティでは、面白いことを実行できます。たとえば、コードの特定のポイントに到達するまでは、何でもMagicianになり、誰がそうなるのかわからないゲームを設計するとします。次に、カエルが来て、 HeyYouConcreteInstanceOfFrog.include Magic
それ以降、このカエルは魔法の力を持つ特定のカエルになります。他のカエルはまだです。静的タイピング言語では、動作の組み合わせの標準的な平均値(インターフェース実装など)によってこの関係を定義する必要があります。動的タイピング言語では、実行時にそれを行うことができ、誰も気にしません。
ほとんどの動的タイピング言語には、インターフェースに渡されるメッセージをキャッチする一般的な動作を提供するメカニズムがあります。例Ruby method_missing
およびPHP __call
よく覚えていれば。つまり、プログラムの実行時にあらゆる種類の興味深いことを実行し、現在のプログラムの状態に基づいて型の決定を行うことができます。これにより、Javaなどの保守的な静的プログラミング言語よりもはるかに柔軟な問題をモデリングするためのツールが提供されます。