Logo

11 декабря 2014 г.

Этюд 8. Реализация свойства памятью

Этот этюд продолжает тему альтернатив «геттерам» и «сеттерам» в Канторе. Как уже говорилось, переменные в Канторе — это функции, реализованные памятью. Пришло время показать это на примере.

На Delphi

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

На Delphi код оберток снова приходится писать вручную:
type
  TLengthy = class
  protected
    function GetLength: Integer; virtual; abstract;
    procedure SetLength(Value: Integer); virtual; abstract;
  public
    property Length: Integer read GetLength write SetLength;
  end;

  TSegment = class(TLengthy)
  private
    FLength: Integer;
  protected
    function GetLength: Integer; override;
    procedure SetLength(Value: Integer); override;
  end;

{ TSegment }

function TSegment.GetLength: Integer;
begin
  Result := FLength;
end;

procedure TSegment.SetLength(Value: Integer);
begin
  FLength := Value;
end;

На Канторе

Компилятор Кантора осведомлен о связи функций и переменных, поэтому нужную обертку сгенерирует сам:
public class Lengthy of
  public out Core:LongInt Length; // out-свойство
  public Length(Core:LongInt value); // in-свойство
end;

public class Segment from Lengthy of
  public var Core:LongInt Length; // реализация памятью
end;
Разница с прошлым этюдом — в расположении автогенерируемой обертки: в прошлый раз она располагалась в предке, а тут — в потомке. С концептуальной точки зрения это трактуется как реализация памятью (переменной), поскольку одно var-свойство перекрывает сразу оба связанных in- и out-свойства из предка.

10 декабря 2014 г.

Этюд 7. Обертывание свойства-переменной

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

Традиционное решение в виде свойств с «геттерами» и «сеттерами» является паллиативом и лишь фиксирует исторически сложившуюся неполноценность ООП, добавленного в устоявшиеся языки без пересмотра старых концепций.

На Delphi

Для Delphi характерны обе проблемы полей-переменных: и утечек не избежать, и в потомках не перекрыть. Штатное решение их в Delphi — свойства, реализованные виртуальными методами с ручным освобождением объектов, где требуется:
type
  TContainedObject = class
    // some object
  end;

  TContainer = class
  private
    FHoldingObject: TContainedObject;
  protected
    function GetHoldingObject: TContainedObject; virtual;
    procedure SetHoldingObject(Value: TContainedObject); virtual;
  public
    property HoldingObject: TContainedObject read GetHoldingObject
      write SetHoldingObject;
  end;

  TBlockableContainer = class(TContainer)
  private
    FUpdateCount: Integer;
  protected
    function GetHoldingObject: TContainedObject; override;
    procedure SetHoldingObject(Value: TContainedObject); override;
  public
    procedure BeginUpdate;
    procedure EndUpdate;
  end;

{ TContainer }

function TContainer.GetHoldingObject: TContainedObject;
begin
  Result := FHoldingObject;
end;

procedure TContainer.SetHoldingObject(Value: TContainedObject);
begin
  FHoldingObject.Free;
  FHoldingObject := Value;
end;

{ TBlockableContainer }

procedure TBlockableContainer.BeginUpdate;
begin
  Inc(FUpdateCount);
end;

procedure TBlockableContainer.EndUpdate;
begin
  Dec(FUpdateCount);
end;

function TBlockableContainer.GetHoldingObject: TContainedObject;
begin
  if FUpdateCount = 0 then
    Result := inherited GetHoldingObject
  else
    Result := nil;
end;

procedure TBlockableContainer.SetHoldingObject(Value: TContainedObject);
begin
  BeginUpdate;
  try
    inherited;
  finally
    EndUpdate;
  end;
end;
Ручной работы предостаточно. Всего каких-то два с половиной класса, а объем кода уже — ого-ого! К сожалению, Delphi-программистам платят на за число строк...

На Канторе

Аналогичный код на Канторе куда немногословней:
public class ContainedObject of
  // some object
end;

public class Container of
  public ref var ContainedObject HoldingObject; // var-свойство
end;

public class BlockableContainer from Container of
  var Core:Integer updateCount;

  public BeginUpdate of
    updateCount++;
  end;
  public EndUpdate of
    updateCount--;
  end;

  public ref ContainedObject HoldingObject = // out-свойство
    ?updateCount{[0, inherited], null};

  public HoldingObject(ref ContainedObject value) of // in-свойство
    BeginUpdate;
    do
      inherited;
    finally
      EndUpdate;
    end;
  end;
end;
Причин краткости две, по числу решаемых проблем:
  1. Трактовка var-свойств как пары in-свойства и out-свойства, перекрываемых по отдельности.
  2. Корректное присваивание объектов без появления висячих ссылок и утечек памяти.
Код, соответствующий TContainer.GetHoldingObject и TContainer.SetHoldingObject в примере на Delphi, компилятор Кантора сгенерирует сам. В этом ему поможет модификатор ref, включающий механизм подсчета равноправных ссылок на объект, если это потребуется. Механизм полностью контролируется компилятором и скрыт от глаз программиста.

9 декабря 2014 г.

Этюд 6. Кантование свойства-кортежа

Продолжая тему кантования, рассмотрим его применение к кортежам — свойствам, инкапсулирующим несколько параметров.

На Delphi

В Delphi нет возможности описать свойство, выдающее или изменяющее несколько параметров за раз, не объединив их в запись. Соответствующую функциональность приходится эмулировать процедурами:
type
  TBounds = record
    Left, Top, Width, Height: Integer;
  end;

  TSubControl = 0..1;

  TControl = class
  private
    FSubControlBounds: array[TSubControl] of TBounds;
  public
    procedure GetSubControlBounds(Index: TSubControl;
      var Left, Top, Width, Height: Integer); virtual;
    procedure SetSubControlBounds(Index: TSubControl;
      Left, Top, Width, Height: Integer); virtual;
  end;

procedure TControl.GetSubControlBounds(Index: TSubControl;
  var Left, Top, Width, Height: Integer);
begin
  Left := FSubControlBounds[Index].Left;
  Top := FSubControlBounds[Index].Top;
  Width := FSubControlBounds[Index].Width;
  Height := FSubControlBounds[Index].Height;
end;

procedure TControl.SetSubControlBounds(Index: TSubControl;
  Left, Top, Width, Height: Integer);
begin
  FSubControlBounds[Index].Left := Left;
  FSubControlBounds[Index].Top := Top;
  FSubControlBounds[Index].Width := Width;
  FSubControlBounds[Index].Height := Height;
end;

// использование
procedure MoveSubControl(Control: TControl);
var
  X, Y, W, H: Integer;
begin
  Control.GetSubControlBounds(1, X, Y, W, H);
  Inc(X, W);
  Inc(Y, H div 2);
  Control.SetSubControlBounds(1, X, Y, W, H);
end;

На Канторе

Задача легко решается свойствами-кортежами, к которым тоже применимо кантование:
public final class Bounds of
  public final var Core:LongInt Left, Top, Width, Height;
end;

public final class SubControl = 0..1;

public class Control of
  var Bounds[SubControl] subControlBounds;
  // либо var Bounds subControlBounds[SubControl];
  // кантование применимо и здесь

  public final Bounds(SubControl index; // out-свойство
    out Core:LongInt left, top, width, height)
  of
    scb = subControlBounds[index];
    return scb.left, scb.top, scb.width, scb.height;
  end;

  public Bounds(SubControl index; // in-свойство
    Core:LongInt left, top, width, height)
  of
    scb partial subControlBounds(index);
    scb.left = left;
    scb.top = top;
    scb.width = width;
    scb.height = height;
  end;
end;

// использование
moveSubControl(Control control) of
  var (x, y, w, h) = control.Bounds[1]; // кантование out-свойства
  x += w;
  y += h \ 2;
  control.Bounds(1) = [x, y, w, h]; // кантование in-свойства
end;
При кантовании out-свойства работает вывод типов, что упрощает объявление переменных, значение которых берется из свойства-кортежа.

Экспериментальная форма оператора partial без знака «равно» аналогична прошлому этюду.

8 декабря 2014 г.

Этюд 5. Кантование параметров

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

На Delphi

Для этюда взят простой пример с индексируемым свойством, изменение которого требует перерисовки. Пример немного надуманный, но в нем есть и чтение, и запись индексированного свойства:
type
  TLabel = 0..2;

  TEquipmentLabels = class
  private
    FLabel: array[TLabel] of string;
    function GetLabel(Index: TLabel): string;
    procedure SetLabel(Index: TLabel; const Value: string);
  public
    procedure Redraw; virtual;
    property Labels[Index: TLabel]: string read GetLabel
      write SetLabel; // label -- ключевое слово в Delphi
  end;

function TEquipmentLabels.GetLabel(Index: TLabel): string;
begin
  Result := FLabel[Index];
end;

procedure TEquipmentLabels.SetLabel(Index: TLabel;
  const Value: string);
begin
  if FLabel[Index] <> Value then
  begin
    FLabel[Index] := Value;
    Redraw;
  end;
end;

procedure TEquipmentLabels.Redraw;
begin
  // do some drawings
end;

// использование
procedure UpdateLabels(Equipment: TEquipmentLabels);
begin
  if Equipment.Labels[0] = 'кран' then
  begin
    Equipment.Labels[1] = 'Открыт';
    Equipment.Labels[2] = 'Закрыт';
  end
  else if Equipment.Labels[0] = 'лампа' then
  begin
    Equipment.Labels[1] = 'Включена';
    Equipment.Labels[2] = 'Выключена';
  end;
end;
В Delphi и других языках, где свойства реализованы схожим образом, именование «геттеров» и «сеттеров» может быть предметом жарких дискуссий.

На Канторе

В Канторе нет понятия метода. У классов могут быть только свойства: in-свойства, out-свойства и var-свойства, согласно параметрам. Кантование параметров позволяет обращаться к свойствам естественным образом (аналогично Delphi) без дополнительных объявлений:
public final class Label = 0..2;

public class EquipmentLabels of
  var Core:String[Label] label;
  // либо var Core:String label[Label];
  // кантование применимо и здесь

  public final Label[Label index] = // out-свойство
    label[index];

  public final Label(Label index; // in-свойство
    Core:String value)
  of
    if value <> label[index] then
      label(index) = value;
      Redraw;
    end;
  end;

  public Redraw of // null-свойство = in-свойство
    // do some drawings
  end;
end;

// использование
updateLabels(EquipmentLabels equipment) of
  e = equipment.Labels[0]; // кантование out-свойства
  "on" partial equipment.Label(1);
  off partial equipment.Label(2);

  if e = 'кран' then
    "on" = 'Открыт'; // кантование in-свойства
    off = 'Закрыт';  // через частичное применение
  elsif e = 'лампа' then
    "on" = 'Включена';
    off = 'Выключена';
  end;
end;
Оператор partial без знака «равно» распространяет частичное применение одновременно на out- и на in-свойства. Это пока экспериментальный синтаксис.

Ключевое слово on пришлось заключить в кавычки, чтобы использовать его в качестве идентификатора.

4 декабря 2014 г.

Этюд 4. Разыменование ссылки

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

На Delphi

Этот этюд отступает от традиций: при его подготовке код переводился с Кантора на Delphi, а не наоборот. В результате пример на Delphi представлен псевдокодом, поскольку точный перевод с Кантора на Delphi невозможен.
var
  Five = 5;
const
  PtrToFive = @Five; // в Канторе это функция
begin
  Inc(Five);
end;

var
  Six := PtrToFive^; // присваивание с явным разыменованием

var
  A: ^Integer; // неинициализированная переменная

На Канторе

Присваивание ссылки не-ссылке есть разыменование, а присваивание одной переменной другой — всегда копирование (new from).
var five = 5;
ref ptrToFive = five;
ptrToFive++; // операций над ссылками в Канторе нет,
             // поэтому five теперь равно 6

var six = ptrToFive; // присвоение ссылки не-ссылке, то есть
                     // var six = new from ptrToFive;
                     // аналог конструктора копирования в Канторе
                     // вызовет исключение при разыменовании null

// Пример-ловушка
var Core:Integer a;  // описывает ссылку, поскольку понятия 
                     // неинициализированных переменных в Канторе нет,
                     // единственное умолчание -- это null, а null
                     // можно присвоить только ссылке
Пример-ловушка скорее всего будет считаться предупреждением и вызывать остановку компиляции в режиме строгой проверки синтаксиса (warnings as errors).

3 декабря 2014 г.

Каррирование → частичное применение

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

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

2 декабря 2014 г.

Этюд 3. Частичное применение переменной

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

На Delphi

В Delphi поля, методы и свойства класса являются самостоятельными сущностями, поэтому для описания перекрываемого свойства требуется обвязка в виде защищенных виртуальных методов GetIntValue и SetIntValue:
type
  TMyClass = class
  private
    FIntValue: Integer;
  protected
    function GetIntValue: Integer; virtual;
    procedure SetIntValue(Value: Integer); virtual;
  public
    property IntValue: Integer read GetIntValue write SetIntValue;
  end;

procedure TMyClass.GetIntValue: Integer;
begin
  Result := FIntValue;
end;

procedure TMyClass.SetIntValue(Value: Integer);
begin
  FIntValue := Value;
end;

На Канторе

С частичным применением эквивалентный код на Канторе получается экстремально коротким:
class MyClass of
  var Core:Integer intValue;
  public IntValue = partial intValue;
end;
Без частичного применения будет длинней, но всё равно короче, чем на Delphi:
class MyClass of
  var Core:Integer intValue;
  public out IntValue = intValue;
  public intValue(Core:Integer value) of
    intValue = value;
  end;
end;

21 ноября 2014 г.

Этюд 2. Частичное применение

Со времен Turbo Vision в Turbo Pascal были записи WordRec и LongRec для приведения типов. С появлением Delphi они перекочевали в модуль SysUtils, но смысл сохранился. Поскольку в практической разработке без прямого доступа к байтам и словам порой никак, некий аналог должен быть и в Канторе, и он сейчас послужит нам этюдом с частичным применением.

На Delphi

Задача типов WordRec и LongRec — подружить строгую типизацию с низкоуровневым доступом к байтам и словам. Возьмем вариант из модуля CoreUtils.
type
  WordRec = packed record
    case Byte of
      0: (Lo, Hi: Byte);
      1: (Bytes: array [0..1] of Byte);
  end;

  LongRec = packed record
    case Byte of
      0: (Lo, Hi: Word);
      1: (Words: array [0..1] of Word);
      2: (Bytes: array [0..3] of Byte);
  end;

На Канторе

Пример на Канторе адаптирован из разрабатываемого кода, в нем демонстрируется использование ключевого слова partial — оператора частичного применения.
public class Core:Typecast with inner of
  class ByteArray with inner of
    final class Bytes = 0..1;
    public out Core:Byte RawBytes[Bytes];
    public RawBytes[Core:Byte; Bytes];
  end;

  class ShortWordArray with inner of
    final class Bytes = 0..3;
    public out Core:Byte RawBytes[Bytes];
    public RawBytes[Core:Byte; Bytes];

    final class ShortWords = 0..1;
    public out Core:ShortWord RawShortWords[ShortWords];
    public RawShortWords[Core:ShortWord; ShortWords];
  end;

  class ShortWord from ByteArray of // аналог WordRec
    public LowNibble = partial RawBytes[0];
    public HighNibble = partial RawBytes[1];
  end;

  class LongWord from ShortWordArray of // аналог LongRec
    public LowNibble = partial RawShortWords[0];
    public HighNibble = partial RawShortWords[1];
  end;
end;
В сравнении с Delphi код вышел объемней, но описание промежуточных классов на Канторе требуется для автоматического учета порядка байт на разных архитектурах. Код настоящего Core:Typecast еще сложней из-за широкого использования обобщений, и для этюда его пришлось упростить.

Без частичного применения код двух последних классов будет выглядеть так:
public class Core:Typecast with inner of
  class ShortWord from ShortWordArray of // аналог WordRec
    public LowNibble = RawBytes[0];
    public LowNibble[Core:Byte value] of
      RawBytes[0] = value;
    end;

    public HighNibble = RawBytes[1];
    public HighNibble[Core:Byte value] of
      RawBytes[1] = value;
    end;
  end;

  class LongWord from ShortWordArray of // аналог LongRec
    public LowNibble = RawShortWords[0];
    public LowNibble[Core:ShortWord value] of
      RawShortWords[0] = value;
    end;

    public HighNibble = RawShortWords[1];
    public HighNibble[Core:ShortWord value] of
      RawShortWords[1] = value;
    end;
  end;
end;
Две строчки с частичным применением против восьми при расписывании вручную, — префикс partial одной строкой описывает сразу несколько свойств — все, к которым подходит частичное применение. Рутина сокращается, код становится чище и наглядней.

Подсветка синтаксиса

Публикация скриншотов с подсветкой синтаксиса Кантора становится традицией этюдов.
Добавлена подсветка синтаксиса, скриншоты удалены.

Этюд 1. WideParamStr — итераторы с охраной

Замена циклов на итераторы с охраной принята окончательно и находит постепенное воплощение в коде. В этом этюде сделана попытка переписать на Кантор функцию WideParamStr из CoreUtils.

На Delphi

type
  TWideParamRec = record
    NextParam, Param: PWideChar;
    Length: Integer;
    Quoted: Boolean;
  end;

function WideParamStr(CommandLine: PWideChar): TWideParamRec;
var
  P: PWideChar;
  L: Integer;
begin
  if CommandLine <> nil then
  begin
    while (CommandLine^ = WideChar(32)) or (CommandLine^ = WideChar(9)) do
      Inc(CommandLine);
    if CommandLine^ = WideChar('"') then
    begin
      Inc(CommandLine);
      L := WideStrLen(CommandLine);
      P := WideStrScan(CommandLine, L, CoreChar('"'));
      if P <> nil then
        L := P - CommandLine;
      Result.Quoted := True;
    end
    else
    begin
      P := CommandLine;
      while (P^ <> WideChar(32)) and (P^ <> WideChar(9)) and (P^ <> WideChar(0)) do
        Inc(P);
      L := P - CommandLine;
      Result.Quoted := False;
    end;
    with Result do
    begin
      NextParam := CommandLine + L + Byte(Quoted);
      if NextParam^ <> WideChar(0) then
        Inc(NextParam);
      Param := CommandLine;
      Length := L;
    end;
  end
  else
    FillChar(Result, SizeOf(Result), 0);
end;

На Канторе

Код не является буквальным эквивалентом собрата на Delphi: работа с локальными переменными заменена функцией-замыканием, чтобы сделать код более функциональным. Также введено ключевое слово outside для операции невхождения в набор (раньше она условно обозначалась сочетанием not in).
final class WideParamRec public of
  var ref Unsafe:WideChar NextParam, Param;
  var Integer Length;
  var Boolean Quoted;
end;

WideParamRec WideParamStr[ref Unsafe:WideChar commandLine] of
  if commandLine! then
    var ref cmdLine = commandLine;

    result[Integer len; Boolean quot] of // замыкание
      nxt = cmdLine + len + quot.AsByte;
      return new of
        NextParam = nxt + ?nxt.First{#quot, 1};
        Param = cmdLine;
        Length = len;
        Quoted = quot;
      end;
    end;

    cmdLine++ where cmdLine.First in {9, 32};
    if cmdLine.First = #quot.AsShortWord then
      cmdLine++;
      with
        len = cmdLine.StrLen;
        ptr = cmdLine.StrScan(#quot, len);
      return
        result[?ptr!{ptr - cmdLine, len}, True];
      end;
    else
      var ref ptr = cmdLine;
      ptr++ where ptr.First outside {32, 9, 0};
      return result[ptr - cmdLine, False];
    end;
  else
    return new from null;
  end;
end;
Планируется, что смысл where будет аналогичен SQL:
Упоминание членов охраняемого выражения обозначает итерации, а внешние по отношению к охране условия — немедленный выход, аналогичный оператору break.

Подсветка синтаксиса

Подсветки синтаксиса в блоге пока нет, а с ней этюд выглядит гораздо приятней.
Добавлена подсветка синтаксиса, скриншоты удалены.

4 июня 2014 г.

Итераторы и охранные выражения. Отличие охраны от следования

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

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

Главное отличие охраны от следования — проверка охраняемого выражения при каждом изменении его членов и немедленный выход из блока итерации при выходе значения за охраняемые границы. Итераторы с where позволят задать как сам цикл, так и условия выхода из него, делая операторы while и until ненужными.

Окончательное принятие итераторов и охранных выражений требует увязки с другими концепциями. Маловерятно, что удастся придумать суррогат для записи охраны операторами цикла. Это означает, что в байт-коде операторов цикла не будет, и компиляция традиционных алгоритмических языков в код Кантора потребует выработки правил превращения циклов в охраняемые итераторы.

Примера кода пока нет, поскольку охрану нужно давать вместе с итераторами, а синтаксис итераторов еще не додуман.

27 мая 2014 г.

Блочная форма записи условий

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

Решение языка Кантор — дать возможность записывать условия в виде блоков с отступом, разделяя отдельные предложения точкой с запятой. Поскольку на практике чаще всего используются связки and и or, блочные формы введены именно для них.

Блок all — блочный and

Для наглядности блочные условия рассматриваются на примере оператора if. Предположим, что у нас есть следующая функция:
with
  var Core:Word screenWidth = 1280, screenHeight = 1024;
do
  onScreenColor[Core:Integer x, y; Core:Word color] =
    if x >= 0 and x < screenWidth.AsInteger and y >= 0 and y < screenHeight.AsInteger then
      color
    else
      -1.RawWord
    end;
end;
Из-за строгости сравнения знаковых и беззнаковых целых условие получилось громоздким. Можно без всяких блоков записать его в две строчки, но в байт-коде они не сохранятся, и в будущем отекстовщик не будет знать, где поставить перевод строки.

Для преодоления этой проблемы в языке Кантор предлагается оператор all — блочная форма логического оператора and:
with
  var Core:Word screenWidth = 1280, screenHeight = 1024;
do
  onScreenColor[Core:Integer x, y; Core:Word color] =
    if 
      all
        x >= 0 and x < screenWidth.AsInteger;
        y >= 0 and y < screenHeight.AsInteger;
      end
    then
      color
    else
      -1.RawWord
    end;
end;
Как и везде, строчные и блочные операторы можно произвольно комбинировать, а блоки могут вкладываться друг в друга. Каждый блочный оператор требует завершающего end.

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

Блок any — блочный or

Пусть имеется некая функция с условием or:
onScreenMsg[Core:Integer what, where1, where2] =
  if what < where1 or what < where2 then
    'меньше'
  else
    'не меньше'
  end;
В блочной форме вместо or используется any:
onScreenMsg[Core:Integer what, where1, where2] =
  if 
    any
      what < where1;
      what < where2;
    end
  then
    'меньше'
  else
    'не меньше'
  end;
Тут также требуется завершающий end. Во всех примерах блочная запись используется внутри строчного оператора, поэтому завершающая точка с запятой после end не ставится.

26 мая 2014 г.

Оператор if как подмножество case

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

Приведение if к case

Поскольку булев тип является обычным перечислимым типом, оператор if может быть записан в форме case над булевым выражением:
if выражение then
  КогдаИстинно(1);
  КогдаИстинно(2);
else
  КогдаЛожно;
end;

case выражение
  when True then
    КогдаИстинно(1);
    КогдаИстинно(2);
else
  КогдаЛожно;
end;
Обе записи синтаксически допустимы, логически тождественны, и потому должны порождать идентичный байт-код.

Оператор следования

Выявленные условия заставляют пересмотреть синтаксис операторов if и case, описанный ранее.

Операторы if и case объединяются в единый оператор следования, имеющий общую форму case и сокращенную if, используемую для перечислимых типов с двумя значениями. Так if становится синтаксическим сахаром для для частного случая case, причем не только для булева типа.

Кроме того, поскольку ранее if допускал цепочки условий elsif, конструкция elsif теперь включается в общий оператор, становясь синтаксическим сахаром для вложенных if/case, без elsif требующих завершающий end на каждую вложенную ветвь.

Оператор следования теперь выглядит так:
case выражение1
  when значение1_1 then
    ветвь;
  when значение1_2 then
    ветвь;
  when значение1_3 then
    ветвь;
elsif выражение2 of
  when значение2_1 then
    ветвь;
  when значение2_2 then
    ветвь;
else
  ветвь;
end;
Блок elsif может повторяться много раз. Аналогичная запись без elsif потребует явного вложения со своим завершающим end:
case выражение1
  when значение1_1 then
    ветвь;
  when значение1_2 then
    ветвь;
  when значение1_3 then
    ветвь;
else
  case выражение2
    when значение2_1 then
      ветвь;
    when значение2_2 then
      ветвь;
  end;
else
  ветвь;
end;
В байт-коде обе формы неразличимы. Использование или неиспользование elsif в case может выступать как параметр отекстовки.

Форма записи оператора if не претерпела изменений, старое описание верно.

Решение проблемы unless

Найдено также элегантное решение для аналога оператора unless — достаточно лишь разрешить писать else сразу за условием if, тем самым сделав ветвь then необязательной:
if условие else
  ветвь;
end;
Поскольку ветвь else всегда стоит последней, эта форма if безальтернативна.

Пролог и эпилог

Предыдущая форма оператора case имела пролог — блок операторов, выполнявшийся перед операторами ветви, если хоть одна ветвь выбрана. Необходимость в прологе, а также возможное добавление эпилога по ключевому слову default рассматривалось, но в данный момент выглядит спорным. Вместо пролога и эпилога всегда можно использовать замыкания, задаваемые префиксом with, придется лишь дать им имена. Стоит ли экономия на именах введения новых сущностей, пока решается. В данной статье оператор case записан без of, подразумевая отсутствие пролога.

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

24 мая 2014 г.

Ссылочные выражения требуют обсуждения

Реализация концепции ссылочных выражений в Канторе требует обсуждения с независимым оппонентом, понимающим, что такое указатели. Лимит обсуждений с самим собой временно исчерпан, увы. Лучше, если обсуждение будет голосовым.

Желающих оппонировать прошу писать на почту или в Jabber. В Skype выхожу по договоренности.

15 мая 2014 г.

Будет ли unless?

Разработка байт-кода несколько продвинулась, и теперь можно дать ответ на вопрос, будет ли в Канторе оператор unless — обратный if, в котором ветка then исполняется при ложности условия. Похожий оператор есть в языке Руби.

Для оператора unless не хватает битов

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

Байт-код Кантора представляет собой вариант синтаксического дерева (AST), узлы которого кодируются не командами виртуальной машины, а набором состояний. В байте 8 бит, тем самым можно закодировать восемь одновременных состояний, среди которых есть Conditional, Ordered и Reversed, а также Parameters, совмещенный с Reversed. Смысл состояний следующий:
Parameters = Reversed

{Conditional}             // if
{Conditional, Parameters} // case, пересекается с {Conditional, Reversed}

{Conditional, Ordered}           // while
{Conditional, Ordered, Reversed} // repeat
Из примера видно, что набор битов для кодирования оператора case совпадает с набором битов для unless. Коллизия вполне ожидаемо разрешается исключением unless из возможного набора операторов.

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

26 января 2014 г.

Общий и чистый вызовы

Рассмотренная ранее трактовка параметров функций в Канторе объединяет понятие вызова функции с трактовкой порожденных функций как синонимов. На уровне модели (байт-кода) обобщенная форма обращения к функциям носит название общего вызова.

Обращение к чистым функциям, четко разделяемое на левую и правую части, может быть записано в альтернативной, более привычной форме, — при помощи оператора сопоставления (знака равенства). Форма записи в виде оператора сопоставления называется в Канторе чистым вызовом.

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

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

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

Предметно-ориентированные языки как подключаемые модули

Технология подключаемых синтаксических модулей (plugins) уже была опробована на вики и хорошо себя зарекомендовала, обеспечив нужную расширяемость и гибкость. Разрабатывая язык общего назначения, хочется и его сделать его максимально расширяемым, для чего и примени́м механизм модулей, подключаемых к компилятору и отекстовщику.

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

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

Как и в вики, для подключаемых языков в Канторе выделена специальная синтаксическая конструкция.

Базовый синтаксис

Поскольку подключаемые модули пока могут реализовывать только описательные языки, им предлагается резервировать мнемонические символы, — они станут тегами предметно-ориентированного описания:
{тег предметно_ориентированное_описание тег}

Текст

Минимальным предметно-ориентированным «языком» является простой текст, — его модуль должен лишь уметь разбивать входной поток на отдельные строки в соответствии с правилами окружения, из которого был запущен компилятор. Мнемоническим тегом текста является апостроф:
txt = {'
  Это блок простого текста в естественном виде на Канторе.
  Сюда можно вписывать цитаты великих людей, если есть что вспомнить.

  Компилятор признаёт только одного великого человека — программиста.
'};
В примере описана функция txt, возвращающая объект типа :Core:Text, состоящий из четырех текстовых строк и имеющий предметно-ориентированную запись. Без модуля пример будет выглядеть так:
txt = new :Core:Text of
  += 'Это блок простого текста в естественном виде на Канторе.';
  += 'Сюда можно вписывать цитаты великих людей, если есть что вспомнить.';
  += '';
  += 'Компилятор признаёт только одного великого человека — программиста.';
end;

Вики

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

Мнемоническим символом вики-разметки является знак равенства:
wk = {=
  == Это вики-заголовок ==
  Сюда тоже можно вписывать цитаты, если --хочется //отжечь//-- есть что вспомнить.
=};
Как и в случае с текстом, получаемый результат проще всего продемонстрировать кодом на Канторе в отсутствие подключаемого модуля вики-разметки (имена классов и свойств на данный момент условны):
wk = new :Core:Wiki:Document of
  += new :Core:Wiki:Block of
    Heading = True;
    += new :Core:Wiki:Text of
      += 'Это вики-заголовок';
    end;
  end;
  += new :Core:Wiki:Block of
    += new :Core:Wiki:Text of
      += 'Сюда тоже можно вписывать цитаты людей, если ';
      += new :Core:Wiki:Text of
        Style = Strikeout;
        += 'хочется ';
        += new :Core:Wiki:Text of
          Style = Emphasis;
          += 'отжечь';
        end;
      end;
      += ' есть что вспомнить';
    end;
  end;
end;

HTML, RTF, TeX

Подключение модулей HTML, RTF и TeX предполагается аналогичным образом.

За HTML зарезервированы символы < и >, а набор классов HTML в Канторе планируется сделать полностью соответствующим W3C DOM. Вероятно, возможности модуля HTML будут расширяться по мере реализации функциональности браузера на Канторе, постепенно вбирая в себя CSS, JavaScript и пр.

За RTF и TeX зарезервирован один и тот же мнемонический символ — \ (обратный слеш). Разрешение этой коллизии и проработка остальных деталей реализации оставлены на будущее.

Неописательные языки

Уже говорилось, что технология предметно-ориентированных языков охватывает только описательные языки (либо языки, реализуемые через описательные, что может быть справедливо для JavaScript). Это логично, поскольку упрощение синтаксиса наиболее востребовано в специализированных (предметных) областях и не должно затрагивать базовую парадигму разработки на Канторе.

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

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

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

Поэтому расширение синтаксиса блоками на других языках программирования не планируется (за исключением ассемблеров, вопрос с которыми еще не решен). В качестве альтернативы возможна эвристическая компиляция исходных текстов на других языках (Delphi, Си, Haskell и др.) в байт-код Кантора, работающая только в одном направлении, с дальнейшей отекстовкой и просмотром уже в синтаксисе Кантора. Есть гипотеза, что просмотр «чужих» библиотек в синтаксисе Кантора может оказаться даже удобнее, чем оригинальный код, особенно с целью изучения и анализа.

16 января 2014 г.

Параметры функций

Концепция параметров функций в Канторе довольно сильно отличается от общепринятой как в теории, так и на практике. Виновница несовместимости — фрактальная модель, в рамках которой переменные и функции объединились в одну сущность, а доступ к параметрам идет не по значению и ссылке, а на прием, возврат и мутацию, что отдаленно напоминает концепцию триггеров в СУБД.

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

Прием, чистота, мутация

Параметры функций в Канторе делятся на три вида — по способу доступа к ним:
  • Входные параметры не имеют ключевого слова. Функция, имеющая только входные параметры, является приемником. Будучи описанной внутри класса, функция-приемник становится свойством-приемником и получает возможность изменять контекст класса или объекта.
  • Выходные параметры обозначаются ключевым словом out. Наличие хоть одного выходного параметра делает функцию чистой и запрещает изменять контекст класса или объекта, если функция описана как свойство.
  • Мутирующие параметры или параметры-переменные обозначаются ключевым словом var. Наличие хоть одного мутирующего параметра делает всю функцию мутирующей — позволяет ей изменять контекст (как для приемника), но делает невозможным обращение к функции при помощи оператора присваивания, разрешая только общий вызов. Наличие var-параметров имеет наивысший приоритет при определении вида функции.
Таким образом, наличие тех или иных параметров накладывает ограничение на функцию целиком, порождая соответственно функции-приемники, чистые функции и мутирующие функции. Функции без параметров, в других языках называемые процедурами, в Канторе являются также мутирующими — var-функциями.

Именованные и анонимные параметры

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

Пример с числом Авогадро из предыдущей статьи может быть записан в эквивалентных формах с ключевым словом out:
out avogadro = 6.022e23;  // неявный анонимный параметр
avogadro(out) = 6.022e23; // явный анонимный параметр
Форма с параметром в скобках является номинативной — именно так хранится функция в байт-коде. Прочие формы записи — синтаксический сахар.

Функция подсчета молярной массы, имеющая входной параметр, в полной форме будет выглядеть так:
mole(out; :Core:Real atomicWeight) = atomicWeight * avogadro;
Несмотря на наличие входного параметра atomicWeight, анонимный выходной параметр делает ее чистой, out-функцией.

Обе функции можно записать в эквивалентной форме с оператором return:
avogadro(out) of
  return 6.022e23;
end;

mole(out; :Core:Real atomicWeight) of
  return atomicWeight * avogadro;
end;
В байт-коде все формы записи идентичны, при отекстовке можно будет выбрать предпочитаемую.

15 января 2014 г.

Функции как синонимы

Реализация функциональной парадигмы в Канторе испытала сильное влияние SQL. SQL-запросы истолкованы автором как чистые функции, синонимы внутри select — как замыкания, а сам SQL признан самым популярным языком функционального программирования и годным источником заимствований. Простота объявления локальных имен, взятая из SQL, получила условное наименование программирования через синонимы.

Синонимы в SQL

Программиста на любом императивном языке, будь то Паскаль или Си, поначалу удивляет простота объявления синонимов полей и таблиц в запросах и командах SQL:
select
  a.code, b.subcode, a.name, b.name subname
from
  data a,
  subdata b
where
  a.code = b.code;
В этом примере subname — синоним поля subdata.name, а a и b — синонимы таблиц data и subdata. Синонимы обладают всеми атрибутами замыканий: описываются по месту использования, не требуют присваивания им значения, и, что самое важное, не создают накладных расходов на выполнение запроса, что подтверждается планом СУБД.

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

Чистые функции

В языке Кантор любое выражение является чистой функцией. Функции можно дать имя, сопоставив ей идентификатор, имеющий тот же смысл, что и синоним в SQL:
avogadro = 6.022e23;
sodium = 22.89;

mole[:Core:Real atomicWeight] = atomicWeight * avogadro;
moleOfSodium = mole[sodium];

megabyte = 1K**; // 1024²
В Паскале объявление аналогов функций avogadro, sodium и megabyte считалось бы объявлением констант при помощи константных выражений. В Канторе функции первичны, а константы являются частным случаем чистых функций. Чистота функций означает, что:
  • Сопоставление выражения функции делается один раз — при описании. Знак равенства тут нужно трактовать как символ синонима, а не как оператор присваивания.
  • Использование функций не влечет дополнительных накладных расходов. В самом простом случае компилятор вставляет тело функции в место ее вызова.
В SQL объявления аналогов avogadro, sodium и megabyte считались бы константными таблицами, а mole и moleOfSodium — представлениями.

Как принято в функциональных языках, тип результата функции может быть опущен, что задействует вывод типов компилятором. В нашем примере функция megabyte получит тип :Core:Int[32], а остальные функции — тип :Core:Real.

Функции, реализованные памятью

Кантор — функциональный язык системного программирования, что заставляет ввести в него понятие переменной непосредственно, не пряча за абстракциями-обертками. Чтобы не сломать первичность функций и не вносить исключений в правила, для переменных в Канторе принята оригинальная трактовка:
Переменные являются функциями, реализованными памятью.  
Превратить функцию в переменную можно, «защелкнув» ее ключевым словом var:
var substance = moleOfSodium;
Память позволяет сопоставлять значение функции много раз, что вполне ожидаемо дает побочный эффект. Сопоставление значений переменным является полноценным присваиванием, но записывается как и для чистых функций — знаком равенства:
substance = mole[15.99]; // oxygen
substance = mole[55.85]; // ferrum
То есть, синтаксически и семантически присваивание является частным случаем сопоставления значения функции, и трактовка знака равенства как объявления синонима или присваиваения зависит от того, что стоит в левой части сопоставления — чистая функция или переменная.

В SQL аналогом переменных являются таблицы, а аналогом переменных с начальным значением — материализованные представления, состоящие из таблицы и связанного с ней представления одновременно.