Logo

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, подразумевая отсутствие пролога.

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

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 г.

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

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

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

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

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 аналогом переменных являются таблицы, а аналогом переменных с начальным значением — материализованные представления, состоящие из таблицы и связанного с ней представления одновременно.