プログラミングにおける型推論

アルゴリズムを記述するとき、同じ数の引数を使用して、ただし一致しない型でいくつかの関数を呼び出す必要がある場合に、状況がよく発生します。 決めます。



累積C ++の例:



#include <iostream> #include <string.h> using namespace std; void a(int x) { cout<<"number\n"; } void a(float x) { cout<<"number\n"; } void a(const char *x) { cout<<"string\n"; } template <typename T> void f(void(*g)(T x), T x) { g(x); } int main() { f(a, 1); f(a, 1.0f); f(a, "Alexey"); return 0; }
      
      





同じJavascriptコード:



 function f(g, x) { g(x) } function a(x) { alert(typeof x) } f(a, 1) f(a, 1.0) f(a, 'Alexey')
      
      





明らかに、javascriptコードの方が成功しています。 これが起こる理由を理解しましょう。



動的アプローチ



javascriptでは、各変数には「type」プロパティがあり、typeof演算子が変数に作用すると返されます。 このプロパティは、変数が存在するたびに実行時に保存されます。 そして、演算子が変数に適用されると、この演算子を実装する関数は変数の型をチェックします。 タイプに応じて、いくつかのアクションが実行されます。 たとえば、2 + 2は4を返し、2 + 'Alexey'は '2Alexey'を返します。 つまり 言語演算子はほとんどの場合、適用する変数のタイプをチェックする必要があります。 このアプローチを「動的型付け」と呼びます。



このアプローチの欠点は非常に明白であり、1つマイナスであるため、プログラムの実行中に追加の計算を行う必要があります。



プラスは、明示的な型の概念がなく、一連のアクションのみがあり、読みやすさにプラスの影響を与えるコードです。



もう1つのプラスは、ロジックの重要な部分を複製する必要のないコードです。 これは上記の例でわずかに見えます-関数fは一度実装されますが、その実行結果は、さまざまなロジックを実装する、それに渡される関数に依存します。



静的アプローチ



C言語では、変数のタイプを指定する必要があり、変数の変更は禁止されています。 その結果、プログラムの実行中に型チェックは行われず、演算子は変数に一意に適用されます。 しかし、美しい一般化メカニズムはありません。 内容:ユニバーサルポインターvoid *、別の関数へのポインターを関数に渡す機能。 何らかの形でC言語の拡張機能であるC ++言語では、関数多型(オーバーロード)やテンプレートなどのメカニズムが登場しました。



関数ポリモーフィズムを使用すると、同じことをするが異なるデータ型で同じ名前の2つの関数に名前を付けることができます。



テンプレートは、すべてのタイプに共通の新しいタイプを導入します。 これらのメカニズムは、プログラマーの関与なしで、関数が必要なタイプに置き換えられるという点で優れています。 つまり 1つの関数のテンプレートが2つの異なるタイプを使用する場合、コンパイル中に2つの異なる関数が作成されます。 コード内の元の関数を呼び出すと、目的のタイプのコピーされた関数の呼び出しがコンパイル済みファイルに置き換えられます。



このアプローチの利点:最大実行速度。

短所:実行に必要なメモリが増えます。



型推論



つまり、最大のパフォーマンスが必要で十分なメモリが必要な場合は静的アプローチが望ましいですが、たとえばC ++言語を選択すると、コードの可読性が失われます。 結局、くしゃみごとに、関数が呼び出されるタイプを示す必要があります。



たとえば、クイックソートコード:



 #include <iostream> #include <string.h> using namespace std; template<class T> void qsort(T** array, int length, int compare(T *a, T *b)) { int left = 0; int right = length-1; T *middle_element; middle_element=array[length/2]; do { while( compare(array[left], middle_element)<0 ) left++; while( compare(array[right], middle_element)>0 ) right--; if(left<=right) { swap(array[left], array[right]); left++; right--; } } while(left<=right); if(right>0) qsort(array, right, compare); if(left<length) qsort(array+left, length-left, compare); } int cmp(char *a, char *b) { return strcmp(a, b); } int main() { char *strings[]={"Alexey", "Borisenko"}; qsort(strings, 2, cmp); return 0; }
      
      





型を完全に削除すると、読みやすさが向上します。



 #include <iostream> #include <string.h> using namespace std; qsort(array, length, compare) { left = 0; right = length-1; middle_element = array[length/2]; do { while( compare(array[left], middle_element)<0 ) left++; while( compare(array[right], middle_element)>0 ) right--; if(left<=right) { swap(array[left], array[right]); left++; right--; } } while(left<=right); if(right>0) qsort(array, right, compare); if(left<length) qsort(array+left, length-left, compare); } cmp(a, b) { return strcmp(a, b); } main() { strings={"Alexey", "Borisenko"}; qsort(strings, 2, cmp); return 0; }
      
      





これらの考慮事項から、タイプを放棄することは良いアイデアであると結論付けられました。 これを実装する方法について説明しましょう。



主に3つのタイプがあります。



1)シンプル

2)化合物

3)機能



他の型がネストされていないすべての型(文字列、数値、配列など)を単純型と呼びます。 異種の配列、構造などの複合構造。 関数は、それ自体が型である引数を持っているため、独立しています。



そのため、次の操作中に型推論が実行されます。



1)割り当て

2)関数呼び出し



型が推定されると、変数は型を変更できません。 反対の証拠:



 f(a) { x=1 if(a<2) x='Alexey' return x }
      
      





このコードでは、関数fの戻り値の型を調べるためにコードを実行する必要がありますが、これは静的アプローチと矛盾しています。



これは、プログラムのコンパイル中に各割り当てまたは関数呼び出しで型をチェックする必要があることを意味します。



式からの型推論は、単項演算子と二項演算子の両方が特定の型のセットで動作するため、非常に明確に行われます。 したがって、2種類のintを追加するときの演算子「+」は結果intを返します。 問題は、異なるタイプの変数に2項演算子を適用する場合に発生します。 intをfloatに追加すると、intまたはfloatのいずれかになる可能性があります。または、精度が失われる可能性があるため、まったくエラーになる可能性があります。



したがって、変数に値を割り当てるとき、変数の型と表示された式の型の一致がチェックされます。 これにより、別の関数を入力として使用する関数呼び出しを持たないコードの型を推定できます。 そのような呼び出しがある場合は、どの関数を引数として置き換えるかを把握する必要があります。



主なタイプの派生についてさらに詳しく考えてみましょう。



残りは明らかにシンプルです。 化合物は、以下を割り当てることにより導出されます。



 x={name:"Alexey"}
      
      





また、この型で使用されるすべての変数を示す関数の引数の型の説明を通じて:



 f(user {name}) { }
      
      





関数の出力に関する問題は、引数として別の関数を取り、関数を返すことができることです。 この場合、ネストされた関数の型を再帰的に推測し、その後で引数の型を推測する必要があります。



終了



書かれたすべてに基づいて、この記事の著者である私は、動的であり、時間が経つにつれて静的になるプログラミング言語を作成します。 関数の最初のレベルでの型推論は既にサポートされています(クロージャーを使用しません)。



All Articles