вторник, 20 июня 2017 г.

Целочисленные типы

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

Целочисленные типы должны быть разделены на две группы:
  1. Ограниченное число предопределённых типов, подходящих для большинства задач, и к которым применимы встроенные арифметические операции: +, -, *, /, %.
  2. Типы из разделов, для работы с которыми необходимо использовать функции из тех же разделов с соответствующими названиями: add, sub, mul, div, mod и т.д.

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

Предопределённые типы

В первую группу должны войти:
НазваниеОтрезок допустимых значений
int -(231 - 1) .. (231 - 1)
longint -(263 - 2) .. (263 - 2)

int и longint — знаковые, симметричные относительно 0 типы. Равенство по модулю максимального и минимального значений упрощает учёт правильных вычислений, в частности, гарантирует, что смена знака и значение по модулю всегда выполнимы без переполнения. Это важней желания вместить на одно допустимое значение больше, как это позволяет наиболее распространённый способ кодирования отрицательных чисел — дополнительный код. В большинстве случаев минимальное отрицательное (- 231, - 263) даже не нужно решения задачи, но непропорционально своему значению требует избыточного внимания в случае необходимости достижения полной правильности, из-за создания особого случая. Эти значения зарезервированы для использования в качестве неопределённого значения.

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

byte занимает особое место — он нужен в первую очередь как строительный элемент, и сам по себе не является целочисленным типом, но легко может быть приведён к нему, равно как и в обратном направлении. byte соответствует отрезку значений 0 .. 28 - 1.

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

Беззнаковые типы по размеру совпадающие с int и longint не включены в предопределение по схожей причине. Кроме того, близость 0 — нижней границы повышает вероятность переполнения даже в случае вычисления малых значений.

***

В арифметических выражениях, в которых смешиваются операнды разных целочисленных типов, операнды меньшего типа рассматриваются как эквивалентные операнды наибольшего. Вычисления выражений для операндов int и longint происходят в диапазоне longint, кроме тех операций с int, которые гарантировано не приводят к увеличению необходимого диапазона, например, сложения целых, приведённых от byte, или просто при делении.

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

Например, следующие операции, которые во многих других языках могут содержать ошибки переполнения при определённых значениях переменных, в защитном языке оказываются всегда правильны:

Защитный языкJava
var (a,b,c,m int; d longint; f bool)

a = max(int) / 2; 
b = a + 2; 
c = 300;
m = min(int);

f = a + b < c; // false
d = a * b; // 1152921504606846975
c = b * 2 / 3; // 715827883
m = -m;        // 2147483647
int a, b, c, m; long d; boolean f;

a = Integer.MAX_VALUE / 2;
b = a + 2;
c = 300;
m = Integer.MIN_VALUE;

f = a + b < c; // true
d = a * b;     // -1
c = b * 2 / 3; // -715827882
m = -m;        //-2147483648

Типы предопределённых разделов

Вторую группу целочисленных типов должны представить типы из состава специальных разделов, отличающихся знаковостью и разрядностью гарантированных диапазонов. Разделы должны предоставлять почти одинаковые наборы функций для работы с собственными целочисленными типами. Имена разделов можно вывести из синтаксического уравнения – имя = [u]int(8|16|32|64), где u обозначает беззнаковость, а число, естественно — разрядность типа. Диапазоны допустимых значений определяются по формулам - [0 .. 2n-1] для беззнаковых и [-2n-1 .. 2n-1-1] для чисел со знаком, так как они должны быть закодированы через двоичное дополнение.

Объявления разделов:
type (t) // сам целочисленный тип

const (min, max) // минимальное и максимальное значение

// Группа функций, которые трактуют переполнение как ошибку.
// Если при их выполнении не было явно получено состояние правильности,
// то в случае переполнения они завершают выполнение программы.
func add(addend1,  addend2 t)    (sum t);
func sub(minuend,  subtrahend t) (difference t);
func mul(factor1,  factor2 t)    (product t);
func div(divident, divisor t)    (ratio t);
func mod(divident, divisor t)    (remainder t);

// Группа процедур, для которых переполнение сопровождается воображаемым
// отбросом старших разрядов.
proc mod_add(addend1, addend2,    > sum t)        (overflow bool);
proc mod_sub(minuend, subtrahend, > difference t) (overflow bool);
proc mod_mul(factor1, factor2,    > product t)    (overflow bool);

// Функции для преобразования к основным типам и обратно. Поскольку при
// этом может возникнуть переполнение, то функции также подразумевают
// явное взятие правильности, либо завершение работы.
func to_byte   (value? t) (result byte);
func to_int    (value? t) (result int);
func to_longint(value? t) (result longint);

func from_byte   (value byte)    (result t);
func from_int    (value int)     (result t);
func from_longint(value longint) (result t);

// Функции интерпретации стандартных типов как соответствующих их
// диапазону типов из разделов противоположной знаковости и наоборот.

// в разделе uint64:
func as_int64  (value t) (result int64.t);
func as_longint(value t) (result longint);
func as_t(value longint) (result t);

// в разделе uint32:
func as_int32  (value t) (result int32.t);
func as_int    (value t) (result int);
func as_longint(value t) (result longint);
func as_t    (value int) (result t);

// в разделе int8:
func as_byte   (value t) (result byte);
func as_int    (value t) (result int);
func as_longint(value t) (result longint);
func as_t   (value byte) (result t);

Битовые операции для встроенных целочисленных типов не предусмотрены как неуместные. Вместо них вводятся:

  1. Встроенная функция для возведения в степень func pow(v, n int) (res longint) или отдельная операция v**n. Выбор будет сделан позднее.
  2. Типы-множества, которые можно приводить к целочисленным и обратно и о которых подробней будет рассказано в отдельной заметке.

Дополнительные материалы:

  1. Danger – unsigned types used here!
  2. INT14-C. Avoid performing bitwise and arithmetic operations on the same data.

среда, 28 декабря 2016 г.

Состояние правильности

В программировании часто возникает задача проверки правильности входных данных, которые могут привести к одному из нежелательных последствий, вроде арифметического переполнения, выход за границы массива и других. Особенно остро стоит проблема для задач, связанных с безопасностью, так как через подачу неправильных данных можно добиться непредусмотренного поведения. Явная проверка правильности нередко требует действий, сопоставимых по объёму с основными действиями. Это приводит к частому игнорированию проверок или их неправильному составлению, что также плохо выявляется тестированием, так как каждые по отдельности случаи неправильных данных, как правило, встречаются реже правильных.

Необходим механизм, который позволит сделать проверки синтаксически более легковесными. Для языка защищённого программирования таковым становится явно выведенное состояние правильности.

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

Рассмотрим для примера сложение — так выглядит оно без явных проверок:

sum = a1 + a2;

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

if (a2 >= 0) 
    ok = a1 <= max(int) - a2
else
    ok = a1 >= min(int) - a2
end;
if (ok)
    sum = a1 + a2
end

Или, при наличии специальной функции, такого кода:

ok = add(> sum, a1, a2);

Язык предлагает такую запись для выполнения действия и явного получения его правильности:

ok ?? sum = a1 +? a2;

Лексема «??» служит для обозначения присваивания состояния правильности, а «?» придаёт операции возможность передать это состояние в соответствующую логическую переменную. Без этого знака поведение операции будет обычным несмотря на наличие «??».

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

Необходимость же введения специальных знаков вместо использования функций, вроде указанной add, объясняется на более сложных примерах. Незащищенный проверками код:

a = b[i] * b[0] + c;

Код с проверками на функциях:

ok = (i >= 0) & (i < len(b)) & mul(> m, b[i], b[0]) & add(> a, m, c);

Новый подход:

ok ??  a = b[?i] *? b[0] +? c;

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

Действия, для которых доступна проверка с помощью «?»:

  1. Арифметика: «+?», «-?», «*?», «/?», «%?»
  2. Арифметика совместно с присваиванием: «+?=», «-?=», «*?=», «/?=», «%?=»
  3. Обращение к элементу массива: array[? i], matrix[? y][? x]
  4. Выделение динамической памяти тоже можно сделать с показателем правильности для избежания лишних проверок на null: new(param)?
  5. Обращение к элементу структуры, заданной указателем, который может быть пустым: pstr .? item
  6. Вызов функции, которая может возвращать состояние неправильности: fun(param)?
  7. Вызов функции с проверкой правильности параметра: fun(? param)
  8. Вызов рекурсивной функции при исчерпании допустимой глубины вызовов recursive(? deep) fun(param)
  9. Преобразование типа: int(? rational)
  10. Уточнение динамического типа — переход от основы к расширению (от предка к наследнику): base.(? extended)

четверг, 28 июля 2016 г.

Объявления и области видимости

В этом вопросе язык для надёжного программирования ближе всего к Оберону.

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

Объявление-d, сделанное на уровне раздела-s доступно по имени от начала его появления в тексте до конца s. В других разделах-so d доступно только в том случае, если оно было помечено как экспортируемое в s, и только в тех из них, где s был импортирован. Доступ к d в so осуществляется черезимпортированное имя s - ins, следующей за ним разделяющей точкой и именем d

section s {
  const(c = d + 1; // ошибка - d ещё не объявлена
       +d = 0;     // пометка объявления "+" как экспортированное
        e = d + 1)
  
  proc p() {
    const(f = d;
          e = 6) // ошибка - е уже объявлено в этой области уровнем выше
    ...
  }
}.

section so {
  import (ins = so) // переименовываем, s недоступна для использования
  const (d = ins.d;
         e = so.d;  // ошибка, раздел s был переименован для использования
         f = ins.e) // ошибка, объявление е не было экспортировано в s
  
  proc p() {
    { const (a = 3)
      ...
    }
    { const (a = 0.6)// ошибка - хотя a=0.6 объявлена в другой области
      ...            // видимости, но в пределах одной процедуры,
    }                // что запрещено
  }
}.
Имя объявления, сделанного на уровне структуры должно быть уникальным лишь в пределах самой структуры. Имя может совпадать как с предопределёнными именами, так и с именами, объявленными на уровне раздела, функций и других структур, в том числе вложенных, так как из-за строго иерархического обращения к элементам структуры не может быть никакого кофликта имён.
section a {
  const (b = 1)
  var (r struct {
           r struct {
              r, b int
           }
         }
      )
  proc p() {
    r.r.r = 0;
    r.r.b = b
  }
}

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

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

section a {
  const(+a = 10)// обращение к собственному модулю не предусмотрено, 
                // поэтому имя объявления может совпадать с именем модуля.
  proc +b() {
    const(c = a;
          d = 4)// d объявлена ниже, поэтому не входит в текущую 
    ...         // область видимости
  }
  
  proc +d() { // область видимости d = 4 закончилась внутри b()
    const(c = a + 1)// хотя с уже встречается в b(), но так как области 
                    // b() и d() не пересекаются, то объявление правильно. 
    ...
  }
}

Запрет на перекрытие имён и возможность иметь несколько конкретизаций синтаксисов создают трудности, требующие разрешения:

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

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

section @const { // @ как в C#
  const (+@section = 1)
}

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

@section m {
  @const (c = 1)
  @type  (t @int)
  @var   (v t)
  
  @func f(i t) (res @bool) {
     res = i < @max(t) / 2
  }
}

понедельник, 18 июля 2016 г.

Разделяемость (модульность)

Организующей программной единицей является раздел. В ряде языков, например, в Oberon их называют модулями [0].

section name {
 /* Это место перечисления используемых разделов 
    и упорядоченных объявлений в строгом порядке:
    констант, типов, переменных, функций,
    отдела начализации переменных */
}.
Oberon
MODULE name;
 (* Это место перечисления используемых разделов 
    и упорядоченных объявлений в строгом порядке:
    констант, типов, переменных, функций,
    отдела начализации переменных *)
END name.

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

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

section m1 {
  const (+a = 0;/* экспорт обозначен «+» перед именем */
          b = a + 1)
  ...
}.

section m2 {
  import (m1) /* доступ к элементам раздела через двоеточие */
  const (a = m1:a;/* a = 0 */
         b = m1:b)/* ошибка - константа "b" не экспортирована в "m1" */
  ...
}.

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

section m1 { import (m2) }.

section m2 {
  import (m1)/* ошибка - m1 уже ссылается на m2 */
}.

Назначение разделов

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

  • Раздел может использоваться исключительно как вспомогательный, не влияя на интерфейс импортирующего раздела. Использование такого раздела может быть заменёно другим кодом, выполняющим те же задачи.
  • Раздел, являющийся частью интерфейса импортирующего его раздела. Объявленные в нём типы могут становиться видимой частью экспортируемых объявлений импортирующего.
  • Обобщённый раздел-заготовка, чьё определение задаётся не только его непосредственным содержимым, но также и импортированными разделами-параметрами.
  • Опциональные разделы, наличие которых можно проверить в коде. Могут использоваться либо для создания более ограниченной версии раздела, либо для задействования более эффективных средств, не приводя к жёсткой зависимости от них.
  • Декоративные разделы, служащие для удобства использования. Они каким-либо образом перестраивают взаимодействие с основным функционалом, практически не меняя его основной сути, например, объединяя интерфейсы разных разделов.

Поскольку разделов, особенно с учётом сторонних библиотек, может быть много, то возникает необходимость в создании иерархий:

section a/m  { const (+c1 = 1) }.
section a/m2 { const (+c2 = 2) }.
section b/m  { const (+c3 = 3) }.

section m {
  import (
    a/m;  /* в качестве идентификатора раздела после импорта     */
    a/m2; /* используется только вторая часть имени после точки */
    bm = b/m/* переименование, чтобы избежать ошибки совпадения имён */
  )
  const (c4 = m:c1 + m2:c2 + bm:c3)
}.

Версии разделов

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

В примере версии разделов указаны напрямую, но также их можно задать в проекте маршрутизации разделов[1], продолжая оперировать только именами в коде самих разделов.

section lib.0.1 { ... }
section lib.0.2 { ... }

section a { import (lib.0.1) }
section b { import (lib.0.2) }

section u { import (a; b; lib1 = lib.0.1; lib2 = lib.0.2) }

Разграничение, подразделы

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

/* path — это имя раздела, а name, clear и all — имена подразделов, 
  открывающие дополнительный функционал пользователям path, 
  которым дан доступ к этим подразделам и если 
  они их указывают при импорте */
section path +name+clear+all {

  type (+t) /* можно обозначить, не раскрывая содержимое */

section +name+clear+all:

  type (+t = { +name string })

section +clear+all:

  type (+t = { +up (*)t })

section +all:
  /* Только в этом подразделе позволено задавать путь, и без доступа 
    к нему путь к произвольным ресурсам закрыт по определению */
  proc +new(name string, indir (*)t) (path *t) { ...;
    path.name = name; path.up = indir }
  proc +str(spath string) (path *t) { ... }

} path.

section editor {
  import (
    /* path+all; */ /* после импорта должен быть доступен по имени path, 
                        но у editor нет доступа к полному разделу, 
                       и здесь была бы ошибка трансляции */

    path; /* эта часть позволяет получать доступ к ресурсам, пути к
            которым явно переданы параметрами, но не позволяет 
            самостоятельно указывать, с какими ресурсами можно работать, 
            как и не даёт доступа к самому имени */
    io 
  )
  proc +do(file path:t) {
    ... io.open(file)
    ...
    // io.open(path.str("~/.ssh/id_rsa")) // ошибка трансляции
  }
}.

Подразделы могут вносить следующие изменения относительно предыдущих подразделов:

  • Добавление объявлений, включая элементы в ранее объявленные типы.
  • Добавление экспортированности ранее закрытым объявлениям, включая отдельные элементы.

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

Трансляция

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

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

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

$ sc build util.do

Что приведёт к созданию исполнимого файла util, полученного из одноименного раздела с точкой входа do — экспортированной процедуры без параметров.


Сноски:

[0] Почему раздел — это не модуль/unit?

Необходимо ответить на вопрос: «Если в разделе несовместимо изменилось экспортированное объявление, должна ли потеряться совместимость с разделами, использующими только неизменившиеся его объявления?»

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

[1] Маршрутизация разделов в защитном языке.
[2] Оберон умер, да здравствует Оберон! Часть 2. Модули
[3] Михаэль Франц Динамическая кодогенерация: ключ к разработке переносимого программного обеспечения
[4] Webassembly: Design Rationale

воскресенье, 17 июля 2016 г.

Синтаксис

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

Язык не обязан иметь лишь одну терминальную часть синтаксиса, которая отвечает за условную кодировку программы - вполне возможна такое воплощение средств программирования, которое позволяло бы конкретные детали синтаксиса выбирать по усмотрению разработчика, оставляя неизменной лишь ту центральную часть, которая ответственна за определения конструкций языка. Хотите C-подобный вид программ — пожалуйста. Больше по вкусу паскалевский подход — и это без проблем. Нужны ключевые слова на родном языке для обучения — и это возможно. Более того, исходный код не обязан быть представлен в виде традиционного печатного формата, а может использовать более богатое полиграфическое представление, а также и не обязан вообще иметь строковое представление, а быть оформленным, к примеру, в виде диаграмм. В развитых средствах программирования возможен учёт особых потребностей людей с инвалидностью, например, слепых или страдающих ДЦП.

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

Необходимость угодить среднестатистическому кодировщику не оставляет выбора — основная лексика должна быть Би-подобный как ныне наиболее популярная. Именно в языке B Кен Томпсон заложил основы вида кодировки, которая сейчас известна благодаря С.

Обсуждение хороших свойств некоторых языков в половине случаев застревает в обсуждении чего-то подобного begin и end, поэтому можно просто дать программистам их любимые {}, и вместо ненужных споров сосредоточиться на главном. Единственное, что нужно сделать, учитывая особенности Си — это привести его синтаксис к более понятному и ошибкоустойчивому виду, наподобие того, как это получилось у создателей Go. Собственно, ради экономии энергии в первом приближении можно взять синтаксис Go за основу, но без трепетного отношения ко всем его решениям, так как иная семантика связана с иным синтаксисом.

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

понедельник, 11 июля 2016 г.

Архитектура языка

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

  1. Правильность исходного кода
  2. Простота использования языка
  3. Потенциальняя лёгкость воплощения транслятора
  4. Эффективность выходного кода

Учитывается, что:

  1. Язык ценен не только тем, что в нём есть, но и тем, чего в нём нет
  2. Добавить новую возможность легче, чем удалить старую
  3. И маловероятные ошибки могут становиться большими проблемами
  4. Возможность негарантированного обнаружения ошибки лучше гарантированного необнаружния

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

Конструктивные особенности:

  1. Синтаксис
  2. Разделяемость
  3. Маршрутизация разделов
  4. Объявления и области видимости
  5. Подпрограммы
  6. Разновидности состояния переменной
  7. Виды постоянных
  8. Состояние правильности
  9. Ошибочное состояние
  10. Присваивание
  11. Утверждение
  12. Целочисленные типы

Другое:

воскресенье, 10 июля 2016 г.

Свойства. Цельный

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

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

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

четверг, 7 июля 2016 г.

Свойства. Эффективный

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

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