Содержание к диссертации
Введение
1 Анализ принципов разработки мобильного параллельного программного обеспечения 11
1.1 Вопросы управление в параллельном программировании 11
1.2 Разработка параллельных программ в функциональной парадигме .. 18
1.2.1 Язык функционального программирования Haskell 19
1.2.2 Функциональный язык программирования Clean 22
1.2.3 Функциональный язык параллельного программирования Sisal.25
1.3 Управление вычислениями по готовности данных 29
1.3.1 Система программирования T-system 29
1.3.2 Система программирования и язык потоков данных DCF 34
1.4 Общее состояние языков параллельного программирования 35
Выводы по главе 1 36
2 Функционально-потоковая модель параллельных вычислений 38
2.1 Общие принципы организации модели 39
2.2 Программо-формирующие операторы 43
2.3 Описание динамики функционирования 50
2.3.1 Правила межоператорных переходов 51
2.3.2 Правила срабатывания программо-формирующих операторов .51
2.4 Эквивалентные преобразования 54
Выводы по главе 2 60
3 Функциональный язык параллельного программирования 61
3.1 Используемый метаязык 61
3.2 Элементарные конструкции 62
3.2.1 Разделители 62
3.2.2 Комментарии 62
3.2.3 Идентификаторы 63
3.2.4 Зарезервированные слова 63
3.3 Обозначения 64
3.4 Объекты 65
3.5 Сигналы 66
3.6 Значащие величины (константы) 67
3.6.1 Целые константы 67
3.6.2 Действительное число 68
3.6.3 Символьные константы 69
3.6.4 Логическая константа 69
3.6.5 Специальные знаки 70
3.6.6 Константы ошибок 70
3.7 Составные объекты 71
3.8 Функция 73
3.8.1 Организация обычной функции 73
3.9 Блок 74
3.10 Выражение 75
3.11 Структура программы 76
3.12 Предопределенные функции и данные 77
3.12.1 Использование специальных знаков 78
3.12.2 Использование данных 88
3.12.3 Использование специальных функций 90
3.13 Использование предопределенных типов 90
3.14 Правила эквивалентных преобразований 92
Выводы по главе 3 94
4 Реализация среды исполнения программ 95
4.1 Последовательная интерпретация функционально-параллельных программ 95
4.1.1 Транслятор 97
4.1.2 Интерпретатор 98
4.1.3 Модуль управления 99
4.2 Примеры программ на ФЯПП 100
4.2.1 Использование параллельного списка аргументов 101
4.2.2 Использование параллельного списка функций 102
4.2.3 Использование задержанных списков 103
4.2.4 Использование параллельной рекурсии 104
4.2.5 Использование функций в качестве параметров 106
4.2.6 Программа умножения двух матриц 109
Выводы по главе 4 ПО
5 Применение функционально-потокового языка для анализа и синтеза алгоритмов 112
5.1 Реализация основных программных конструкций 112
5.1.1 Реализация последовательного выполнения 112
5.1.2 Альтернатива 113
5.1.3 Итерация 115
5.2 Использование максимального параллелизма для анализа параллельных алгоритмов с заданными ограничениями 116
5.2.1 Теоретическая сортировка 117
5.2.2 Вывод ограниченных алгоритмов 121
5.3 Эквивалентные функциональные преобразования с использованием обобщенных функций 127
5.4 Использование разных форм одной и той же функции для повышения эффективности вычислений 129
Выводы по главе 5 133
Заключение 135
Литература
- Разработка параллельных программ в функциональной парадигме
- Описание динамики функционирования
- Элементарные конструкции
- Использование параллельного списка аргументов
Введение к работе
В связи с развитием параллельных вычислений и появлением новых архитектур вычислительных систем становится актуальной задача переноса программ между различными системами.
В настоящее время проблема мобильности программного обеспечения (ПО) практически решена для последовательных ЭВМ. При этом перенос традиционного программного обеспечения может осуществляться различными способами.
Возможность переноса ПО на уровне исходных кодов обеспечивается реализацией наиболее распространенных языков программирования практически для всех архитектур и операционных систем. Для обеспечения полного соответствия реализаций принимаются международные стандарты, как на языки программирования [1], так и на библиотеки архитектурно зависимых модулей [2].
Существуют также способы переноса программ на уровне исполняемых модулей, например технология Java [3].
Иначе обстоит дело с мобильностью ПО параллельных вычислительных систем (ПВС). Методы, используемые в настоящее время при его разработке, не обеспечивают возможность эффективного переноса программ с одной ПВС на другую.
Существуют разнообразные способы написания параллельных программ, которые условно можно разделить на три группы:
последовательное программирование с дальнейшим автоматическим распараллеливанием;
непосредственное формирование потоков параллельного управления, с учетом особенностей архитектур ПВС или операционных систем (ОС);
описание параллелизма без использования явного управления.
В первой группе способов используются различные приемы построения графа информационных зависимостей алгоритма на основе формального анализа исходного текста программы с последующей автоматической генерацией параллельной версии программы [4, 5, 6, 7, 8, 9].
Во второй используется расширение традиционных последовательных языков операторами распараллеливания и синхронизации [10, 11, 12]. Уровень параллелизма полностью контролируется разработчиком и может меняться в зависимости от конкретных условий. К этой же группе относятся и работы по созданию специализированных языков описания параллельных процессов с директивной стратегией управления [13].
К третьей группе относятся методы описания алгоритмов с использованием только информационного графа решаемой задачи с неявным управлением вычислениями. Предполагается, что параллельный программно-аппаратный комплекс, который будет выполнять данную задачу, автоматически адаптирует заданный алгоритм к особенностям архитектуры и обеспечит его эффективное выполнение. Такой подход предлагался в работах по описанию схем потоков данных [14, 15, 16], а так же рассматривался в различных функциональных языках программирования [17, 18, 19].
Первый метод построения параллельных программ способен обеспечить высокий уровень параллельности только на очень узком кругу задач [4, 6, 7, 20]. Возможности автоматического анализа последовательного текста программы в значительной степени зависят от используемого алгоритма, от способа задания графа управляющих связей.
Второй способ в настоящее время наиболее распространен. Практически каждый современный язык программирования имеет параллельное расширение [21, 10], создано большое число библиотек [23, 24], обеспечивающих создание и синхронизацию параллельных процессов. Созданы также специфические языки, реализующие этот подход.
Однако в подобных программах степень параллелизма директивно задается разработчиком ПО, исходя из его представлений об особенностях ар-
хитектуры конкретной ПВС [24, 25, 26]. Перенос такого ПО без существенной модификации с одной архитектуры на другую практически невозможен.
С точки зрения мобильности параллельного ПО наиболее перспективными являются методы разработки программ на основе описания информационных зависимостей алгоритма. В этом случае информационный граф алгоритма описывает максимально возможный уровень параллелизма [19, 23], а в последствии, в ходе трансляции и исполнения программы, производится его преобразование согласно тем требованиям и ограничениям, которые определяются исполняющей ПВС. Это обеспечивает возможность переноса ПО на уровне исходных кодов между различными ПВС.
Обладая рядом потенциальных преимуществ, такой подход долгое время не получал развития из-за высокой ресурсоемкости этапа подготовки вычислений. В настоящее время широкое распространение ПВС и разнообразие их архитектур привело к повышению интенсивности исследований в данном направлении [16, 18, 27, 28].
Современное соотношение архитектур ПВС и средств разработки параллельных программ, позволяет сказать, что актуальна разработка новых инструментальных систем для проектирования переносимого параллельного ПО.
Цель работы состоит в исследовании и разработке языковых средств, обеспечивающих создание мобильных параллельных программ на основе неявного управления по готовности данных.
Достижение цели связывается с решением следующих задач:
Разработка формальной функционально-потоковой модели параллельных вычислений, которая позволяет строить процесс вычислений на основе информационного графа алгоритма, не ограничивая его потенциальный параллелизм.
Разработка методов эквивалентных преобразований информационных моделей алгоритмов.
Исследование и разработка синтаксиса и семантики функционально-потокового языка параллельного программирования, основанного на предложенной модели вычислений.
Разработка инструментальных средств, поддерживающих создание и трансляцию параллельных программ.
Использование разработанной системы для создания функционально-потоковых программ, исследование методов создания параллельного ПО.
Методы исследования. В диссертационной работе использовались методы и понятия теории графов, теории алгоритмов, элементы теории множеств, теории синтаксического анализа и построения ЯП. В качестве формальных моделей анализа параллелизма применялись различные классы потоковых схем. Для описания синтаксиса языка программирования использовались расширенные формы Бэкуса-Наура (РБНФ), диаграммы ВиртаВ экспериментальной части исследований применялись теория синтаксического анализа и компиляции, методы объектно-ориентированного программирования.
Научная новизна. В ходе проведенных исследований автором получены следующие результаты:
Предложена функционально-потоковая модель параллельных вычислений, позволяющая представить программу в виде ацикличного информационного графа без явного задания стратегии выполнения. Это позволяет обеспечить ресурсную независимость от конкретных архитектур ПВС.
На основе предлагаемой модели разработан функциональный язык параллельного программирования, позволяющий создавать архитектурно независимые программы.
Предложен метод разработки архитектурно независимых параллельных алгоритмов, описывающих максимальный параллелизм решае-
мой задачи, и их последующее преобразования в архитектурно зависимые алгоритмы.
Практическая ценность. Разработанные автором диссертации функционально-потоковая модель вычислений и язык параллельного программирования позволяют создать систему проектирования мобильного параллельного программного обеспечения. Для исследования возможностей созданного функционально-потокового языка разработана программная реализация инструментальной системы для последовательной архитектуры ВС.
Публикации. Основные результаты диссертационной работы опубликованы 15 печатных работах, из которых 6 статей, 9 тезисов докладов.
По теме исследований были получены гранты ККФН № 4F0093, 1995 г. и РФФИ № 02-07-90135, 2002 г.
Апробация работы. Основные положения диссертации докладывались и обсуждались на следующих конференциях и семинарах: 3-ая региональная школа-семинар «Распределенные и кластерные вычисления», г. Красноярск 2003 г.; третий сибирский конгресс по прикладной и индустриальной математике (ИНПРИМ-98), г. Новосибирск, Института математики СО РАН, 1998 г.; 6-й Всероссийский семинар "Нейроинформатика и ее приложения" г. Красноярск, 1998 г.; межрегиональная конференция «Проблемы информатизации региона» г. Красноярск, 1995 г.; всероссийский рабочий семинар «Нейроинформатика и ее приложения» г. Красноярск, 1994 г.
Структура и объем работы. Диссертация состоит из введения, 5 глав, заключения, списка литературы и двух приложений. Работа содержит 147 страницы текста, 13 рисунков и 10 таблиц. Список литературы содержит 107 наименований.
В первой главе обосновывается выбор функционального стиля программирования и приводится обзор современного состояния языков параллельного программирования, которые относятся к данному стилю. Во второй главе приводится описание предлагаемой функционально-потоковой модели параллельных вычислений и основных правил эквивалентных преобразова-
ний информационного графа задачи. В третьей главе рассматривается синтаксис и семантика языка, приводится формальное определение синтаксиса с использования форм Бэкуса-Наура. Приводятся примеры использования основных встроенных функций. В четвертой главе дается описание инструментальной среды трансляции программ и примеры, демонстрирующие основные приемы программирования на предлагаемом языке программирования. В пятой главе, на примере алгоритмов сортировки, показывается метод разработки ресурсно-ограниченных программ путем преобразования алгоритма с максимальным параллелизмом (ресурсно-неограниченной программы). В приложении приведены диаграммы Вирта (приложение А) и примеры реализации различных параллельных алгоритмов (приложение Б).
Разработка параллельных программ в функциональной парадигме
Использование функциональной парадигмы для создания параллельных языков программирования дает несколько существенных преимуществ [54].
Изначально, на уровне модели, все функциональные языки явно не определяют последовательность передачи управления между функциями. Функциональная программа представляет собой набор определений функций, одна из которых, является стартовой. Функции определяются через другие функции или рекурсивно - через себя. В качестве входных параметров функции принимают результаты вычисления других функций. Функциональные программы не используют модель памяти с произвольным доступом (переменные), поэтому в программах исключены конфликты по данным.
В процессе выполнения программы функции получают параметры, вычисляют и возвращают результат, в случае необходимости вычисляя значения других функций. Каждый вызов возвращает некоторое значение в вызвавшую его функцию, вычисление которой после этого продолжается; этот процесс повторяется до тех пор, пока запустившая вычисления функция не вернет конечный результат пользователю.
Передача управления, активизация функции, определяется неявно, а по необходимости получения результата вычисления какой-либо функции. Это отличает функциональный от императивного стиля программирования, когда программы описывают процесс последовательного, пошагового решения задачи с четко заданным процессом передачи управления [29].
С точки зрения разработки переносимости ПО, функциональные языки программирования позволяют неявно задавать управление и создавать архитектурно независимые программы.
Современные функциональные языки позволяют писать мощные и в то же время компактные программы. Среди первых языков, обеспечивших поддержку ограниченного параллелизма, был FP [54, 56]. Использование парадів лелизма было реализовано в одной из версий Haskell [17] и в схожем языке программирования Clean [57]. Интересным языком, который совмещал функциональный стиль с управлением по готовности данных является разработанный в 1985 г. язык Sisall [58].
Язык функционального программирования Haskell
Язык Haskell является современным развитием языков редукции графов. Программа определяет информационный граф программы и варианты его расширения при вызове функций. В качестве основных характеристик нужно отметить «чисто» функциональный стиль и использование механизма «ленивых» вычислений [17, 59].
Для организации параллельных вычислений в язык введены две конструкции, обеспечивающие явное управление. Комбинатор par, получает два параметра, являющиеся параллельно выполняемыми функциями. Выражение р лраг е имеет значение, определяемое е. Помимо этого, конструкция указывает на то, что р может быть выполнен новой нитью, параллельной с родительской нитью, продолжающей выполнение е. Специфика языка, поддерживающего «ленивые» вычисления, накладывает свою специфику на параллелизм. Считается, что функция р может быть выполнена тогда, когда потребуется результат ее вычисления. В этом случае будет создана дополнительная нить, осуществляющая поддержку параллельного процесса.
Для синхронизации ранее инициированных параллельных вычислений введен оператор последовательной композиции, seq. Если el - не пустая конструкция, то выражение el seq е2 имеет значение е2\ иначе результат — «пустое значение». Декларируется, что конструкция el должна быть обработана перед возвратом результата е2.
В качестве примера, использующего эти конструкции, рассмотрим вычисление числа Фибоначчи по заданному его порядковому номеру. Функция pfib, получает целочисленный номер и возвращает такое же целое число, определяющее требуемое значение: pfib :: Int - Int pfib n I n = 1 = 1 I otherwise = nl par n2 4seq4 nl+n2 where nl = pfib (n-1) n2 = pfib (n-2) Тело функции определенно как две альтернативы: если входной параметр п меньше либо равен единицы, то возвращается 1; во всех остальных случаях порождается параллельная ветвь для вычисления п-1, вычисляется п-2, а затем их сумма. Подобный прием можно использовать и для обработки списков. Про стейшее преобразование функции быстрой сортировки quicksort. Делается попытка создавать две нити, каждая из которых должна осуществлять сорти ровку своего подсписка: одна нить сортирует значения меньшие чем первый элемент списка, а другая — большие, либо равные ему. После независимой сортировки происходит объединение результатов: quicksortN :: (Ord а) = [а] - [а] quicksortN [] = [] quicksortN [х] = [х] quicksortN (x:xs) = losort чрагч hisort чрагч losort ++ (x:hisort) where losort = quicksortN [yly - xs, у x] hisort = quicksortN [yly - xs, у = x]
Описание динамики функционирования
Правила распространения разметки по графу складываются из общих правил межоператорных переходов, правил срабатывания программо-формирующих операторов, правил выполнения оператора интерпретации над предопределенными функциями модели (а далее и языка), правил эквивалентных преобразований операторов и связей допустимого графа (алгебры преобразований, связанной с исполнением отдельных операторов и описанной выше).
Правила межоператорных переходов задают распространение разметки по графу:
1. Если входные дуги вершины имеют разметку, то на выходных дугах происходит формирование разметки в соответствии с правилами срабатывания вершины, определяющий программо-формирующий оператор.
2. Если входные разметки имеют кратность, превышающую единицу, то для заданной вершины формирование выходной разметки может начинаться при неполной входной разметке независимо для каждого из сформированных наборов входных данных, и осуществляется в соответствии аксиомами срабатывания программо-формирующих операторов.
3. В процессе распространения разметка не убирается и не замещается. Каждая дуга графа может получить разметку только один раз. Из требования о недопустимости повторной разметки вытекает требование ацикличности графа.
4. Процесс распространения разметки заканчивается, когда все дуги графа имеют полную разметку в соответствии с предписанной кратностью или при невозможности дальнейшего распространения разметки.
Правила срабатывания программо-формирующих операторов конкретизируют формирование разметок на выходных дугах для каждого из ранее введенных операторов.
Оператор интерпретации обеспечивает преобразование входного набора данных X, выступающего в качестве аргумента, в выходной набор Y, играющего роль результата, используя при этом входной набор F в качестве функции, определяющей алгоритм преобразования. В постфиксной нота ции, выбранной для дальнейших иллюстраций, данное преобразование можно записать следующим образом: X:F= Y. Можно рассмотреть множество унарных функций F, разделив его при этом на два подмножества: F={f„f2}, где f х - множество предопределенных функций языка, для каждой из которых аксиоматически задается области определения и изменения; 2 -множество функций, порождаемых при программировании.
Необходимо отметить, что областью определения любой функции из F является множество одноэлементных наборов данных. Обработка же параллельного списка задается правилами эквивалентных преобразований. Результатом выполнения функции может быть любой тип данных, включая параллельный список произвольной кратности. Следует отметить, что выбор базового набора предопределенных функций осуществляется в некоторой степени субъективно, исходя из соображений удобства пользования разрабатываемым языком. Вводятся аксиоматически определенные арифметические функции, функции сравнения и прочие, аналогично тому, как это сделано и в других языках программирования. Например, функция сложения двух чисел хх, х2, порождающая в качестве разметки число у, задается следующим образом: (хь х2):+ = у, где первый аргумент оператора интерпретации является двухэлементным списком данных. Каждый элемент этого списка должен быть числом. Второй аргумент оператора интерпретации является функцией сложения, обозначенной значком м+". Результат функции сложения, значение у, является атомарным элементом.
Наряду с определением функций, присущих большинству языков программирования, целесообразно определить множество спецефичных функций. Например, целое число может непосредственно интерпретироваться как функция выбора элемента списка: (х,, х2,... X;, ... X„):i = Xj , где і - натуральное число, х± - элемент списка. Данная функция выделяет из списка данных і-й элемент, который и определяет разметку выходной дуги. Другой полезной предопределенной функцией является: (bi, b2, b3,... b„):? = [ii, i2,... ik], где (bi, . . .bn) - список булевских величин; [i1#, . . . ik] - параллельный список натуральных чисел, определяющих номера тех компонент булевского списка, которые имеют истинные значения.
Элементарные конструкции
Зарезервированные слова используются для ключевых слов встроенных типов данных, предопределенных обозначений и функций. Ниже приведен общий их список: block break bool const dup else error false float func funcdef int nil prefunc return signal true type typedef Зарезервированные слова записываются строчными латинскими буквами. Использовать их в качестве идентификаторов запрещено.
Следует отметить, что в текущей версии языка отсутствует деление зарезервированных слов по группам, что обычно связывается с природой их создания и использования. Это объясняется простотой языка и нацеленностью текущей реализации на сценарный вариант. Предполагается, что в последующих версиях произойдет более четкое дробление, явно увязанное с особенностями реализации и использования.
В языке, построенном на основе принципа единственного присваивания, отсутствует понятие переменной. Вместо него вводится понятие обозначения как идентификатора, поставленного в соответствие с каким-либо программным фрагментом. В пределах некоторой области видимости использование идентификатора в качестве обозначения должно быть уникальным. Обозначение получает тип и величину сопоставленного элемента и может использоваться для дальнейшей передачи этих параметров в любую точку программы, обеспечивая тем самым копирование объекта, полученного в ходе вычислений. В языке определены два способа задания обозначений: - префиксное, при котором знак идентификатор пишется слева от знака "«", а определяемый объект справа; - постфиксное, когда слева от знака "»" задается определяемый объект, а справа его идентификатор. $ обозначение := идентификатор "«" элемент элемент "»" идентификатор. Под элементом понимается любой из объектов языка, выражение, блок или ранее введенное обозначение. Идентификатор задает имя ранее обозначенного элемента. Понятия объекта, выражения и блока будут даны ниже. $ элемент := объект выражение блок обозначение идентификатор. Примеры: X 100; Pi « 3.1415; 10 ten; (a, b):+ sum; хО уО 0;
К объектам языка относятся конструкции, рассматриваемые при выполнении операций интерпретации как единое целое. Каждый объект характеризуется двойкой: (тип, значение).
Объекты могут формироваться как до вычислений, так и в ходе их. Объект, сформированный до вычислений, является константой. $ объект := атом список функция. $ атом = идентификатор константа.
В дальнейшем предполагается реализация в языке частично сформированных объектов, для которых определен только тип, а окончательное значение еще не вычислено.
Существует неупорядоченное множество типов предопределенных объектов, задаваемых соответствующими именами. Предопределенные объекты делятся на атомарные и составные. Типы атомарных объектов (атомов) и области их допустимых значений определяются аксиоматически. Составные объекты являются комбинацией атомарных и уже существующих составных объектов. Они конструируются по заданным правилам. К составным объектам относятся описания функций и списки. Обозначения предопределенных типов языка, используемые в данной версии, приведены в таблице 3.1.
Сигнальные объекты (или просто сигналы) отличаются от других атомарных объектов тем, что у них отсутствует значение. Вместо этого готовность к интерпретации определяется самим фактом появления атома. Учитывая то, что обязательной составляющей всех объектов является тип, появление сигнального типа в качестве результата указывает на сам факт срабатывания соответствующего оператора интерпретации.
Использование параллельного списка аргументов
Использование механизма параллельных списков позволяет задавать массовый параллелизм вычислений, описывающий одновременную обработку нескольких потоков данных одной функцией.
Рассмотрим пример умножения элементов числового вектора на скаляр. В качестве аргумента предполагается передавать двухэлементный список, первый элемент которого является вектором, а второй — скаляром: ( (Xi, Х2, ... , Хп) , У где Xi, х2,... хп — элементы вектора, у — скаляр. Функция, возвращающая произведение вектора на скаляр: VecScalMult funcdef Param { X Param:1; //1 у Param:2; //2 Len X:; //3 V (y,Len):dup; // 4 ((X,V) // 5 :# : [] //6 : ) return // 7 } Пример выполнения: ((3,5. 02,-2,0,1.5),10): VecScalMult = (30,5.02 0000e+0 01,-2 0,0,1.500000e+001)
В первой строке производится выделение из списка параметров первого элемента вектора и обозначение его идентификатором X, после этого выделение из списка параметров второго элемента — скаляра и обозначение его идентификатором у. Далее, в строке 3, определяется длинна вектора и обозначается идентификатором Len.
В строке 4, Формируется вектора длины Len путем дублирования скаляра у Len раз и обозначение его идентификатором V. Объединение двух векторов X и Y в двухстрочную матрицу выполняется в строке 5. В строке 6 выполняется транспонирование полученной матрицы в список двухэлементных подсписков, и последующее преобразование полученного списка в параллельный.
В строке 7 - параллельное выполнение операции умножения над всеми парами, охват полученного параллельного списка круглыми скобками и выход из функции с возвратом полученного списка.
Кроме этого, функциональный стиль, поддерживаемый языком, позволяет, во многих случаях сводить исходный текст функции к одному оператору, определяющему все вычисления. Подобная версия программы для рассматриваемого примера будем выглядеть следующим образом: VecScalMultBrief funcdef Param { ( (Param: 1, (Param: 2, Param: 1: ) :dup) :#:[]: ) »re turn } Формат аргумента: ((xl, x2, : xn), у), где ((xl, x2, : xn) - числовой вектор, у - числовой скаляр.
Следует отметить, что не всегда целесообразно придерживаться подобного стиля, так как затрудняется восприятие программы и происходит дублирование одних и тех же вычислений.
Функции тоже могут задаваться параллельным списком, определяя тем самым множество потоков функций над одним потоком данных. Следующий пример иллюстрирует параллельное нахождение суммы, разности, произведения и частного двух чисел: ParAddSumMultDiv funcdef Param // Формат аргумента: (число, число), { (Param:[+,-, ,/]) return } Осуществляем параллельное нахождение суммы, разности, произведения и частного двух чисел с возвращением списка результатов Пример выполнения: (3,5): ParAddSumMultDiv = (8,-2,15,6.0000006-001)
Для программирования вычислительных алгоритмов, предусматривающих ветвление, применяются задержанные списки с последующим выбором и раскрытием элемента, соответствующего дальнейшим вычислениям. Рассмотрим пример функции, находящей абсолютное значение скалярного аргумента: Abs funcdef Param // Аргумент является числом { ({Param:-}, Param): // 1 [(Param,0) : ( ,= ) // 2 :?] // З :. return // 4 } Строка 1: Задержанное выражение, результат которого - аргумент с изменённым знаком - {Param:-}. Второй аргумент не нуждается в задержке, поскольку не влечёт никаких вычислений. (Param). Строка 2: Параллельное сравнение аргумента функции с нулём на «меньше» и «больше либо равно» с последующим охватом результата круглыми скобками. Строка 3: Преобразование списка булевских скалярных величин в список целочисленных констант, значения которых соответствуют позициям булевских величин, имеющих значение «истина». Поскольку условия «меньше» и «больше либо равно» исключают друг друга, то результатом данной операции будет целочисленная константа, имеющая значение, равное 1 или 2, соответствующее первому или второму элементу списка. Применение этой константы к списку повлечёт за собой выбор соответствующего элемента, но раскрытия задержки не произойдёт (в случае выбора первого элемента), пока не будет предпринята попытка получение значения его результата. Строка 4: Раскрытие задержки (применяя пустую функцию) и завершение функции с возвратом. В некоторых случаях можно не раскрывая задержку возвращать результат.