Записи в категории ·

Без рубрики

· Category...

TInvokeableVariantType

К написанию этого поста меня подтолкнула статья на хабре про написание ORM для Delphi. Велосипед с квадратными колесами, конечно, но идея интересная. Хочу попробовать подойти к этому вопросу с другой стороны. Очень кратко.

В Delphi, начиная с версии 6 2007 (поправьте меня, если я ошибаюсь), в модуле Variants есть малоизвестный, но весьма занимательный класс TInvokeableVariantType.

Чтобы сэкономить время, я опишу совершенно абстрактный пример. Этого будет достаточно, чтобы продемонстрировать, что применить этот подход в работе над ORM или где-то еще достаточно просто. Здесь именно тот случай, когда придумать достойное применение намного сложнее, чем применить :)

В классе наследующем TInvokeableVariantType можно обрабатывать обращения к свойствам и методам динамически. То есть нет необходимости предварительно описывать интерфейс класса. В каких-то случаях это может помочь сэкономить немалое количество строк кода. Но любые плюсы тянут за собой и минусы — как минимум, IDE не будет иметь никакой информации о таком классе и подсказки в коде не будут работать.

Ниже простой класс-счетчик. Пусть он имеет поле Counter, с помощью которого можно получать или изменять значение счетчика, и два метода — процедуру Reset, которая сбрасывает счетчик в ноль, и функцию Pow2, возвращающую квадрат значения счетчика и не изменяющую сам счетчик.

  TVariantCounter = class(TInvokeableVariantType)
  private
    FCounter: Integer;
  public
    procedure Clear(var V: TVarData); override;

    function GetProperty(var Dest: TVarData; const V: TVarData;
      const Name: string): Boolean; override;
    function SetProperty(const V: TVarData; const Name: string;
      const Value: TVarData): Boolean; override;
    function DoFunction(var Dest: TVarData; const V: TVarData;
      const Name: string; const Arguments: TVarDataArray): Boolean; override;
    function DoProcedure(const V: TVarData; const Name: string;
      const Arguments: TVarDataArray): Boolean; override;
  end;

При взгляде на объявление класса многое становится ясно. В нем нет ни поля Counter, ни методов Reset и Pow2, но зато есть методы GetProperty, SetProperty, DoFunction и DoProcedure, в которые передаются все нужные имена и значения для динамической обработки.

Рассматриваемый пример очень прост, поэтому меньше слов — больше кода :)

function TVariantCounter.DoFunction(var Dest: TVarData; const V: TVarData;
  const Name: string; const Arguments: TVarDataArray): Boolean;
begin
  Result := False;

  if SameText(Name, 'Pow2') and (Length(Arguments) = 0) then
  begin
    Variant(Dest) := FCounter * FCounter;
    Result := True;
  end;
end;

function TVariantCounter.DoProcedure(const V: TVarData; const Name: string;
  const Arguments: TVarDataArray): Boolean;
begin
  Result := False;

  if SameText(Name, 'Reset') and (Length(Arguments) = 0) then
  begin
    FCounter := 0;
    Result := True;
  end;
end;

function TVariantCounter.GetProperty(var Dest: TVarData; const V: TVarData;
  const Name: string): Boolean;
begin
  if SameText(Name, 'Counter') then
  begin
    Variant(Dest) := FCounter;
    Result := True;
  end else
    Result := False;
end;

function TVariantCounter.SetProperty(const V: TVarData; const Name: string;
  const Value: TVarData): Boolean;
begin
  if SameText(Name, 'Counter') then
  begin
    FCounter := Variant(Value);
    Result := True;
  end else
    Result := False;
end;

Использовать TVariantCounter можно практически как обычный класс.

var
  Cntr: Variant;
begin
  Cntr := NewVariantCounter;
  Cntr.Reset;

  Memo1.Lines.Add(Cntr.Counter);
  Cntr.Counter := 25;
  Memo1.Lines.Add(Cntr.Counter);
  Memo1.Lines.Add(Cntr.Pow2);
  Cntr.Reset;
  Memo1.Lines.Add(Cntr.Counter);
end;

В результате в Memo появляется следующий текст:

0
25
625
0

Пока все радужно, но я ничего не сказал про функцию NewVariantCounter. В разговоре об инициализации придется упомянуть некоторые нюансы. Неприятность в том, что данный класс — синглтон. Со всеми вытекающими.

var
  VariantCounter: TVariantCounter;

function NewVariantCounter: Variant;
begin
  VarClear(Result);
  TVarData(Result).VType := VariantCounter.VarType;
end;

initialization
  VariantCounter := TVariantCounter.Create;

finalization
  VariantCounter.Free;

То есть в этом коде счетчик для всех экземпляров общий. А в этом мало смысла.

Если подходить серьезно, то TVariantCounter не должен быть счетчиком, он должен управлять счетчиками, а значения счетчика для каждого экземпляра можно хранить в структуре TVarData (ссылка на конкретный экземпляр TVarData в свою очередь передается в каждый из методов класса).

Определим свою собственную TVarData вот так:

TCounterVarData = packed record
  VType: TVarType;
  Reserved1, Reserved2, Reserved3: Word;
  CounterValue: PInteger;
  Reserved4: LongWord;
end;

PInteger вместо Integer здесь используется в связи с тем, что саму запись во время работы изменять нельзя. Поэтому пусть сам указатель остается постоянным, а изменяемую память мы выделим где-то еще.

Функцию NewVariantCounter придется изменить соответствующим образом:

function NewVariantCounter: Variant;
begin
  VarClear(Result);
  TCounterVarData(Result).VType := VariantCounter.VarType;

  New(TCounterVarData(Result).CounterValue);
  TCounterVarData(Result).CounterValue^ := 0;
end;

Изменения также коснутся и остального кода. Вот так:

function TVariantCounter.DoFunction(var Dest: TVarData; const V: TVarData;
  const Name: string; const Arguments: TVarDataArray): Boolean;
begin
  Result := False;

  if SameText(Name, 'Pow2') and (Length(Arguments) = 0) then
  begin
    Variant(Dest) := TCounterVarData(V).CounterValue^ * TCounterVarData(V).CounterValue^;
    Result := True;
  end;
end;

Остальные методы я опущу, так как здесь все в общем-то очевидно.

И вот тут становится ясно для чего классу метод Clear. Этот метод позволяет корректно финализировать структуру. В данном конкретном случае — освободить память выделенную для CounterValue.

procedure TVariantCounter.Clear(var V: TVarData);
begin
  Dispose(TCounterVarData(V).CounterValue);
  inherited;
end;

Пример очень простой, но сам механизм очень мощный. Если вернуться к тому с чего начался этот пост, то при разработке ORM, вместо CounterValue структура TVarData могла бы хранить ссылку на TDataSet и работать с полями этого датасета как со свойствами класса.

Полный код примера можно скачать тут.

Programming Languages

Курс очень понравился. Я не преувеличиваю, действительно очень понравился. Тема сама по себе интересна и 10 недель работы с Standard ML, Racket и Ruby под руководством Дэна Гроссмана — это незабываемый опыт. Рекомендую всем.

Собственно, один из моих недавних постов непосредственно связан с одним из домашних заданий этого курса.

Марко Канту переезжает в Россию

cantu_spb

Не знаю, стоит ли уже об этом говорить публично, но я похоже все равно не удержусь.

Мало кто знает, что на днях Марко Канту был в Санкт-Петербурге. Как я понимаю, это особо не афишировалось, я сам с ним случайно столкнулся у Казанского собора.

И он был здесь не только ради визита в местный офис Embarcadero. Сейчас наиболее перспективные технологии компании (Firemonkey и FireDAC) тесно связаны с Россией, поэтому Марко будет удобнее жить и работать здесь. Он уже начал учить русский язык и вполне может быть, что со временем получит российское гражданство вслед за его любимым актером Жераром Депардье.

Довольно интересно поговорили. Канту пригласил меня в команду, но я еще думаю. Все-таки смена работы — это серьезный шаг. Но кто знает?

Next Delphi Yacc & Lex

Не так редко возникает ситуация, когда приходится писать парсер. Написание парсера вручную с нуля — задача не очень сложная, но довольно нудная и однообразная. Поэтому…

Для генерации парсеров существуют известные утилиты yacc и lex. Они позволяют достаточно кратко описать синтаксис некоего языка и по этому описанию автоматически сгенерировать код парсера для него.

По какой-то причине я не смог найти их версию для Delphi. Есть довольно известный проект TP Lex and Yacc, его корни уходят аж в 1990 год (да-да, TP — это именно Turbo Pascal). Добрые люди его долгое время поддерживали, но несколько лет назад и их энтузиазм иссяк.

В итоге я решил попробовать самостоятельно реанимировать этот проект, взяв за основу последнюю его инкарнацию из найденных мной — Delphi Lex & Yacc. Были и другие попытки оживить проект, но по-моему эта попытка самая свежая.

Во-первых, пришлось внести некоторые изменения, чтобы код компилировался и работал в свежей версии Delphi. Во-вторых, я добавил немного объектно-ориентированности — все-таки в 2013 году не очень приятно интегрировать в свое приложение нечто пусть и полезное, но использующее кучу глобальных переменных и STDIN/STDOUT для организации ввода-вывода.

Так родился Next Delphi Lex & Yacc (ndyacclex).

Это только первый шаг. На чем-то серьезном я этот код пока не тестировал. Остановился на том, что добился корректной работы тестового приложения, прилагавшегося к оригинальной версии кода, немного переделав его под современные реалии.

К сожалению, все еще нет никакой поддержки юникода. Всюду AnsiString и AnsiChar. Пожалуй, этим вопросом я и займусь, но это явно потребует некоторого времени.

Для тех, кому лень качать, прямо здесь маленькое демо.

exprparse

Имея описание синтаксиса (в данном случае это арифметические выражения), утилиты ndyacclex позволяют сгенерировать исходный код класса-наследника TCustomParser, который затем можно использовать вот так:

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
  StrStream: TStringStream;
begin
  if Key = #13 then
  begin
    Key := #0;
    Memo1.Lines.Add('> ' + Edit1.Text);

    StrStream := TStringStream.Create;
    try
      StrStream.WriteString(Edit1.Text);
      StrStream.Position := 0;
      try
        Parser.parse(StrStream, WriteCB);
      except
        on E: EExprParserException do
          Memo1.Lines.Add(E.Message);
      end;
      Edit1.Text := '';
    finally
      StrStream.Free;
    end;
  end;
end;

procedure TForm1.WriteCB(Value: Real);
begin
  Memo1.Lines.Add(Format('%2.2f',[Value]));
end;

Это практически весь код тестового приложения, если не учитывать сгенерированный парсер.

TChromium в FireMonkey

10.12.2013 update
Этот код тестировался только в Delphi XE3 и в дальнейшем поддерживаться скорее всего не будет. Версию для Delphi XE5 (и, может быть, будущих версий) можно скачать тут: http://code.google.com/p/dcef3/

Очень я страдал из-за отсутствия компонента-браузера в FireMonkey. Известный проект Delphi Chromium Embedded все-таки включил поддержку FMX в последнем билде. Но не смотря на то, что прошло довольно много времени, поддержку FMX2 автор добавлять не торопится. В итоге пришлось брать ситуацию в свои руки.

Компонент TChromiumFMX из официальной сборки вполне себе работает в FireMonkey (в XE2), но в FMX2 даже не компилируется. Пришлось немного разобраться с тем, как он устроен и исправить. Благо, серьезных изменений не потребовалось.

В FMX2 изменились две нужные компоненту вещи.

Первое — TBitmap больше не имеет свойств ScanLine и StartLine. Прямой доступ к содержимому TBitmap переделали (интересно, зачем?) и теперь оно доступно через класс TBitmapData, который возвращает метод TBitmap.Map.

Ну и второе, более известное — Platform.* больше нет, теперь необходимо получать нужный интерфейс через TPlatformServices.GetPlatformService. Здесь все довольно прямолинейно и проблем нет.

chromium-fmx

Особо изобретательно я его не тестировал, но для моих целей компонент вполне подходит — сайты через него смотреть можно. Скачать его можно тут. Еще, пожалуй, отправлю мои правки автору, может быть сочтет нужным добавить их в официальную версию.

LISPообразное нечто

Возился немного с задачами курса Programming Languages и захотелось попробовать сделать на Delphi интерпретатор простого языка. Отбирать хлеб у MS и Embarcadero я не хочу, поэтому не буду даже пытаться что-то оптимизировать и тип данных будет только один — целые числа. Синтаксис пусть будет похож на LISP.

Назвал я этот язык гордым именем PDSL, что означает — PseudoDSL.

Так вот. Каждый элемент этого языка будет выражением, которое можно вычислить. То есть, грубо говоря, процедур не будет, будут только функции.

Простейшее выражение — это целое число, которое вычисляется само в себя:

(Number 5) -> (Number 5)

Другие выражения, должны быть более полезными. Например, функция Add должна работать примерно так:

(Add (Number 2) (Number 3)) -> (Number 5)

или так:

(Add (Number 2) (Add (Number 1) (Number 3))) -> (Number 6)

Очевидно, степень вложенности может быть любой и прежде, чем вычислить выражение, нужно вычислить его параметры. Мы имеем дело с деревом и вычислять его нужно рекурсивно.

Так как речь, напомню, о Delphi, то все выражение можно представить в виде дерева объектов, реализующих такой интерфейс:

IExpression = interface
  function Evaluate: IExpression;
end;

Объекты для примеров выше у меня выглядят примерно так (я не буду вдаваться в подробности, полный код можно скачать :)

constructor TNumber.Create(AValue: Integer);
begin
  inherited Create;
  FValue := AValue;
end;

function TNumber.Evaluate: IExpression;
begin
  Result := Self;
end;

Т.е. число держит в себе значение и возвращает само себя при вычислении.

С Add чуть сложнее. Объект принимает два выражения. И при вычислении сначала вычисляет их, а затем их сумму.

constructor TAdd.Create(AValue1, AValue2: IExpression);
begin
  inherited Create;
  FExprs.Add(AValue1);
  FExprs.Add(AValue2);
end;

function TAdd.Evaluate: IExpression;
var
  Expr1, Expr2: IExpression;
  Val1, Val2: IHasValue;
begin
  Expr1 := FExprs[0].Evaluate;
  Expr2 := FExprs[1].Evaluate;

  if Supports(Expr1, IHasValue, Val1) and Supports(Expr2, IHasValue, Val2) then
    Result := TNumber.Create(Val1.Value + Val2.Value)
  else
    raise EExprException.Create('Invalid expression applied to TAdd');
end;

Здесь интерфейс IHasValue используется для проверки, является ли выражение конечным значением и собственно для получения этого значения.

IHasValue = interface
  ['{567A6313-3ABE-4620-9560-64F93BC4979A}']
  function GetValue: Variant;
  property Value: Variant read GetValue;
end;

Этот интерфейс реализован в объекте TNumber. Тип у нас один, но на всякий случай я решил подойти к вопросу более универсально. То есть не так сложно будет добавить и другие типы в язык.

Так же для удобства я завел функции-конструкторы. Ничего особенного они не делают, но мне с ними немного удобнее.

function Number(AValue: Integer): IExpression;
begin
  Result := TNumber.Create(AValue);
end;

function Add(Expr1, Expr2: IExpression): IExpression;
begin
  Result := TAdd.Create(Expr1, Expr2);
end;

Так же, для удобства отладки я добавил в интерфейс IExpression свойство AsString, которое возвращает описание объекта в виде строки, например:

function TNumber.GetAsString: string;
begin
  Result := Format('(%s %d)', [Self.ClassName, FValue]);
end;

Это относится и к TAdd, и к будущим объектам, я не буду больше подробно на этом моменте останавливаться.

Этого должно быть достаточно, чтобы вычислить один из примеров выше.

var
  Test: IExpression;
begin
  Test := Add(Number(2), Add(Number(1), Number(3)));
  Edit1.Text := Test.AsString;
  Edit2.Text := Test.Evaluate.AsString;
end;

После выполнения этого кода в Edit1 мы видим:

(TAdd (TNumber 2) (TAdd (TNumber 1) (TNumber 3)))

Все правильно. А в Edit2:

(TNumber 6)

Бинго! :)

Все это очень приятно, но практически бесполезно.
Что отличает настоящий язык программирования, от того, что мы имеем? Главное отличие в том, что для того, чтобы двигаться чуть дальше совсем простых примеров, кроме синтаксического дерева нужны переменные, нужны области видимости. То есть код выполняется не сам по себе, он выполняется в рамках некоего окружения.

Давайте подумаем о переменных и области видимости. Что такое переменная? У нее есть имя и значение. Т.е. это пара — имя и связанное с ним выражение (в нашем языке всё — выражения, помните? :)
Здесь мне понадобился вот такой интерфейс:

IEnvironment = interface
  function GetValue(const AName: string): IExpression;
  function SetValue(const AName: string; AExpr: IExpression): IEnvironment;
end;

С GetValue все понятно, а на SetValue я остановлюсь чуть подробнее. Так как мы говорили об областях видимости, давайте договоримся, что если мы объявляем переменную, то она видима текущему узлу дерева и тем, кто ниже, но не тем, кто выше. Поэтому, чтобы не испортить окружение того, кто нас вызвал, объявляя переменную, мы по сути создаем новую копию окружения и отправляем его в его собственную независимую жизнь в рамках текущей области видимости.

Реализована эта функция у меня вот так. В лоб, никакой магии. Оптимальнее было бы не делать полную копию, но к этому я пока не стремлюсь.

function TEnvironment.SetValue(const AName: string; AExpr: IExpression): IEnvironment;
var
  EnvPair: TPair<string, IExpression>;
  NewEnv: TEnvironment;
begin
  NewEnv := TEnvironment.Create;
  for EnvPair in FEnv do
    NewEnv.FEnv.Add(EnvPair.Key, EnvPair.Value);
  NewEnv.FEnv.AddOrSetValue(AName, AExpr);

  Result := NewEnv;
end;

Так как теперь каждое выражение при вычислении должно учитывать свое окружение, то интерфейс IExpression придется слегка изменить:

IExpression = interface
  function GetAsString: string;

  function Evaluate: IExpression; overload;
  function Evaluate(Env: IEnvironment): IExpression; overload;

  property AsString: string read GetAsString;
end;

Evaluate оставшийся без параметров — это ничего особенного, просто возможность вычислить нечто с пустым окружением. Это понадобится, например, при запуске программы.

function TExpression.Evaluate: IExpression;
begin
  Result := Evaluate(TEnvironment.Create);
end;

Так как у нас теперь все есть, давайте сделаем переменные.

constructor TVariable.Create(AName: string);
begin
  inherited Create;
  FName := AName;
end;

function TVariable.Evaluate(Env: IEnvironment): IExpression;
begin
  Result := Env.GetValue(FName);
end;

Объект просто хранит свое имя, а при вычислении возвращает связанное с ним выражение из окружения. Переменные теперь есть, но по-прежнему нет синтаксиса для их объявления.

Предлагаю использовать lisp-оподобную функцию let. Семантика ее такова: (let [varname varvalue] body). Данная функция связывает имя varname с выражением varvalue, а затем возвращает результат вычисления выражения body, естественно вычисляя его в только что созданном новом окружении. Если еще не понятно, скоро станет понятно :)

constructor TLet.Create(const AVarName: string; AVarValue, ABody: IExpression);
begin
  inherited Create;
  FVarName := AVarName;
  FExprs.Add(AVarValue);
  FExprs.Add(ABody);
end;

function TLet.Evaluate(Env: IEnvironment): IExpression;
var
  VarValue: IExpression;
begin
  VarValue := FExprs[0].Evaluate(Env);
  Result := FExprs[1].Evaluate(Env.SetValue(FVarName, VarValue));
end;

Здесь значение переменной вычисляется в рамках внешнего окружения, а значение тела функции уже в рамках вновь созданного.

Настало время небольшого теста:

var
  Test: IExpression;
begin
  Test := Let('N', Number(5),
              Add(Variable('N'), Add(Number(1), Number(3))));
  Edit1.Text := Test.AsString;
  Edit2.Text := Test.Evaluate.AsString;
end;

В Edit1 видим:

(TLet [N (TNumber 5)] (TAdd (TVariable N) (TAdd (TNumber 1) (TNumber 3))))

А в Edit2:

(TNumber 9)

Ура :)

Но это еще не все. Полезному языку не помешали бы функции. Давайте подумаем о них. Для простоты пусть у функций будет только один параметр. На самом деле, это не накладывает вообще никаких ограничений на язык, но это сейчас не важно. Идем дальше. Во-первых, функции нужно объявлять, во-вторых — вызывать. Это два разных действия. Пусть это будут TDefineFunc и TCallFunc.

Очевидно, функция — это выражение. То есть те переменные, что мы имеем — это вполне себе функции, только без параметров. Разница еще и в том, что значение переменной мы вычисляем сразу, а в вычислении значения функции в момент ее объявления смысла мало. Еще один важный нюанс заключается в том, что функция должна вычисляться в рамках окружения, в котором она была объявлена (плюс значение параметра, конечно), а не в рамках окружения, в котором она была вызвана. Это так называемый lexical scope — подход принятый в большинстве языков программирования.

Это приводит нас к простой мысли. В результате вычисления TDefineFunc должен получаться некий объект, хранящий в себе тело функции, информацию об окружении и конечно имя параметра. И затем уже вычисляя TCallFunc в применении к этому объекту, мы присвоим параметру значение и получим результат. Пусть этот «некий» объект будет называться TClosure.

constructor TClosure.Create(AFunc: IExpression; AEnv: IEnvironment; const AParamName: string);
begin
  inherited Create;
  FEnv := AEnv;
  FParamName := AParamName;
  FExprs.Add(AFunc);
end;

function TClosure.EvaluateClosure(AParamValue: IExpression): IExpression;
begin
  Result := FExprs[0].Evaluate(FEnv.SetValue(FParamName, AParamValue));
end;

Как я выше и говорил, он осведомлен об окружении, имени параметра, а так же имеет ссылку на тело функции. Метод Evaluate в данном случае другой, т.к. окружение здесь свое собственное и требуется получить значение параметра функции извне.

Таким образом, TDefineFunc выглядит проще некуда:

constructor TDefineFunc.Create(const AParamName: string; ABody: IExpression);
begin
  inherited Create;
  FParamName := AParamName;
  FExprs.Add(ABody);
end;

function TDefineFunc.Evaluate(Env: IEnvironment): IExpression;
begin
  Result := TClosure.Create(FExprs[0], Env, FParamName);
end;

А TCallFunc немного сложнее:

constructor TCallFunc.Create(const AFuncName: string; AParamValue: IExpression);
begin
  inherited Create;
  FFuncName := AFuncName;
  FExprs.Add(AParamValue);
end;

function TCallFunc.Evaluate(Env: IEnvironment): IExpression;
var
  FuncExpr, ParamVal: IExpression;
  Closure: IClosure;
begin
  ParamVal := FExprs[0].Evaluate(Env);
  FuncExpr := Env.GetValue(FFuncName);

  if Supports(FuncExpr, IClosure, Closure) then
    Result := Closure.EvaluateClosure(ParamVal)
  else
    raise EExprException.Create('Invalid expression applied to TCallFunc');
end;

TCallFunc принимает имя функции и параметр, находит соответствующий TClosure привязанный к переменной в окружении, и затем вычисляет его значение, передавая параметр.

Еще один тест:

var
  Test: IExpression;
begin
  Test := Let('Add2', DefineFunc('N', Add(Variable('N'), Number(2))),
              CallFunc('Add2', Number(3)));
  Edit1.Text := Test.AsString;
  Edit2.Text := Test.Evaluate.AsString;
end;

Т.е. объявляем функцию с параметром N возвращающую N+2, привязываем ее к имени Add2, затем вызываем ее с параметром равным 3. Результат, очевидно, должен быть равным 5.

В Edit1:

(TLet [Add2 (TDefineFunc N (TAdd (TVariable N) (TNumber 2)))] (Add2 (TNumber 3)))

В Edit2:

(TNumber 5)

Ура! :)

Многое уже можем :) Но признаюсь, что начальной моей целью было написать хотя бы вычисление факториала. В данной реализации языка этого сделать нельзя. Почему? Потому что он не поддерживает рекурсию. В момент объявления функции в текущем окружении нет никакой информации о ней самой, она появляется в окружении только после этого. То есть, имея только копию окружения до объявления функции, функция не в состоянии вызвать саму себя.

В связи с этим я немного изменил ход вычисления Let.

function TLet.Evaluate(Env: IEnvironment): IExpression;
var
  VarValue: IExpression;
  Closure: IClosure;
begin
  VarValue := FExprs[0].Evaluate(Env);
  if Supports(VarValue, IClosure, Closure) then
    Closure.Env := Closure.Env.SetValue(FVarName, VarValue);

  Result := FExprs[1].Evaluate(Env.SetValue(FVarName, VarValue));
end;

То есть в окружение TClosure добавляется информации о самой переменной, к которой TClosure привязан.

Настало время финального теста. Не буду останавливаться подробно на добавленных операциях сделанных по образу и подобию функции Add:

function TSub.Evaluate(Env: IEnvironment): IExpression;
var
  Expr1, Expr2: IExpression;
  Val1, Val2: IHasValue;
begin
  Expr1 := FExprs[0].Evaluate(Env);
  Expr2 := FExprs[1].Evaluate(Env);

  if Supports(Expr1, IHasValue, Val1) and Supports(Expr2, IHasValue, Val2) then
    Result := TNumber.Create(Val1.Value - Val2.Value)
  else
    raise EExprException.Create('Invalid expression applied to TSub');
end;

function TMul.Evaluate(Env: IEnvironment): IExpression;
var
  Expr1, Expr2: IExpression;
  Val1, Val2: IHasValue;
begin
  Expr1 := FExprs[0].Evaluate(Env);
  Expr2 := FExprs[1].Evaluate(Env);

  if Supports(Expr1, IHasValue, Val1) and Supports(Expr2, IHasValue, Val2) then
    Result := TNumber.Create(Val1.Value * Val2.Value)
  else
    raise EExprException.Create('Invalid expression applied to TMul');
end;

// возвращает 1, если выражения равны или 0, если нет
function TEquals.Evaluate(Env: IEnvironment): IExpression;
var
  Expr1, Expr2: IExpression;
  Val1, Val2: IHasValue;
begin
  Expr1 := FExprs[0].Evaluate(Env);
  Expr2 := FExprs[1].Evaluate(Env);

  if Supports(Expr1, IHasValue, Val1) and Supports(Expr2, IHasValue, Val2) then
  begin
    if Val1.Value = Val2.Value then
      Result := TNumber.Create(1)
    else
      Result := TNumber.Create(0);
  end
  else
    raise EExprException.Create('Invalid expression applied to TEquals');
end;

// IfThenElse e1 e2 e3
// Возвращает значение e2, если e1 > 0, иначе e3
function TIfThenElse.Evaluate(Env: IEnvironment): IExpression;
var
  Expr1: IExpression;
  Val1: IHasValue;
begin
  Expr1 := FExprs[0].Evaluate(Env);

  if Supports(Expr1, IHasValue, Val1) then
  begin
    if Val1.Value > 0 then
      Result := FExprs[1].Evaluate(Env)
    else
      Result := FExprs[2].Evaluate(Env);
  end
  else
    raise EExprException.Create('Invalid expression applied to TIfThenElse');
end;

Ну и наконец сам тест. Вроде бы все очевидно. Объявлена рекурсивная функция Factorial и вызвана с параметром 10.

var
  Test: IExpression;
begin
  Test := Let('Factorial',
    DefineFunc('N', IfThenElse(Eq(Variable('N'), Number(0)),
                               Number(1),
                               Mul(Variable('N'), 
                                   CallFunc('Factorial', Sub(Variable('N'), Number(1)))))),
    CallFunc('Factorial', Number(10)));
  Edit1.Text := Test.AsString;
  Edit2.Text := Test.Evaluate.AsString;
end;

В Edit2 видим «(TNumber 3628800)». Как раз то, чего я хотел.

Следующий шаг — это уже дать возможность пользователю написать что-то вроде:

Let(Factorial,
    DefineFunc(N, IfThenElse(Eq(N, 0),
                             1,
                             Mul(N, Factorial(Sub(N, 1))))),
    Factorial(10))

разобрать этот текст автоматически, построить синтаксическое дерево и вычислить. Возможно, в следующей серии. Подобный однообразный синтаксис парсить достаточно просто. Можно пойти дальше и сделать синтаксис более дружелюбным, здесь уже на что фантазии хватит. Лично мне приятнее было бы читать функцию вот так:

let
  fun Factorial N =
    if Eq(N, 0) then 1 else Mul(N, Factorial(Sub(N, 1))
do
    Factorial(10)
end

Думаю, очевидно, что семантика все еще полностью совпадает с описанным здесь языком. Но это уже совсем другой вопрос.

И можно наконец уже скачать весь пример :)

Вкратце о презентации Delphi XE3

Посетил презентацию Delphi XE3. Было интересно. Изложу скороговоркой то, что зацепило внимание. На презентации было сказано много больше, но не все темы мне близки.

Metropolis UI
Metro UI не настоящий, а пока всего-лишь его иммитация в VCL или FireMonkey с помощью стилей. Для полноты картины к приложению прикрепляется специальный апплет (вот он — настоящий WinRT), который и будет установлен в стартовом экране Windows 8. Через этот апплет можно будет запускать приложение, а само приложение сможет с ним взаимодействовать с помощью компонента TLiveTile. Т.е. user experience воспроизводится довольно точно.

FireMonkey2
Долгожданный TActionList теперь есть и в FireMonkey. Интересно, что появились растровые контролы. Говорят, что для «pixel perfect»-интерфейсов, что в общем-то разумно. Вообще, в этой версии заметно внимание к деталям и попытка с помощью стилей воспроизвести нативный интерфейс и в Windows и в MacOS. В будущем же этот «pixel perfect» подход явно будет еще более актуален на мобильных платформах. Поддерживается Retina Display. Как я понял, наличие ретины определяется автоматически, затем подгружается нужный стиль. Появилось больше возможностей для управления размещением контроллов на форме: grid/flow layout, anchors, alignment. Сделали внешний редактор стилей и для VCL, и для FireMonkey; теперь эту работу проще отдать дизайнерам. Появился фреймворк Sensors API для различных датчиков и сенсоров, это скорее уже нужно для мобильных платформ.

Visual LiveBindings
В XE3 можно не писать expressions, а визуально соединять квадратики стрелочками. Выглядит занимательно.

Разное
Компилятор для iOS покидает Delphi и вернется к нам чуть позже в составе Mobile Studio. Долгожданный многими 64-битный компилятор C++ обещают выпустить в 4м квартале, бета доступна уже сейчас. Не смотря на выход XE3, новые апдейты для XE2 можно ждать.

Первый взгляд на CouchDB

Решил написать этот пост в ответ на вопрос о CouchDB в Google+. Сразу оговорюсь, что специалистом по CouchDB я не являюсь и мой взгляд весьма поверхностный.

Главное отличие CouchDB от многочисленных РСУБД в том, что в CouchDB нет никакой схемы данных. База данных является хранилищем набора произвольных JSON-документов:

{ "type": "book", "title": "Voyna i mir", "author": "Tolstoi" }
{ "type": "movie", "title": "Stalker", "director": "Tarkovsky", "country": "USSR" }
...

Для запросов к этим данным используются заранее подготавливаемые view, состоящие из map/reduce-функций. Эти функции можно писать на JavaScript, Erlang или Java. Насколько я понимаю, можно подключить и другие языки.

На вход функции map подается каждый из документов базы данных, функция должна вернуть пару ключ-значение (или не вернуть, если документ нужно пропустить). Ключ может быть составным.

Например, чтобы выбрать все книги, можно использовать такую функцию:

function(doc) {
  if (doc.type == 'book') {
    emit(doc.title, doc);
  }
}

Её результат хранится как индекс по ключу (в примере выше ключ — это doc.title, значение — весь документ) и рассчитывается однажды при создании view, а затем обновляется при добавлении в БД новых документов.

Таким образом выборка из view производится по ключу или диапазону ключей из готового индекса, а потому работает очень быстро даже на больших объёмах данных.

Функция reduce получает на вход результирующий список ключей и значений. Она может использоваться, например, для группировки и/или как агрегирующая функция.

Логика функций может быть фактически произвольной и таким образом можно обрабатывать данные проще и гибче, чем с помощью SQL.

Join-ы в CouchDB делать можно, но как-то очень замысловато, я с этим пока толком не разобрался. Но с другой стороны, документы могут содержать массивы и вложенную иерархию, что во многих случаях и является смыслом join-ов в РСУБД:

{
  "type": "movie",
  "title": "Stalker",
  "director": "Tarkovsky",
  "country": "USSR",
  "keys": ["key1", "key2", "key3"],
  "attributes": {
     "attr1": "val1",
     "attr2": "val2" }
}

Документы являются неизменяемыми. При редактировании автоматически создаётся новая версия документа, а старая остается в БД. Размер базы может сильно распухать, если периодически не производить чистку. Этот нюанс делает CouchDB не очень пригодной для задач, где много редактируемых данных (этого лишена MongoDB).

Следствием такого подхода является гарантированное отсутствие блокировок. А так же хорошая поддержка master-master репликации (в отличие от MongoDB), т.е. можно иметь несколько серверов с идентичными данными. Это полезно, если надежность критична.

Доступ к данным осуществляется через HTTP (точнее REST-интерфейс). Не самый быстрый способ, но зато очень масштабируемый.