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;