しかし、なぜ問題はデータ型のみに限定されるのでしょうか? 少し空想してみましょう、コンパイラが他に何ができるか。
日数の秒数の定数があるとします:
const int secondsPerDay = 24*60*60;
電卓で数字を掛け始めなかったので、コードを読むときに数字がどのようにカウントされたのかが明確になります(または、電卓で手元になかったのかもしれません)。 ただし、プログラムは、初期化ごとにこれらの数値を再度乗算する必要はありません! コンパイラーにそれらを乗算させ、完成した値を設定します。 優れたコンパイラーがそれを行うと思います。
サイエンスフィクションの分野に移ります(サブジェクトエリアとC ++の両方)。
火星の住民向けにローカライズされたプログラムのバージョンをリリースすることにしました。
enum Planet{ Earth, Mars } const Planet locale = Mars; int secondsPerDay() { if(locale == Earth) { return 24*60*60; } if(locale == Mars) { return 24*60*60+37*60+23; } assert(false, "unknown locale"); }
secondsPerDay()関数は完全に一定です。 コンパイルされたコードの場合、常に同じ値を返します。 したがって、我々の仮想コンパイラはこの関数の値を計算し、呼び出しの場所でそれを代入する権利を持っています。 そして、Venusを追加し、secondsPerDay()の修正を忘れると、locale = Venusでコンパイルするときに、assertが動作します(実行時ではなく、コンパイル時に、実際に発生します)。
C以来知られている擬似関数sizeof()を思い出してください。この構造は通常の関数のように見えますが、実際にはコンパイラーは関数の引数がメモリ内で取るバイト数を計算します。 実行時には、関数呼び出しは既に発生していません。 コンパイラには、C ++テンプレートも含めることができます。 しかし、何らかの理由で、より柔軟な機能はサポートされていません。
興味深い例を考えてみましょう。 構造タイプとそのフィールドの名前に基づいてSQLクエリを生成する関数を作成します。
// - , // - T fetchById<T>(Connection conn, int id){ // !!! assert(is_struct(T), name_of(T)+" is not struct"); assert(has_field(T, id), name_of(T)+ " doesn't contains field id"); string query = "SELECT "; // ! foreach (field f in T) { if(field.index != 0) { query += ", "; } query += field.name; } query += "FROM " + name_of(T) + " WHERE ID=?ID"; // ...................................................................................................................... // Record r, // C++ Java? T result = new T; // foreach (field f in T) { result[f] = r.getFieldValue<f.type>(f.name); } return result; }
現在、関数は定数ではありませんが、個々のフラグメントは定数であり、仮想コンパイラーはサイクル全体を実行し、それらを定数値またはステートメントのシーケンスに置き換えます。
ここでRTTIが発明されたとは言わないでください! RTTIは実行時の型情報であり、実行時に機能します。 ここで、型情報は、コンパイル時に定数命令を実行するためにコンパイル時に使用されます。
したがって、関数の結果として構造を受け取った後、文字列表現によってレコードフィールドにアクセスする場所で、構造名を参照するときに静的チェックをお楽しみください。 確かに、プログラムの構造がデータベースの実際のフィールドと一致しない場合、実行時エラーが再び発生します。 しかし、少なくともそれが依存するコードは構造定義でローカライズされています。 安全上の理由から、データベースが宣言された型と一致するかどうかを確認する同じ汎用関数を作成し、必要に応じてデータベースを再構築できます。
上記の問題が少なくとも2つの静的言語で解決されることは興味深いです(動的言語では問題なく解決できます)。
C#およびVB.NETに組み込まれているLINQにより、データベースを静的に操作できます。 ラムダ式、匿名型、メソッドの演算子形式など、多くの機能が非常に合法的に実装されました。 しかし、Microsoftは「SQLスキーマ情報のCLRメタデータへの統合」と呼ばれる第8レベルのマジックを使用して、静的に宣言されたクラスをテーブルおよびデータベースフィールドとペアにします[1]。
別の例は、HaskellDBライブラリ[2,3]です。 Haskell自身は、彼のモナドと共に、一流の魔法とみなすことができます。 しかし、開発者は、「Extensible Trex Structures」と呼ばれるカスタム言語拡張の形式の第3レベルの魔法を必要としていました。 そして、それにもかかわらず、データベースとリンクするために、構造の各フィールドは過度のアナウンスを与えなければなりません。 2つのフィールドを持つテーブルを宣言する例:
students :: Table(name :: String, mark :: Char)
name :: r\name =>
Attr (name :: String | r) String
mark :: r\mark =>
Attr (mark :: Char | r) Char
イノベーションの利点をよりよく説明するために、さらに例を示します。
次の2つの構造があります。
struct Order{ Date date; Client client; Product product; int qty; Numeric cost; }; struct Sales{ Product product; int qty; Numeric cost; };
どこかで注文から販売までデータをスケーリングすることにしました
void foo(Order o, Sales s){ foreach(field f in Sales){ s[f.name] = o[f.name]; } }
これで、関数foo()を変更せずにフィールドに新しい構造を追加できます! また、フィールドの不一致によりfoo()の計算が不可能になると、コンパイルエラーが発生します。
オブジェクト指向設計の分野の例:
class Shape{ .............................. }; class Square: public Shape{ public: Square(); }; class Circle: public Shape{ public: Square(); }; .................................... // , // , // Shape* shapeFactory(string shapeName) { foreach(type T in descendants(Shape)){ if(name_of(T) == shapeName){ return new T; } return null; }
コンパイル中に、shapeFactory()は通常の関数に変わります。
Shape* shapeFactory(string shapeName) { if("Square" == shapeName){ return new Square; } if("Circle" == shapeName){ return new Circle; } return null; }
参照:
1. LINQ:.NET言語統合クエリ
2. HaskellDBの公式プロジェクトページ
3. Daan Leijen、Eric Meijer。 ドメイン固有の組み込みコンパイラ。 HaskellDBの仕組みを説明する記事。 また、SQLとプログラミング言語のペアリングの問題についても説明しています。