Известно, что языки программирования бывают статические и динамические. В статических языках типы всех значений известны в момент компиляции. В результате компилятор может проверить, правильно ли используется значение, применима ли к нему та или иная операция. Узнавать об ошибке в момент компиляции приятнее, чем во время исполнения — меньше ошибок выйдет при тестировании и дойдет до пользователя. За это ошибок и ценят статические языки.
Но почему дело ограничивается только типами данных? Попробуем немного по-фантазировать, что еще мог бы делать компилятор.
Допустим, у нас есть константа для количества секунд в сутках:
Мы не стали перемножать числа на калькуляторе, чтобы при чтении кода было поянтно как число посчитано (а может просто не было под рукой калькулятора). Но программа не обязана при каждой инициализации перемножать эти числа заново! Пусть компилятор перемножит их сам и поставит готовое значение. Я подозреваю, что хороший компилятор так и сделает.
Переходим в область научной фантастики (как в предметной области, так и в С++).
Мы решили выпускать локализованную для жителей Марса версию нашей программы:
Функция secondsPerDay() полностью константная. Для откомпилированного кода она всегда возвращает одно и то же значение. Поэтому наш гипотетический компилятор в праве вычислить значение этой функции, и подставить его по месту вызова. А если мы потом добавим Венеру, забыв исправить secondsPerDay(), то при компиляции с locale = Venus сработает assert (при компиляции, а не при выполнении, как это бывает в жизни).
Вспомним псевдо-функцию sizeof(), известную еще со времен C. Эта конструкция выглядит как обычная функция, но на самом деле компилятор вычисляет количество байт, которое занимает в памяти аргумент функции. Во время выполнения никакого вызова функции уже не происходит. К области вычислений, производимых компилятором, можно также отнести шаблоны C++. Но более гибкие возможности почему-то не поддерживаются.
Рассмотрим пример по-интереснее. Напишем функцию, которая будет генерировать SQL — запрос на основании имени типа структуры, и ее полей:
Теперь наша функция уже не константная, но отдельные ее фрагменты — константны, и гипотетический компилятор выполняет целые циклы, заменяя их константыми значениями или последовательностями операторов.
И не говорите, что тут изобретено RTTI! RTTI это run time type information, она работает во время выполнения программы. Здесь информация о типе используется во время компиляции для выполнения компилятором константных инструкций.
Итак, получив структуру как результат функции мы наслаждаемся статической проверкой при обращении к именам структуры, в место того чтобы обращаться к полям записи по строковому представлению. Правда, если наша структура впрограмме не совпадет с действительными полями в базе данных, мы опять налетим на ошибку выполнения. Но по крайней мере код, от которого это зависит локализован в определении структуры. Для подстраховки мы можем написать такую же универсальную функцию, которая проверит соответствие базы данных заявленным типам, а если надо — выполнит реструктуризацию базы.
Любопытно, что приведенная задача решена как минимум в двух статических языках (в динамических она решается без проблем).
Втроенный в C# и VB.NET язык LINQ позволяет статически работать с базами данных. Многие возможности были реализованы вполне легально: лямбда выражения, анонимные типы, операторные формы методов. Но для сопряжения статически объявленных классов с таблицами и полями базы данных Microsoft применил магию восьмого уровня под названием «integration of SQL schema information into CLR metadata» [1].
Другой пример — библиотека HaskellDB [2,3]. Сам Хаскель, с его монадами, можно расценить как магию первого уровня. Но разработчикам понадобилась магия третьего уровня в виде нестандартного расширения языка под названием «расширяемые структуры Trex». И не смотря на это, для увязки с базой данных каждому полю струкуры приходится давать избыточное объявление. Пример объявления таблицы сдвумя полями:
Чтобы яснее представить пользу от наших новшеств, предлагаю еще примеры.
У нас есть две структуры:
Где-то мы решили скорировать данные из Order в Sales
Теперь мы можем добавлять в наши поля новые структуры не меняя функцию foo()! А если вычисление foo() станет невозможным из-за несовпадения полей мы получим ошибку компиляции.
Пример из области объектно-ориентированного дизайна:
В процессе компиляции shapeFactory() превратится в обычную функцию:
Ссылки:
1. LINQ: .NET Language-Integrated Query
2. Официальная страница проекта HaskellDB
3. Daan Leijen, Eric Meijer. Domain Specific Embedded Compilers. Статья, рассказывающая как устроен HaskellDB. Там же обсуждается проблема сопряжения SQL и языка программирования
Но почему дело ограничивается только типами данных? Попробуем немного по-фантазировать, что еще мог бы делать компилятор.
Допустим, у нас есть константа для количества секунд в сутках:
const int secondsPerDay = 24*60*60;
Мы не стали перемножать числа на калькуляторе, чтобы при чтении кода было поянтно как число посчитано (а может просто не было под рукой калькулятора). Но программа не обязана при каждой инициализации перемножать эти числа заново! Пусть компилятор перемножит их сам и поставит готовое значение. Я подозреваю, что хороший компилятор так и сделает.
Переходим в область научной фантастики (как в предметной области, так и в С++).
Мы решили выпускать локализованную для жителей Марса версию нашей программы:
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() полностью константная. Для откомпилированного кода она всегда возвращает одно и то же значение. Поэтому наш гипотетический компилятор в праве вычислить значение этой функции, и подставить его по месту вызова. А если мы потом добавим Венеру, забыв исправить secondsPerDay(), то при компиляции с locale = Venus сработает assert (при компиляции, а не при выполнении, как это бывает в жизни).
Вспомним псевдо-функцию sizeof(), известную еще со времен C. Эта конструкция выглядит как обычная функция, но на самом деле компилятор вычисляет количество байт, которое занимает в памяти аргумент функции. Во время выполнения никакого вызова функции уже не происходит. К области вычислений, производимых компилятором, можно также отнести шаблоны 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 это run time type information, она работает во время выполнения программы. Здесь информация о типе используется во время компиляции для выполнения компилятором константных инструкций.
Итак, получив структуру как результат функции мы наслаждаемся статической проверкой при обращении к именам структуры, в место того чтобы обращаться к полям записи по строковому представлению. Правда, если наша структура впрограмме не совпадет с действительными полями в базе данных, мы опять налетим на ошибку выполнения. Но по крайней мере код, от которого это зависит локализован в определении структуры. Для подстраховки мы можем написать такую же универсальную функцию, которая проверит соответствие базы данных заявленным типам, а если надо — выполнит реструктуризацию базы.
Любопытно, что приведенная задача решена как минимум в двух статических языках (в динамических она решается без проблем).
Втроенный в C# и VB.NET язык LINQ позволяет статически работать с базами данных. Многие возможности были реализованы вполне легально: лямбда выражения, анонимные типы, операторные формы методов. Но для сопряжения статически объявленных классов с таблицами и полями базы данных Microsoft применил магию восьмого уровня под названием «integration of SQL schema information into CLR metadata» [1].
Другой пример — библиотека HaskellDB [2,3]. Сам Хаскель, с его монадами, можно расценить как магию первого уровня. Но разработчикам понадобилась магия третьего уровня в виде нестандартного расширения языка под названием «расширяемые структуры Trex». И не смотря на это, для увязки с базой данных каждому полю струкуры приходится давать избыточное объявление. Пример объявления таблицы сдвумя полями:
students :: Table(name :: String, mark :: Char)
name :: r\name =>
Attr (name :: String | r) String
mark :: r\mark =>
Attr (mark :: Char | r) Char
Чтобы яснее представить пользу от наших новшеств, предлагаю еще примеры.
У нас есть две структуры:
struct Order{
Date date;
Client client;
Product product;
int qty;
Numeric cost;
};
struct Sales{
Product product;
int qty;
Numeric cost;
};
Где-то мы решили скорировать данные из Order в Sales
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 Language-Integrated Query
2. Официальная страница проекта HaskellDB
3. Daan Leijen, Eric Meijer. Domain Specific Embedded Compilers. Статья, рассказывающая как устроен HaskellDB. Там же обсуждается проблема сопряжения SQL и языка программирования