Записи с пометкой ·

Delphi

·...

Перспективы мобильной разработки на Delphi

Близится релиз Delphi XE5 (многие уже отмечают) и Embarcadero вслед за поддержкой iOS обещает нам поддержку Android. Мне как гику это кажется интересным, но нужна ли поддержка мобильных устройств рынку? Ведь есть уже немало средств разработки: от Objective C и Java до штук вроде Xamarin. Зачем еще одно?

Вот что я об этом думаю.

Традиционно сложилось, что сильная сторона Delphi — это корпоративные приложения. Различных учетных систем и прочих систем внутренней автоматизации на Delphi всегда было написано куда больше, чем коробочных продуктов. И мне кажется, что как раз в этой среде у мобильных приложений на Delphi есть шанс.

Благодаря инициативам вроде BYOD (Bring Your Own Device) мобильные устройства все больше проникают в корпоративную среду. В России, пожалуй, пока не очень, но в целом корпоративный мир движется в эту сторону. И мы там будем.

Мобильные возможности получают даже такие казалось бы традиционно десктопные продукты как Toad, получивший свое интернет-развитие в виде сервиса MyToad, позволяющего удаленно запускать выполнение SQL-скриптов [а вот DBArtisan от Embarcadero так не умеет ;)].

Многие считают веб-интерфейс панацеей, но это не совсем так. Есть и обратные мнения:

Facebook CEO said that «the biggest mistake we made as a company was betting too much on HTML5 as opposed to «native» code on the iOS and Android platforms».

Не будем останавливаться на веб, вернемся к BYOD. Слово «bring» как бы намекает нам, что устройство можно носить, то есть скорее всего оно мобильное. И вот эта возможность принести и использовать на работе собственное устройство влечет за собой некоторые последствия.

В первую очередь это конечно вопросы безопасности (что будет если пустить какой-то непонятный iPad в корпоративную сеть?). Но мы говорим про Delphi, вопросы безопасности пусть решают системные администраторы, благо крупные вендоры уже предлагают готовые инфраструктурные решения.

Главный касающийся Delphi вопрос — это отсутствие программного обеспечения. Если вы зайдете на Apple Store или Android Market, то увидите огромное количество приложений. Но корпоративным пользователям как правило нужны совсем не Angry Birds и не калькулятор калорий. Тем более что очень часто они работают с какой-то самодельной системой, мобильные клиенты для которой еще только предстоит разработать.

И если сама система была написана на Delphi, то встает выбор — разработать мобильное приложение на Delphi, повторно использовав часть кода и опыт имеющихся разработчиков, или нанять новых/обучить старых разработчиков и написать все с нуля на Objective C или Java. При такой постановке вопроса преимущества Delphi становятся видны. То есть определенная ниша для «мобильной» Delphi все-таки существует.

Так что посмотрим. Ваши мнения?

Delphi и предметно-ориентированные языки

Предметно-ориентированные языки (DSL) в прикладных приложениях используются намного чаще, чем может показаться на первый взгляд. Пожалуй, самый распространенный в повседневной практике случай использования DSL — это разнообразные конфигурационные файлы.

Для описания каких-то сложных конфигураций довольно часто используют XML. Сегодня он де-факто стал чем-то вроде универсального DSL, благодаря обилию готовых средств синтаксического анализа, лексического разбора и всего остального, что необходимо для работы. Но универсальные плюсы влекут за собой и минусы. Давайте посмотрим.

Скажем, поставлена задача разработать программу для светофора. Светофоры в принципе все одинаковые, но вести они себя должны по-разному. Настраивая каждый отдельный светофор, мы можем либо хранить логику его работы в коде, перекомпилируя программу для каждого отдельного светофора (что неприемлемо!), либо вынести эту логику во внешний конфигурационный файл и поручить настройку отдельных светофоров сотрудникам ГИБДД (то есть в общем случае это будут эксперты предметной области, а не программисты).

Представим, как бы мог выглядеть этот конфигурационный файл в формате XML. Пусть конкретный светофор ведет себя следующим образом. 10 секунд горит красный, затем 3 секунды мигает желтый и, наконец, 7 секунд горит зеленый. Проявив немного фантазии, можно представить себе вот такой XML-файл:

<trafficlight>
  <red>10</red>
  <yellow blink="yes">3</yellow>
  <green>7</green>
</trafficlight>

Этот файл мал и прост, но мы представим, что конфигурация очень сложная и на бумаге занимает много страниц — в него можно добавить зависимость режима работы от времени суток, синхронизацию с другими светофорами и многое другое, чего душа пожелает. Для нас — программистов — работа с таким форматом файла потребует минимум усилий. Идиллия? Не совсем.

Каковы минусы? В этой конфигурации большое количество визуального мусора и ненужных деталей. Этот «код» плохо читаем. Читаемость очень важна при общении с экспертами предметной области и в их дальнейшей самостоятельной работе. Но мы обычно ленимся предложить что-то удобнее XML, а заказчики зачастую даже не знают, что можно делать иначе.

Но иначе можно.

Что, если записать настройки так?

red 10
yellow 3 blink
green 7

Уже чуть больше ясности. Или даже так, добавим гибкости, чтобы в будущем иметь возможность менять начальное состояние светофора и задавать более сложные режимы работы:

init
  red off
  yellow off
  green off
endinit
loop
  red on
  wait 10
  red off
  yellow on
  wait 1
  yellow off
  wait 1
  yellow on
  wait 1
  yellow off
  green on
  wait 7
  green off
endloop

Мне нравится эта программа. Давайте попробуем её выполнить. Так как у меня дома нет настоящего светофора (а жаль), то программировать придется его простую программную модель.

Скриншот программы TrafficLight

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

Для автоматизации задач синтаксического и лексического анализа текста часто используют утилиты вроде Lex/Yacc. Недавно я писал о попытке реанимировать их версию для Delphi. И здесь они к месту.

Не буду пытаться объять необъятное, на просторах интернета множество руководств по использованию Lex и Yacc. Буду краток. С помощью данных утилит можно сгенерировать код парсера некоего языка по его формальному описанию. Собственно, они позволяют сгенерировать два куска кода: лексер (Lex) и парсер (Yacc). Лексер будет разбивать текст на токены, а парсер собирать эти токены в большую осмысленную программу.

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

Описание токенов в нашем случае весьма однообразное. В языке программирования светофора есть ключевые слова RED, YELLOW, GREEN, ON, OFF, INIT, ENDINIT, LOOP, ENDLOOP, WAIT, а так же числа. Переводы строк и пробелы игнорируются. Ниже содержание файла описания токенов для Lex (как водится, слева регулярное выражение, справа шаблон Delphi-кода для его обработки).

D				[0-9]

%start
%%

  var result : integer;

{D}+(\.{D}+)?([Ee][+-]?{D}+)?	begin
				  val(yytext, yylval.yyCardinal, result);
				  if result=0 then
				    return(NUM)
				  else
				    return(ILLEGAL)
				end;

RED				begin
	   			    return(RED)
				end;

YELLOW				begin
	   			    return(YELLOW)
				end;

GREEN				begin
	   			    return(GREEN)
				end;

INIT				begin
	   			    return(INIT)
				end;

ENDINIT				begin
	   			    return(ENDINIT)
				end;

LOOP				begin
	   			    return(LOOP)
				end;

ENDLOOP				begin
	   			    return(ENDLOOP)
				end;

ON				begin
	   			    return(ON)
				end;

OFF				begin
	   			    return(OFF)
				end;

WAIT				begin
	   			    return(WAIT)
				end;

" "             		;

.				|
\n				;
\r				;

Затем нужно показать Yacc’у, по каким правилам из этих токенов формируется синтаксическое дерево программы (ниже фрагмент конфигурационного файла):

%token <Cardinal> NUM 
%token <TCustomLight> RED YELLOW GREEN
%token <TEventList> INIT ENDINIT LOOP ENDLOOP

%type <TCustomEvent> expr
%type <TCustomLight> light

%token ON OFF
%token WAIT

%token ILLEGAL 		/* illegal token */

%%

input	:  /* empty */
	|  _init _loop		 { yyaccept; }   
	;

_init   : INIT exprs ENDINIT 	 { FInitList.AddRange(FTemporaryList.ToArray); FTemporaryList.Clear; }
        ;

_loop   : LOOP exprs ENDLOOP	 { FLoopList.AddRange(FTemporaryList.ToArray); FTemporaryList.Clear }
        ;

exprs   :  /* empty */
	|  exprs expr 		 { FTemporaryList.Add($2); }
        ;

expr    :  WAIT NUM		 { $$ := TWaitEvent.Create($2); }
	|  light ON		 { $$ := TLightOnEvent.Create($1); }
	|  light OFF		 { $$ := TLightOffEvent.Create($1); }
	;

light   :  RED			 { $$ := FRedLight; }
        |  YELLOW		 { $$ := FYellowLight; }
        |  GREEN		 { $$ := FGreenLight; }
	;

Здесь видно, что есть тип токена light, который олицетворяет собой название конкретной лампочки. Тип expr, представляющий собой отдельное действие над лампой или задержку. А так же списки этих действий (exprs, на которых построены блоки _init и _loop). И опять же — слева описание, справа в скобках шаблон кода. Для программы из примера по этому описанию парсер может сформировать примерно вот такое синтаксическое дерево и обработать его:

Синтаксическое дерево программы (TrafficLight)

На нижнем уровне находятся отдельные команды и их операнды, выше они постепенно объединяются в единое синтаксическое дерево. Проходя дерево «снизу вверх», останавливаясь в вершинах и выполняя код, указанный в шаблонах, парсер формирует результат. В нашем случае результат — это просто два списка команд (INIT и LOOP), эти списки затем используются для управления моделью светофора.

При должной сноровке подобные парсеры можно создавать очень и очень быстро. Не считая конфигурации утилит, в данной программе мною написано всего-лишь порядка 200 строк кода (включая модель светофора и код, который ей управляет). Это довольно мало, то есть разработка небольшого языка не требует больших затрат сил и времени. Хотя, конечно, необходима несколько большая квалификация, чем при работе с XML. Удобство пользователей обычно того стоит :)

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

Ссылка на полный код примера.

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 и работать с полями этого датасета как со свойствами класса.

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

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

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 можно ждать.

Презентация Delphi XE3 в Питере

11 сентября планирую посетить презентацию Delphi XE3. Надеюсь, дадут понимание куда теперь движется Delphi. Последние новости, мягко говоря, не радуют.

Кто-нибудь из Санкт-Петербурга меня читает? Если есть желание, можно было бы заодно и познакомиться. Те, кто не сможет пойти, что вообще вам интересно? Могу попробовать передать какие-то вопросы.

NakeyMonkey

Jason Southwell предлагает разработать набор FireMonkey-оберток для нативных контролов Windows/OSX и собирает на это деньги. Планирует для начала собрать 20 тысяч долларов.

Идея ясна. Существующие компоненты FireMonkey отрисовываются средствами Delphi практически с нуля, что с одной стороны во многом и обеспечивает их кроссплатформенность, но с другой — в результате мы получаем компоненты не вполне естественно выглядящие в обеих поддерживаемых на сегодня ОС. И это полбеды — кроме внешнего вида, приходится самостоятельно разрабатывать и логику этих компонентов. Например, RichEdit довольно сложен, самостоятельно повторить его логику в рамках FireMonkey — задача не из тривиальных. И VCL, и CLX не изобретали велосипедов, а пользовались готовым.

На резонный вопрос «почему у NakeyMonkey получится то, что у Kylix CLX в свое время не получилось?» Джейсон отвечает, что проблема CLX была в использовании лишнего промежуточного слоя в виде Qt, NakeyMonkey же не будет привязан к циклу разработки и прочим прихотям третьесторонней библиотеки, но в целом ей будут присущи все тяготы и лишения CLX.

Ваши мнения? Как это могло бы вписаться в идеологию стилей FireMonkey?

update
Алексей Казанцев обнаружил интересный комментарий Дэвида И. Суть в том, что Embarcadero работает над нативными компонентами для FireMonkey и у разработчиков будет выбор, какие компоненты использовать: векторные или нативные. По-моему, это очень серьезная новость.