1.О языке программирования параллельных вычислений управляемых потоком данных
… применимым как на уровне машинных команд, так и при оперировании любыми структурами с возможностью подстановки функций как аргументов, подобно как в объектно-ориентированных или функциональных языках.
Поскольку в мультикомпьютерном комплексе передача данных между вычислительными модулями блок-схемы имеет существенное значение, то целесообразно иметь язык программирования, на котором её удобно описать, обозначенный далее как DFCP (data flow control programming).
Каждый блок (или модуль) программы выполняет некоторую группу связанных операций над входными данными получая содержательный результат, который используется на следующих этапах обработки в других блоках.
Исходные данные поступают на входные порты (изображены кружочками) блока, а результаты помещаются в выходные порты. Одинаково обозначены входы и выходы модулей адресующиеся к одному и тому же порту. Не связанные на приведенном фрагменте блок-схемы, т.е. свободные, порты отмечены жёлтым цветом.
Для примера попробую вообразить, как могла бы быть представлена программа для блок-схемы, приведенной на рисунке. Введём некоторые определения и соглашения.
Угловые скобки в примере и в описании языка ниже заключают описание того, что в реальном тексте программы должно бы быть на их месте.
Комментарий - это строка любых символов (кроме самих апострофов) между апострофами. Это 'комментарий'.
Разделители - это, символы "," ";" "." и символ пробела. В тексте программы комментарий, множество пробелов подряд, или знак разделителя эквивалентны одному символу пробела. Это позволяет акцентировать разделение ими данных и их групп, отличающихся по смыслу, принадлежности или просто для наглядности.
Символ конца строки в тексте программы игнорируется, что позволяет писать длинные выражения в нескольких строках программы. Знак разделителя ";" в конце строки предотвращает случайное слияние текстов соседних строк.
Пример программы:
code ( module (ThisBlock; 'cвободные порты:' n1,n2 ; n3,n4) begin
'1.Назначение свободным портам n1,n2,n3,n4 данного блока фактических портов внешнего блока осуществляется через объявление формальных параметров. Структуры этих портов описанные внутри и вне блока должны совпадать.'
include (<подключаемые библиотеки>, …) 'Выполняется выборка из базы данных'
'Поиск в БД и выполнение загрузки библиотечных модулей:'
load (lib_module_1 'далее могло быть перечисление других библ. модулей');
'2.Компиляция и загрузка программ модулей:'
code (module('имя:' NewMod2; 'формальные параметры:' p1, p2 ; p3, p4), begin <программа тела модуля … >end );
code (module(NewMod3; p1, p2; p3), begin< программа тела модуля … >end );
'3.Имена модулей B1,B2,B3,B4 связываются с их содержанием:'
block ((lib_module_1; 'псевдонимы:' B3,B4), (NewMod2; B1), (NewMod3; B2));
'4.Описываются структуры портов с целым числом и числом с пл.точкой:'
struct (( port_i; b, flag; i, data; l, next), (port_f; b, flag; f, data; l, next));
'Структура порта с указателем адреса и несколькими потребителями данных:'
struct ( port_n; b, flag; l, pnt; l, next; b, mmb; b, cnt);
'5.Описываются порты по их принадлежности к типам'
dec ((port_i; n4, n6, n7) (port_f; n1, n2, n3) (port_n, n5));
'6.Указываются связи портов с блоками модулей'
#({n6.next}'←'B1); #({n7.next}'←'B2); #({n5.next}'←'B3);
'7.'dec (f, [MM, 19]) '← здесь объявлен массив ММ на 20 чисел с пл.точкой'
'Устанавливается связь с данными и число получателей данных порта n5:'
#({n5.pnt}'←'@MM); #({n5.mmb}'←'b);
'8.Порты связываются с модулями через их формальные параметры:'
assign ((B1; n1, n6 ; n5, n7), (B2; n7, n4 ; n6), (B3; n5 ; n2), (B4; n5 ; n3));end)
Вот, примерно, и всё. О некоторых выражениях в программе из этого примера расскажу далее. Правда, в примере не показаны операторы занесения данных в порт и передачи управления следующему модулю, которые должны использоваться в модулях, но в данном языке не будет ничего такого, чего не было бы в языках Си либо Лисп. Разве только он имеет гораздо более ограниченный синтаксис. В частности, в нём допустимы выражения только вида <Что делать>(<перечень над чем>), то есть состоящие из оператора и операндов: <инструкция>(<операнд>, …, <операнд>). Так что профи могут сразу переходить к следующей главе.
Оператор - это группа символов с инструкцией компилятору, или указанием на действие выполняемое программой, а операнд - это символьное имя (идентификатор) данного или константа представляемые адресом в памяти ячеек содержащих их значения.
Круглые скобки определяют иерархию вложенности операторов и порядок обработки операндов.
Оператор присвоения значения данному использовался в примере выше. Так, операция #(a, b) означает, что в ячейки по адресу данного "a" будет занесено количество байт соответствующее типу "a" из ячеек по адресу данного "b". Если типы данных не совпадают, например, a-float, а b-integer, тогда надо использовать функцию "f" для преобразования типов: #(a, f(b)).
Опишу, для примера, ещё несколько операторов.
Сумму целочисленных данных k, l и m получим операцией +i(k,l,m), а сумму чисел с пл.точкой a, b и c операцией +f(a,b,c), так как для данных разного типа и операторы нужны разные.
Оператор массива пусть выражается функцией arr(<имя массива>, <индекс>). Индекс представлен целым положительным числом начиная с нуля. Но в таком виде операции с массивами было бы трудно находить среди прочего текста программы. Несколько более привычным и заметным был бы вариант написания в тексте [<имя массива>, <индекс>].
Но если предусмотрим оператор замены текста "def (<подстрока>,<подмена>)" в компиляторе программы, то инструкции def ( "[", "arr(" ) и def ( "]", ")" ) обеспечат желаемое.
Имени массива сопоставляется адрес в памяти его первого элемента. Например, операция #(LL, <имя массива>) содержимое именно этого элемента и загрузит в ячейку LL. Функция [<имя массива>, <индекс>] выполняет операцию
<указатель> = <адрес первого элемента> + <значение индекса>*<размер элемента>.
И уже полученный <указатель> подставляется вместо адреса первого элемента.
Указателю адреса (pointer) значение присваивается выполнением функции <адрес><данное>. Значение данного можно получить функцией <значение><указатель>. Используем, например, в качестве имени функции <адрес> символ "@", а функции <значение> символ "~"
Операция #(Pnt_NN, @(NN)) заносит адрес данного NN в ячейку с именем данного Pnt_NN. Операция ~(Pnt_NN) выдаст как операнд содержимое ячейки обозначенной Pnt_NN. Значит значением выражения ~(Pnt_NN) будет данное NN.
Например, присвоить данному LL значение 3-го элемента из массива целых чисел M можно операцией #(LL [M,2]) или операцией #(LL,~(+l(@(M), *i(2,4)))), поскольку 1-й элемент имеет индекс =0, а каждое целое число занимает 4 байта в памяти. Оператор *i(…) перемножает целые числа, а оператор +l(…) прибавляет результат к адресу (типа длинное целое - long) первого элемента массива.
Описание структур выполняется оператором:
struct (<назв. структуры>, <тип>, <название поля>, … <тип>, <название поля>).
Например, в описании элемента узла списка: struct (node, b flag, l Pnt_data, l next), поле "flag" содержит сведения о данных в списке и их состоянии, поле "Pnt_data" указывает адрес данного, а поле "next" - адрес следующего узла списка. На рисунке ниже, на схеме А показан список именно такого типа. На схеме Б показан возможный вариант списка списков.
Операция со структурами rec (<назв. структуры>, <название поля>) возвращает, как операнд, адрес ячеек с указанным данным. Как и в случае массивов, нас бы более устроило написание {<назв. структуры>.<название поля>}. Для этого надо сделать подмену текста при компиляции программы инструкциями def ( "{", "rec(" ) и def ( "}", ")" ), а "точка" и так является разделителем.
Объявление данных выполняется оператором: dec (<тип данного>, <имя данного>, …, <имя данного>).
Например, в строке объявлений dec ((i, a, b, c) (f, w, [M 19], r, t) (node, list)) объявлены целочисленные данные (a, b, c), данные (w, [M 19], r, t) с пл.точкой, включая массив M на 20 чисел, а также начат пустой пока список с именем list. На схеме слева показан формат списка списков соответственный объявлению данных.
Вероятно целесообразно установить следующие правила обработки списков аргументов и формирования выходных результатов, которые, полагаю, проще показать, чем объяснить. Например, применяя унарные операторы (функции f ) к списку f (a, b, c) получим список значений ( f (a), f (b), (f (c) ), а для бинарных, т.е. вида f (a, b), операторов получим:
f (a, b, c, d)
→ f ( f ( f (a,b), c), d)
→ значение
f ((a, b, c), (d, e, k))
→ ( f (a,b,c), f (d,e,k) )
→ список значений
2.Технологичность VS красоты и изящества
"Лямбда-исчисление, разработанное А. Чёрчем для преодоления некоторых проблем в математической логике …" Но при использовании в программах простых рекуррентных циклов и переменных подобных теоретических проблем точно не возникнет.
Как бы облегчая труд программирования, изобрели множество языков, которые мало соответствовали машинным технологиям, но были как бы близки к разговорным человеческим. Изобретатели языков всё более отдаляют в область абстракций описание алгоритма от способа его машинной реализации.
Цитата: "Лямбда-исчисление, разработанное А. Чёрчем для преодоления некоторых проблем в математической логике, фактически служит теоретическим базисом функционального программирования" /исправлено и выделено А.С./.Не оспариваю гениальность и изящество воплощения, но по мере перехода ко всё более сложным аспектам применения, простой и удобный поначалу язык превращается в трудно понимаемый набор вложенных одно в другое заклинаний.
Вероятно это приносит эстетическое удовольствие некоторым программистам, но усложняет компиляцию программы, почти наверняка потребует больше памяти в ЭВМ и замедлит работу, а многие программисты предпочли бы употребить простой рекуррентный цикл вместо рекурсий и вспомогательных функций, нужных чтобы всего лишь избежать применения оператора присваивания. Могу предположить, что невозможность присваивания значения переменным в функциональных языках может быть чревата повторным вычислением функций.
Я считаю, что инструменты работы с ЭВМ должны быть адекватны присущим ЭВМ технологиям: <инструкция> (<операнд>, …, <операнд>).
Сравним запись выражения D=√(b2-4ac) на Алголе: D:=sqtr(b↑2-4˟a˟c)
с записью на предполагаемом языке DFCP: #(D, sqrt( +(^(b 2),-(*(4 a c))) )).
В первом случае требуется синтаксический анализ выражения, а во втором программу его вычисления можно в процессе компиляции сразу загружать в память ЭВМ прямо "с листа". Конечно, в последнем выражении многовато скобок. Поэтому в Лиспе скобки и запятые не ставятся, если можно понять, где оператор, а где операнды - тогда здесь, например, стало бы: # D sqrt(+ ^ b 2 -* 4 a c). Но похоже, теперь придётся задумываться, что к чему относится. Особенно если имена функций (операторов) будут тоже буквенные, как и у операндов. А компилятор тогда должен знать сколько операндов может быть у конкретного оператора.
Язык DFCP предназначен для программирования модулей обменивающихся потоками данных через порты ввода/вывода. Для портов справедлива концепция "однократного присваивания", т.е. в каждом последовательном кадре потока данные один раз заносятся в порт и затем изымаются из него. Но если аналогично поступать и с просто переменными, т.е. не использовать одно и то же переменное для разных по смыслу и назначению данных, то они фактически не будут отличаться от портов. Таким образом рекомендации не использовать переменные в потоковых языках весьма условны.
Кстати, упомянутое правило применения переменных позволит любую императивную программу изобразить в виде блок-схемы операторов и операндов, так что синтаксис программ может быть совершенно одинаков и для машинных команд и для блоков модулей. Но для блоков программа станет декларативной.
Наверное можно попенять, что достаточного описания языка так и не представлено. Но на самом деле всё уже сказано тем, что вся семантика языка исчерпывается конструкциями <оператор> (<операнд>, …, <операнд>). А согласно синтаксису, операнд в свою очередь может быть выражением с операндами включая операторы функций. Иерархия вычислений и результатов, в случае императивной программы внутри тела модуля, определяется вложением скобок и последовательностью операторов.
Язык применим от самого низкого уровня, когда его операторы копируют машинные команды или инструкции ассемблера, и до неограниченно высокого, когда операндами являются объекты сколь угодно сложной структуры, нисколько не уступая в этом языкам ООП. Более того, не так сложно запрограммировать модуль, чтобы во время исполнения программы в некоторые его формальные параметры подставлялись бы адреса тех процедур, которые нужны согласно алгоритму и должны исполняться в теле модуля.
При выставлении данного в порт, если обрабатывающие его модули находятся в одном процессорном модуле, то вместе с данным требуется передать и управление следующему обрабатывающему его программному модулю. Поэтому в программной части языка DFCP предполагается использование команд перехода "Goto" (передачи управления). А в таком случае в программе естественно использовать и переход на метки, что как бы противоречит принципам структурного программирования.
У меня есть опыт программирования начиная с кодировки команд на перфокартах в двоичном машинном коде и абсолютных адресах. До НИИ я работал в ОКБ, где проектировались двигатели для "лунной" ракеты Королёва, и там для обработки телеметрии испытаний авиационных и ракетных двигателей были установлены первые полупроводниковые аппаратно-программные комплексы изготовленные пензенским заводом САМ (з-д счётно-аналитических машин). Но матобеспечения не было никакого. Все драйверы для устройств ввода/вывода и регистраторов телеметрии, все библиотечные программы пришлось разрабатывать с нуля, включая переводы чисел из двоичного в 10-й формат и обратно.
Поэтому пришлось сначала разработать простенький язык типа ассемблера, затем отладчик, который (в целях экономии бумаги) во всех циклах распечатывал только два первых пропуская остальные и диагностируя бесконечные, и пр. Потом, и для перфоленточной ЭВМ, хотя уже и следующего поколения, пришлось разработать и совместимую магнитноленточно-дисковую ОС (если диски откажут в критический момент). Это при том, что приходилось заниматься и собственно обработкой данных, как то спектральным анализом телеметрии и даже заново изобретать для этого алгоритм быстрого преобразования Фурье, и пр.
Исходя из собственного опыта программирования, могу утверждать, что программа с метками и командами перехода обычно компактнее и более понятна, чем с продублированными копиями блоков и набором «флажков» ради исключения команд перехода. А уж если кто-то сознательно захочет сделать код непонятным, то в языках ООП возможностей для этого гораздо больше. Аналогичного мнения придерживаются и некоторые профи:
https://habr.com/ru/post/114211/Поскольку операндом может быть любой цифровой объект или параметры состояния и ресурсов вычислительной системы, а оператором любой выполнимый в ней алгоритм, то ясно, что возможности и универсальность применения данного языка почти не ограничены. При том, что его синтаксис и структура остаются неизменными и не появляется никаких новых синтаксических определений и правил. Любые "расширения" языка будут иметь всё тот же формат операторов включая, быть может, некие сигнатуры для операндов. Здесь в примере гл.1 это служебные слова "begin" и "end". И совершенно неважно, соблюдались ли какие-либо правила при программировании кода этих операторов.
Представление в виде блок-схем соответствует концепции программирования «сверху вниз», а взаимодействие модулей только через порты гарантирует соблюдение принципов инкапсуляции. Кроме того, естественный интерфейс по границам отсылки или приёма данных оптимально подходит для коллективной разработки программ.