В связи с оживлённой дискуссией в соседней теме
viewtopic.php?f=30&t=6344&start=180#p115095 хочется обсудить точки с запятой в отрыве от конкретного проекта, но в контексте Оберонов.
Краткое содержание предыдущих серий:
есть множество языков, которые прекрасно живут без точки с запятой. Однако некоторые, такие как JavaScript - не прекрасно. Помимо упомянутых в теме про Ofront, я наутро вспомнил ещё Transact-SQL. Там нет точек с запятой и конец строки тоже не имеет значения, но там любое предписание (предложение, statement) начинается с ключевого слова, в том числе и присваивание. Это облегчает задачу разбора текста, даже по сравнению с Обероном. Вообще, синтаксис легко читается, когда для его чтения не нужен глубокий стек. Именно поэтому VAR a : array 5 of pointer to array 5 of integer гораздо лучше, чем ... я что-то вообще не могу записать это на Си, редко пишу на этом языке. Как-то так, что ли? int ((*a)[5])[5] (честно скажу, я написал это с помощью компилятора и всё равно не уверен, что правильно).
Код:
#include <stdio.h>
int main()
{
// array 5 of pointer to array 5 of integer
int b[5];
int (*pb)[5];
int ((*a)[5])[5];
pb = a[0];
b[0] = (*pb)[0];
int z = b[0];
return 0;
}
В целом см. картинку.
Вложение:
нос-утконосонос.png [ 277.13 КБ | Просмотров: 5246 ]
В Си типы и читаются, и пишутся не с начала, не с конца, а откуда-то из середины. В голове нужно поддерживать довольно сложный парсер, чтобы читать код на Си. Это здорово, когда надо померяться, у кого длиннее извилины в мозгу, но не здорово, когда у нас ремесло и нужно получить результат - работающую программу. И потому новые языки - Go, Rust и Котлин - перешли к паскалевскому стилю определения переменных.
Так вот, с типами в Обероне всё в порядке, но присваивание это всё же десигнатор := выражение, а при том десигнатор может быть каким-то раскудрявым, например,
Код:
MODULE Init;
IMPORT Log;
VAR a: ARRAY 5 OF ARRAY 5 OF INTEGER;
PROCEDURE b():INTEGER;
BEGIN
RETURN 1
END b;
PROCEDURE Do;
BEGIN
a[3+a[2][2]][0]:=b();
Log.Int(a[3+b()][0]); Log.Ln;
Log.Int(b()); Log.Ln;
END Do;
BEGIN
Do();
Do
END Init.
(программа проверена на
https://visual.sfu-kras.ru/)
В этом случае, предписание (statement) в Обероне так же нужно читать из середины, как и выражение-тип в языке Си. Например, это необходимо, чтобы отличить присваивание от вызова процедуры даже при беглом просмотре кода.
А теперь представим себе такой кусок кода без точек с за
пятыми:
Код:
Log.Int(a[3+b()][0]) a[3+a[2][2]][0]:=b()+a[3-b()][2] Log.Int(a[3+b()][0])
Такой код уже не так уж легко прочитать без разделителей.
В общем, я подумал-подумал и всё же склоняюсь к варианту, что:
- ";" обязательна
- ";" подразумевается в конце строки
- ";" подразумевается перед END
- пустое предписание (statement) запрещено
Всё. Если предписание слишком длинное - то уповаем на возможности среды разработки по переносу строк.
Последнее правило нужно с той целью, чтобы разделители выставлялись однозначно и не имел бы смысла код
Код:
ОченьМногозначительныйВызов;;;;;;;;;;;;;;;;;;;;;;
и чтобы было неповадно ставить точку с запятой в конце строки там, где она не нужна.
Я долго думал над продолжением строк с помощью "\" - оно присутствует в таком уважаемом мной языке, как tcl. Однако на практике это выглядит почему-то очень уродливо. Например, потому, что если не выравнивать
пилу из "\" после каждой правки, то получается уродливо:
Код:
#define max(a,b) \
({ __typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a > _b ? _a : \
_b; })
В принципе есть ещё один вариант, не совсем хороший: не в конце строки ставить знак продолжения, а в начале следующей:
Код:
#define max(a,b)
|({ __typeof__ (a) _a = (a);
| __typeof__ (b) _b = (b);
| _a > _b ? _a :
|_b; })
В вышеприведённом примере я бы ещё обратил внимание на Do/Do() - здесь как раз скобки были сделаны необязательными ради чистоты кода (другой причины я не вижу), но это приводит к тому, что в некоторых случаях компилятор путает вызов процедуры и её адрес. Это далеко не всегда безобидно, в АО скорее всего есть грабли, что
Код:
PROCEDURE P:ADDRESS; BEGIN RETURN 0 END;
BEGIN
Z := P + P()
END
Здесь оба вызова P и P() имеют смысл, но разный (ща проверю, кстати).
P.S. С END получается нехорошо - если запретить пустой оператор, то нельзя будет и написать BEGIN END. Значит, правила недостаточно хороши - думаем дальше. Например, оператор NOP всё равно понадобится - писать вместо "BEGIN END" - "BEGIN NOP END". В этом тоже можно найти плюсы - такая запись означает, что программист не забыл заполнить блок, а явно указал, что ничего делать не надо.