Функция ????? принимает ... аргумент Примеры:


Типы данных и особенности арифметики HomeLisp

В HomeLisp имеются данные следующих типов: FIXED, FLOAT, BITS и STRING.

Данные типа FIXED - это целые числа практически неограниченной разрядности. Описываемые ниже функции PLUS, DIFFERENCE, TIMES, QUOTIENT, REMAINDER позволяют вычислить, например, величину "100 факториал" или "2 в сотой степени".

Данные типа FLOAT соответствуют числам с плавающей точкой двойной точности (DOUBLE). Для работы с данными типа FLOAT можно использовать функции PLUS, DIFFERENCE, TIMES, ADD1, SUB1. Для деления чисел типа FLOAT следует использовать функцию DIVIDE (отсутствующую в реализации Лаврова и Силагадзе). Попытка использовать для деления чисел с плавающей точкой функции QUOTIENT и REMAINDER приведет к ошибке.

Данные типа BITS представляют собой 32-х битовые шкалы. Над ними можно выполнять битовые поразрядные операции LOGAND, LOGOR, LOGXOR, а также сдвигать в обе стороны, используюя функцию с двумя аргументами LEFTSHIFT. Второй аргумент задает количество сдвигов (число типа FIXED). Если оно отрицательно - сдвиг выполняется вправо.

Арифметика HomeLisp частично полиморфна. У функций PLUS, DIFFERENCE, TIMES допустимы аргументы типов FIXED и/или FLOAT. При вычислении происходит "поглощение" типом FLOAT типа FIXED. Так, при вычислении выражения:

(+ 555 1.0)

будет получен результат 556.0, имеющий тип FLOAT. Тогда как при вычислении выражения:

(+ 555 1)

будет получен результат 556, имеющий тип FIXED.

Функция EXPT (возведение в степень) возвращает результат типа FIXED только если оба ее аргумента имеют тип FIXED. В противном случае возвращается результат типа FLOAT. Дополнительно следует отметить, что попытка возведения отрицательного числа (любого типа) в вещественную степень (даже с нулевой дробной частью) вызовет ошибку. Тогда как возведение отрицательного числа в целую степень выполняется без проблем. Нуль в любой положительной степени равен нулю. Попытка возвести нуль в отрицательную степень вызовет ошибку. Возведение числа в нулевую степень дает единицу.

Функции инкремента и декремента ADD1, SUB1 возвращают результат того же типа, что и аргумент.


Предопределенные атомы HomeLisp

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

Встроенные атомы
Имя встроенного атома | Описание встроенного атома
_VER | Атом, имеющий в списке свойств индикаторы APVAL и STRING. Значением атома является строка символов "HomeLisp Вер. N.m.k (Файфель Б.Л.)" Здесь N, m и k - номера версии, подверсии и ревизии соответственно.
APVAL | Стандратный флаг свойства "быть константой или иметь определяющее выражение"
BITS | Стандратный флаг свойства "иметь значением строку битов"
BLOB | Стандратный флаг свойства "являться большим двоичным объектом" (Binary Large Object - BLOB).
BRKSTATE | Атом, обозначающий, что вычисление было прервано кнопкой STOP.
COM | Стандратный флаг свойства "быть COM-объектом"
CONTROL | Стандартный флаг "являться контролом (элементом управления)"
DIALOG | Стандартный флаг "являться диалогом"
ERRSTATE | Атом, обозначающий, что при вычислении произошла ошибка. Выработка результата ERRSTATE вызывает прекращение вычислений.
EXPR | Стандратный флаг свойства "быть функцией типа EXPR"
FEXPR | Стандратный флаг свойства "быть функцией типа FEXPR"
FIXED | Стандратный флаг свойства "иметь значением число типа FIXED"
FLOAT | Стандратный флаг свойства "иметь значением число типа FLOAT"
FSUBR | Стандратный флаг свойства "быть встроенной функцией типа FSUBR"
FUNARG | Служебный атом, использующийся при вычислении функциональных аргументов. Результат вычисления выражения (FUNCTION ff) является S-выражение: (FUNARG ff nn). Здесь ff - имя функции, а nn - текущее значение указателя заполнения ассоциативного списка.
LAMBDA | Служебный атом,использующийся при создании функций (как именованных, так и безымянных)
MACRO | Стандратный флаг свойства "быть функцией типа MACRO". Такие функции вычисляются в два этапа: сначала проиходит обычное вычисление (аналогичное вычислению функции FEXPR), а затем полученное S-выражение снова передается на вход EVAL.
NIL | Этот атом используется двумя способами: как логическое "Ложь", как признак конца списка.
FILE | Стандартный флаг свойства "являтся файлом"
STRING | Стандратный флаг свойства "иметь значением строку символов"
SUBR | Стандратный флаг свойства "быть встроенной функцией типа SUBR"
T | Атом, обозначающий логическую истину.
WINDOW | Стандартный флаг свойства "являться графическим окном".
Атомы, создаваемые при загрузке библиотеки
Имя встроенного атома | Описание встроенного атома
_APPEND | Константа, предназначенная для открытия текстового файла в режиме APPEND. Значение константы равно 2.
_AQUA | Константа, задающая бирюзовый цвет. Значение равно &H00FFFF.
_BINARY_READ | Константа, предназначенная для открытия двоичного файла в режиме READ. Значение константы равно 3.
_BINARY_READ_WRITE | Константа, предназначенная для открытия двоичного файла в режиме READ/WRITE. Значение константы равно 5.
_BINARY_WRITE | Константа, предназначенная для открытия двоичного файла в режиме WRITE. Значение константы равно 4.
_BLACK | Константа, задающая черный цвет. Значение равно &H000000.
_BLUE | Константа, задающая синий цвет. Значение равно &H0000FF.
_BUTTON | Тип контрола - командная кнопка. Значение равно 5.
_CENTER | Признак выравнивания по центру. Значение равно 2.
_CHECK | Тип контрола - чекбокс. Значение равно 6.
_COMBO | Тип контрола - комбобокс. Значение равно 4.
_E | Математическая константа "e" с плавающей точкой, равная 2.718281828.
_FUCHSIA | Константа, задающая лиловый цвет. Значение равно &HFF00FF.
_GRAY | Константа, задающая серый цвет (50%). Значение равно &H808080.
_GREEN | Константа, задающая зеленый цвет. Значение равно &H008000.
_INPUT | Константа, предназначенная для открытия текстового файла в режиме INPUT (только для чтения). Значение константы равно 0.
_LABEL | Тип контрола - метка. Значение константы 1.
_LEFT | Признак выравнивания влево. Значение равно 0.
_LIST | Тип контрола - список. Значение константы 3.
_LIME | Константа, задающая ярко-зеленый цвет. Значение равно &H00FF00.
_MAROON | Константа, задающая темно-красный цвет. Значение равно &H800000.
_NAVY | Константа, задающая темно-синий цвет. Значение равно &H000080.
_OLIVE | Константа, задающая коричнево-зеленый (оливковый). Значение равно &H808000.
_OPTION | Тип контрола радиокнопка. Значение равно 7.
_OUTPUT | Константа, предназначенная для открытия текстового файла в режиме OUTPUT (только для записи). Значение константы равно 1.
_Pi | Математическая константа "пи" с плавающей точкой, равная 3.1415926535.
_PURPLE | Константа, задающая фиолетовый цвет. Значение равно &H800080.
_RED | Константа, задающая красный цвет. Значение равно &HFF0000.
_RIGHT | Выравнивание вправо. Значение равно 1.
_SILVER | Константа, задающая серый цвет (25%). Значение равно &HC0C0C0.
_TEAL | Константа, задающая сине-зеленый цвет. Значение равно &H008080.
_TEXT | Тип контрола поле ввода (текстбокс). Значение равно 2.
_WHITE | Константа, задающая белый цвет. Значение равно &HFFFFFF.
_YELLOW | Константа, задающая желтый цвет. Значение равно &HFFFF00.
KEY_ALL_ACCESS | Константа доступа к реестру. Значение равно &HF003F
HKEY_CLASSES_ROOT | Обозначение ветви реестра. Значение равно &H80000000
HKEY_CURRENT_CONFIG | Обозначение ветви реестра. Значение равно &H80000005
HKEY_CURRENT_USER | Обозначение ветви реестра. Значение равно &H80000001
HKEY_DYN_DATA | Обозначение ветви реестра. Значение равно &H80000006
HKEY_LOCAL_MACHINE | Обозначение ветви реестра. Значение равно &H80000002
HKEY_PERFORMANCE_DATA | Значение равно &H80000004
HKEY_USERS | Значение равно &H80000003
KEY_CREATE_LINK | Значение равно &H20
KEY_CREATE_SUB_KEY | Значение равно &H4
KEY_ENUMERATE_SUB_KEYS | Значение равно &H8
KEY_EXECUTE | Значение равно &H20019
KEY_NOTIFY | Значение равно &H10
KEY_QUERY_VALUE | Значение равно &H1
KEY_READ | Значение равно &H20019
KEY_SET_VALUE | Значение равно &H2
KEY_WRITE | Значение равно &H20006
REG_BINARY | Значение равно 3
REG_DWORD | Значение равно 4
REG_DWORD_BIG_ENDIAN | Значение равно 5
REG_DWORD_LITTLE_ENDIAN | Значение равно 4
REG_EXPAND_SZ | Значение равно 2
REG_LINK | Значение равно 6
REG_MULTI_SZ | Значение равно 7
REG_NONE | Значение равно 0
REG_RESOURCE_LIST | Значение равно 8
REG_SZ | Значение равно 1


Благодарности
Пользуюсь случаем выразить самую искреннюю благодарность:

Алексею Жаркову, открывшему мне красоту Лиспа;

Лидии Васильевне Городней и Николаю Вячеславовичу Шилову (г. Новосибирск) - за интерес к работе и ряд чрезвычайно ценных идей (реализацию которых автор в первой версии, к сожалению, упустил);

Разработчикам пакета XLisp Дэвиду Бетцу (David Betz) и Томасу Алми (Thomas Almy), а также разработчикам Российского пакета YLisp Дмитрию Иванову и Арсению Слободюку за бесценную возможность использовать эти добротные разработки в качестве "базы отладки";

Владимиру Гуськову и Борису Викторовичу Глумову (г. Москва) за найденные ошибки и многочисленные пожелания (большую часть которых удалось реализовать);

Моему сыну Анатолию - за конструктивную критику, полезные советы и помощь с хостингом;

Моей жене Тамаре, поддерживающей меня во время работы над проектом;

Моему другу Андрею Энтелису - за плодотворный скепсис, за подаренные мне книги по Лиспу и за многочисленые уроки реального программирования, преподанные мне на протяжении более чем двадцатилетней дружбы;

Моим коллегам по работе, которым я "прожужжал" все уши рассказами о Лиспе - за терпение и доброжелательность;

И, наконец, моим домашним любимцам - котам и кошкам, согревавшим меня длинными вечерами...


Описание функций работы с BLOB-объектами

Большой двоичный объект (Binary large Object - BLOB) - это буфер в памяти компьютера. При создании буфера с ним связывается атом, в список свойств которого добавляется стандартный флаг BLOB. Будем называть этот атом идентификатором BLOB-а. Используя идентификатор BLOB-а, как ссылку, HomeLisp позволяет выполнять различные действия с буфером: загружать в него данные из двоичных файлов(FILGETBLOB), выгружать данные в файл на диске (FILPUTBLOB), менять данные внутри буфера и т.д.) Использование BLOB-ов позволяет, например, сделать на HomeLisp-е компилятор с какого-либо языка непосредственно в машинный код. Дело за желающими!

Работа с BLOB-ом начинается с создания BLOB-а вызовом функции BLOCREATE. С полученным BLOB-ом можно выполнять различные операции, а перед завершением работы все созданные BLOB-ы желательно уничтожить вызовом функции BLODESTROY.

Функции работы с BLOB-ами недоступны при работе WEB-компоненты.

Ниже перечислены все функции работы с BLOB-ами в алфавитном порядке. Функции FILGETBLO и FILPUTBLO описаны в разделе, посвященном действию с файлами.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
BIT2BLO 4 1-BLOB;
2-BITS;
3-FIXED;
4-FIXED (1, 2, 3 или 4) 		Занести в BLOB, заданный ПЕРВЫМ аргументом, битовую шкалу, заданную ВТОРЫМ аргументом с позиции, заданной ТРЕТЬИМ аргументом. Размер шкалы задается значением ЧЕТВЕРТОГО аргумента (может быть 1, 2, 3 или 4 байта)
BLO2BIT 3 1-BLOB;
2-FIXED;
3-FIXED (1, 2, 3 или 4) 		Извлечь из BLOBа, заданного ПЕРВЫМ аргументом, битовую шкалу с позиции, заданной ВТОРЫМ аргументом. Размер шкалы задается значением ТРЕТЬЕГО аргумента (может быть 1, 2 или 4 байта)
BLO2FIXED 3 1-BLOB;
2-FIXED;
3-FIXED (1, 2, 3 или 4) 		Извлечь из BLOBа, заданного ПЕРВЫМ аргументом, число типа FIXED с позиции, заданной ВТОРЫМ аргументом. Размер числа задается значением ТРЕТЬЕГО аргумента (может быть 1, 2, 3 или 4 байта)
BLO2FLO 3 1-BLOB;
2-FIXED;
3-FIXED (4 или 8) 		Извлечь из BLOBа, заданного ПЕРВЫМ аргументом, число типа FLOAT с позиции, заданной ВТОРЫМ аргументом. Размер числа задается значением ТРЕТЬЕГО аргумента (может быть 4 или 8 байтов)
BLO2STR 3 1-BLOB;
2-АТОМ;
3-FIXED 		Извлечь из BLOBа, заданного ПЕРВЫМ аргументом, строку (STRING) с позиции, заданной ВТОРЫМ аргументом. Размер строки задается значением ТРЕТЬЕГО аргумента.
BLOCOPY 2-5 1-BLOB;
2-BLOB;
[ 3-FIXED (1);
4-FIXED (1);
5-FIXED (длина BLOB-1)] 		Копировать содержимое 1-го BLOBа во второй с позиции, заденной значением ТРЕТЬЕГО аргумента (по умолчанию с начала) в позицию во втором BLOBе, заданную значением ЧЕВЕРТОГО аргумента. Копируется размер BLOBа, заданный значением последнего аргумента (по умолчанию - весь исходный BLOB).
BLOCREATE 2 1-Атом;
2-FIXED 		Создать BLOB с длиной, заданной значением второго аргумента. Атом, стоящий на месте 1-го аргумента, получит в список свойств индикатор BLOB.
BLODESTROY | 1 | 1-BLOB | Уничтожить BLOB
BLODUMP* 4 [1-STREAM];
2-BLOB;
[3-FIXED(1);
4-FIXED (длина BLOB) ] 		Выполняется дамп BLOBа, с позиции, заданной значением ТРЕТЬЕГО аргумента (по умолчанию - с начала). Дампируется размер BLOBа, заданный значением ЧЕТВЕРТОГО аргумента (по умолчанию - весь BLOB). Если задан первый аргумент (STREAM, открытый в режиме INPUT), то дамп попадает в соответствующий файл.
BLOEQ 2 1-BLOB;
2-BLOB; 		Сравнить два BLOBа на полное совпадение
BLOFBLO 3 1-BLOB;
2-FIXED;
3-BLOB; 		Найти в BLOB-е, заданным первым аргументом, положение BLOB-а, заданного третьим аргументом. Поиск начинается с позиции, заданной вторым аргументом.
BLOFBYT 3 1-BLOB;
2-FIXED;
3-FIXED; 		Найти в BLOB-е байт, заданный третьим аргументом. Поиск начинается с позиции, заданной вторым аргументом.
BLOFFLO 3 1-BLOB;
2-FIXED;
3-FLOAT; 		Найти в BLOB-е число с плавающей точкой (double), заданное третьим аргументом. Поиск начинается с позиции, заданной вторым аргументом.
BLOFILL 2 1-BLOB;
2-FIXED; 		Заполнить BLOB символом-заполнителем, заданного значением ВТОРОГО аргумента.
BLOFINT 3 1-BLOB;
2-FIXED;
3-FIXED; 		Найти в BLOB-е короткое (двухбайтовое) целое, заданный третьим аргументом. Поиск начинается с позиции, заданной вторым аргументом.
BLOFLNG 3 1-BLOB;
2-FIXED;
3-FIXED; 		Найти в BLOB-е длинное (четырехбайтовое) целое, заданное третьим аргументом. Поиск начинается с позиции, заданной вторым аргументом.
BLOFSTR 3 1-BLOB;
2-FIXED;
3-STRING; 		Найти положение строки, заданной ТРЕТЬИМ аргументом в BLOBе, заданным первым аргументом. Поиск начинается с позиции, заданной значением ВТОРОГО аргумента (по умочанию - с начала).
BLOREPLACE 4 1-BLOB;
2-FIXED;
3-STRING;
4-STRING 		Заменяет в BLOBе (заданным ПЕРВЫМ аргументом) все вхождения строки, заданным ТРЕТЬИМ аргументом, на строку, заданную ЧЕТВЕРТЫМ аргументом. Замена начинается со смещения, заданного ВТОРЫМ (необязательным) аргументом. Если он опущен - замена выполняется с начала BLOBa.
FIX2BLO 4 1-BLOB;
2-FIXED;
3-FIXED;
4-FIXED 		Заносит в BLOB, заданный ПЕРВЫМ аргументом, число типа FIXED, значение которого задано значением ВТОРОГО аргумента c позиции, заданной ТРЕТЬИМ аргументом. Длина числа (1, 2 или 4 байта) задается значением ЧЕТВЕРТОГО аргумента.
FLO2BLO 4 1-BLOB;
2-FLOAT;
3-FIXED;
4-FIXED 		Заносит в BLOB, заданный ПЕРВЫМ аргументом, число типа FLOAT, значение которого задано значением ВТОРОГО аргумента c позиции, заданной ТРЕТЬИМ аргументом . Длина числа (4 или 8 байтов) задается значением ЧЕТВЕРТОГО аргумента.
STR2BLO 3 1-BLOB;
2-STRING;
3-FIXED 		Занести в BLOB, заданный ПЕРВЫМ аргументом, строку (STRING), значение которой задано значением ВТОРОГО аргумента c позиции, заданной ТРЕТЬИМ аргументом.
BIT2BLO

Функция BIT2BLO принимает четыре аргумента: идентификатор BLOBa, битовую шкалу, и два значения типа FIXED. Функция заносит битовую шкалу (значение 2-го аргумента) в тело BLOBa, заданного идентификатором. Смещение от начала, BLOBа, куда будет заноситься битовая шкала, Задается третьим параметром, а размер битовой шкалы (1, 2 или 4 байта) - последним, четвертым параметром. Вот подробный пример вызова функции BIT2BLO:

(blocreate 'buf 50)

==> buf

(bit2blo 'buf &H12 5 1)

==> T

(bloDump* 'buf)

00000001 | 00 00 00 00 12 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00                   |                         | ..               |

==> T

(bit2blo 'buf &H1234 23 2)

==> T

(bloDump* 'buf)

00000001 | 00 00 00 00 12 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000017 | 00 00 00 00 00 00 12 34 | 00 00 00 00 00 00 00 00 | ......4......... |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00                   |                         | ..               |

==> T

Здесь создается BLOB размером 50 байтов, а затем в него заносится байт &H12 со смещением пять. После этого BLOB дампируется. Занесенный байт отчетливо виден.

Затем в тот же BLOB заностися двухбайтовая битовая шкала &H1234 со смещения 23. После повторного дампирования все становится ясным.

BLO2BIT

Функция BLO2BIT принимает три аргумента: идентификатор BLOBa, и два значения типа FIXED. Функция извлекает из тела BLOBa битовую шкалу со смещения, заданного значением 2-го аргумента и длиной, заданной значением 3-го аргумента. Длина может принимать значения 1, 2, 3 или 4. Если смещение начала или конца извлекаемой битовой шкалы выходит за пределы BLOBа, выдается сообщение об ошибке. Вот подробный пример вызова функции BLO2BIT:

(blocreate 'buf 50)

==> buf

(blofill 'buf 0)

==> T

(blodump* 'buf)

00000001 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00                   |                         | ..               |

==> T

(bit2blo 'buf &H123456 7 3)

==> T

(blodump* 'buf)

00000001 | 00 00 00 00 00 00 12 34 | 56 00 00 00 00 00 00 00 | .......4V....... |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00                   |                         | ..               |

==> T

(blo2bit 'buf 8 3)

==> &H345600

Здесь создается BLOB размером 50 байтов, а затем в него заносится трехбайтовая шкала &H123456 со смещением семь. После этого BLOB дампируется. Далее, со смещения 8 извлекается трехбайтовая шкала. Получается, естественно, &H345600.

BLO2FIXED

Функция BLO2FIXED принимает три аргумента: идентификатор BLOBa, и два значения типа FIXED. Функция извлекает из тела BLOBa число типа FIXED со смещения, заданного значением 2-го аргумента и длиной, заданной значением 3-го аргумента. Длина может принимать значения 1, 2 или 4. Если смещение начала или конца извлекаемого числа выходит за пределы BLOBа, выдается сообщение об ошибке. Вот подробный пример вызова функции BLO2FIXED:

(bloCreate 'buf 48)

==> buf

(fix2blo 'buf 2211 3 2)

==> T

(blodump* 'buf)

00000001 | 00 00 A3 08 00 00 00 00 | 00 00 00 00 00 00 00 00 | ..Ј............. |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |

==> T

(blo2Fixed 'buf 3 2)

==> 2211

Здесь создается BLOB размером 48 байтов, а затем в него заносится целое число 2211 со смещением 3. После этого BLOB дампируется. Далее, со смещения 3 извлекается двухбайтовое целое. Получается, естественно, 2211.

Пусть читатель обратит внимание на то, что хотя десятичное число 2211 в шестнадцатеричном виде равно 08A3, внутри BLOB-а байты имеют обратный порядок, т.е. число хранится в виде A308. Это связано с особенностью интеловских микропроцессоров. А вот для битовых шкал HomeLisp порядок байтов не меняет (см. описание функции BIT2BLO).

BLO2FLO

Функция BLO2FLO принимает три аргумента: идентификатор BLOBa, и два значения типа FIXED. Функция извлекает из тела BLOBa число типа FLOAT со смещения, заданного значением 2-го аргумента и длиной, заданной значением 3-го аргумента. Длина может принимать значения 4 или 8. Если смещение начала или конца извлекаемого числа выходит за пределы BLOBа, выдается сообщение об ошибке. Вот подробный пример вызова функции BLO2FLO:

(bloCreate 'buf 46)

==> buf

(flo2blo 'buf 12.34 6 4)

==> T

(blo2flo 'buf 6 4)

==> 12.34

(blodump* 'buf)

00000001 | 00 00 00 00 00 A4 70 45 | 41 00 00 00 00 00 00 00 | .....¤pEA....... |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00       | ..............   |

==> T

(flo2blo 'buf 12.34 17 8)

==> T

(blo2flo 'buf 17 8)

==> 12.34

(blodump* 'buf)

00000001 | 00 00 00 00 00 A4 70 45 | 41 00 00 00 00 00 00 00 | .....¤pEA....... |
00000017 | AE 47 E1 7A 14 AE 28 40 | 00 00 00 00 00 00 00 00 | ®Gбz.®(@........ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00       | ..............   |

==> T

Здесь создается BLOB размером 46 байтов, а затем в него заносится число 12.34 со смещением 6 как короткое число с плавающей точкой (длиной 4 байта). Затем занесенное число извлекается. Далее, в этот же BLOB заносится то же число, но в формате длинного числа с плавающей точкой. Следует обратить внимание на то, что одно и то же число в разных форматах во внутреннем представлении не имеют ничего общего!

BLO2STR

Функция BLO2STR принимает три аргумента: идентификатор BLOBa, и два значения типа FIXED. Функция извлекает из тела BLOBa строку со смещения, заданного значением 2-го аргумента и длиной, заданной значением 3-го аргумента. Если смещение начала или конца извлекаемого числа выходит за пределы BLOBа, выдается сообщение об ошибке. Вот подробный пример вызова функции BLO2STR:

(bloCreate 'buf 35)

==> buf

(str2blo 'buf "Проба пера" 5)

==> T

(blodump* 'buf)

00000001 | 00 00 00 00 CF F0 EE E1 | E0 20 EF E5 F0 E0 00 00 | ....Проба пера.. |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00                |                         | ...              |

==> T

(blo2str 'buf 5 10)

==> "Проба пера"

(blo2str 'buf 5 8)

==> "Проба пе"

Здесь создается BLOB размером 35 байтов, а затем в него заносится строка Проба пера со смещением 5. Затем занесенная строка извлекается.

BLOCOPY

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

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

Вот развернутые примеры вызова функции BLOCOPY:

(bloCreate 'b1 50)

==> b1

(bloCreate 'b2 200)

==> b2

(blofill 'b1 102)

==> T

(blodump* 'b1)

00000001 | 66 66 66 66 66 66 66 66 | 66 66 66 66 66 66 66 66 | ffffffffffffffff |
00000017 | 66 66 66 66 66 66 66 66 | 66 66 66 66 66 66 66 66 | ffffffffffffffff |
00000033 | 66 66 66 66 66 66 66 66 | 66 66 66 66 66 66 66 66 | ffffffffffffffff |
00000049 | 66 66                   |                         | ff               |

==> T

(blocopy 'b1 'b2 1 12 16)

==> T

(blodump* 'b2)

00000001 | 00 00 00 00 00 00 00 00 | 00 00 00 66 66 66 66 66 | ...........fffff |
00000017 | 66 66 66 66 66 66 66 66 | 66 66 66 00 00 00 00 00 | fffffffffff..... |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000065 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000081 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000097 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000113 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000129 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000145 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000161 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000177 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000193 | 00 00 00 00 00 00 00 00 |                         | ........         |

==> T

(bloCopy 'b2 'b1)

bloCopy: область копии выходит за пределы приемника
==> ERRSTATE

Здесь создаются два BLOB-а: первый - размером 50 байтов, второй - размером 200 байтов. Первый BLOB заполняется кодом &H66. Для наглядности первый BLOB дампируется. Затем участок первого BLOB-а со смещения 1 длиной 16 байтов копируется во второй BLOB со смещения 12. Дамп BLOB-а-приемника подтверждает, что копия выполнена успешно. И, наконец, делается попытка скопировать второй BLOB в первый. Поскольку размер области копирования (весь второй BLOB - 200 байтов) больше размера приемника, выдается сообщение об ошибке.

BLOCREATE

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

(bloCreate 'b 3000)

==> b

(proplist 'b)

==> (BLOB Size 3000 BLOBHANDLE 1)

(bloCreate 'b 700)

BLOB b уже существует
==> ERRSTATE

(bloCreate _pi 200)

bloCreate: недопустимый флаг у первого аргумента
==> ERRSTATE

(putprop 'b 'Size 4000)

Попытка модификации списка свойств BLOBa
==> ERRSTATE

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

Попытка модифицировать список свойств BLOB-а завершается аварийно - список свойств защищен от модификации.

Следует обратить внимание на то, что даже если атом-идентификатор BLOB-а не участвует ни в одном из загруженных S-выражений, он не будет уничтожаться при сборке мусора. Этому препятствует установка стандартного флага BLOB.

BLODESTROY

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

При успехе функция BLODESTROY возвращает T. Ниже приводятся примеры вызова функции BLODESTROY.

bloCreate 'buf 100)

==> buf

(proplist 'buf)

==> (BLOB Size 100 BLOBHANDLE 1)

(bloDestroy 'buf)

==> T

(proplist 'buf)

==> NIL

(bloDestroy 'buf)

Аргумент у bloDestroy не имеет тип BLOB
==> ERRSTATE

Отчетливо видно, что список свойств атома-идентификатора очистился. Именно поэтому повторная попытка удаления BLOB-а вызывает ошибку.

BLODUMP*

Функция BLODUMP* выполняет дампирование BLOB-a. Дампирование заключается в формирования текстово - шестнадцатеричной картинки BLOB-a (эта форма представления давно стала негласным стандартом отображения двоичной информации). У функции BLODUMP* может быть до четырех аргументов. Первый (необязательный) - идентификатор открытого текстового потока (режим открытия должен быть _OUTPUT или _APPEND). Второй (обязательный) - идентификатор BLOB-a. Третий и четвертый параметры (типа FIXED) являются необязательными - третий задает смещение в BLOB-e, с которого начинается дампирование. Четвертый параметр задает длину дампируемого участка. Если третий и четвертый параметры опущены, выполняется дампирование всего BLOB-a. Если задан только третий параметр, то дампирование выполняется с задаваемого этим параметром смещения и до конца BLOB-a. При успехе функция возвращает T.

Если первый параметр (выходной файл) задан, то дамп помещается в этот файл, в противном случае дамп выводится в область вывода среды разработки.

Кстати, по имеющимся у автора сведениям, ни в одном из "расхожих" языков программирования нет аналога функции BLODUMP* - эти действия нужно программировать самому. Пользователь HomeLisp-а может без труда "заглядывать" в любые файлы (включая двоичные)!

(bloCreate 'buf 36)

==> buf

(bloFill 'buf 65)

==> T

(bloDump* 'buf)

00000001 | 41 41 41 41 41 41 41 41 | 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA |
00000017 | 41 41 41 41 41 41 41 41 | 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA |
00000033 | 41 41 41 41             |                         | AAAA             |

==> T

(filOpen 'fo ".\o.out" _OUTPUT)

==> fo

(bloDump* 'fo 'buf)

==> T

(filClose 'fo)

==> T

(fix2blo 'buf 112233 7 4)

==> T

(blodump* 'buf 7)

00000007 | 69 B6 01 00 41 41 41 41 | 41 41 41 41 41 41 41 41 | i¶..AAAAAAAAAAAA |
00000023 | 41 41 41 41 41 41 41 41 | 41 41 41 41 41 41       | AAAAAAAAAAAAAA   |

==> T

Здесь создается BLOB размером 36 байтов. Затем он заполняется кодом "A", после чего выполняется полный дамп BLOB-а с помещением картинки в область вывода. Далее открывается выходной файл и дамп повторяется с указанием приемного файла первым параметром, после чего файл закрывается. Можно убедиться, что в текущей директории создался файл o.out с соответствующим содержимым. Затем в BLOB заносится 4-х байтовое целое 112233 (&H1B669) со смещения 7 и BLOB дампируется со смещения 7 до конца.

BLOEQ

Функция BLOEQ сравнивает содержимое двух BLOB-ов на полное совпадение. У функции два праметра, оба типа BLOB. Функция возвращает T если оба BLOB-ов имеют одинаковую длину и содержимое. В противном случае возвращается Nil.

Следует отметить, что для сравнения BLOB-ов бессмысленно применять классическую функцию EQ. Ведь функция EQ сравнивает атомы на совпадение. Применение функции EQ к двум разным BLOB-ам (даже одинаковой длины и идентичного содержимого) всегда будет давать Nil (ведь атомы-идентификаторы у BLOB-ов разные). И только вызов (EQ 'b1 'b1) даст в результате T. Ниже приводятся примеры вызова BLOEQ:

(bloCreate 'b1 40)

==> b1

(bloCreate 'b2 40)

==> b2

(bloeq 'b1 'b2)

==> T

(eq 'b1 'b2)

==> Nil

(bloCreate 'b3 45)

==> b3

(bloeq 'b1 'b3)

==> NIL

(fix2Blo 'b1 100 5 1)

==> T

(bloEq 'b1 'b2)

==> NIL

(fix2Blo 'b1 0 5 1)

==> T

(bloEq 'b1 'b2)

==> T

Создаются два BLOB-а размером по 40 байтов. Они сравниваются вызовом BLOEQ. Результат равен T, поскольку после создания BLOB-ы заполнены нулями. Попытка сравнить BLOB-ы вызовом EQ дает, естественно Nil.

Далее создается еще один BLOB размером 45 байтов. Сравнение его с первым BLOB-ом дает в результате Nil (из-за разности длин). Затем в BLOB b1 заносится байтовое значение 100. Теперь сравнение b1 и b2 дает в результате Nil, поскольку у BLOB-ов разное содержимое. Если заменить занчение 100 нулем, сравнение снова показывает идентичность.

BLOFILL

Функция BLOFILL заполняет BLOB, заданный первым аргументом, кодом, равным значению второго аргумента (типа FIXED). Значение второго аргумента должно быть беззнаковым целым в диапазоне [0,255].

Если значение первого аргумента не является BLOB-ом или значение второго аргумента меньше нуля или больше 255, фиксируется ошибка. Все это выглядит так:

(bloCreate 'b3 45)

==>  b3

(bloFill 'b3 55)

==>  T

(bloDump* 'b3)

00000001 | 37 37 37 37 37 37 37 37 | 37 37 37 37 37 37 37 37 | 7777777777777777 |
00000017 | 37 37 37 37 37 37 37 37 | 37 37 37 37 37 37 37 37 | 7777777777777777 |
00000033 | 37 37 37 37 37 37 37 37 | 37 37 37 37 37          | 7777777777777    |

==> T

(bloFill 'b3 11)

==>  T

(bloDump* 'b3)

00000001 | 0B 0B 0B 0B 0B 0B 0B 0B | 0B 0B 0B 0B 0B 0B 0B 0B | ................ |
00000017 | 0B 0B 0B 0B 0B 0B 0B 0B | 0B 0B 0B 0B 0B 0B 0B 0B | ................ |
00000033 | 0B 0B 0B 0B 0B 0B 0B 0B | 0B 0B 0B 0B 0B          | .............    |

==>  T

(bloFill 'b3 1123)

bloFill: значение аргумента COD вне допустимого диапазона
==>  ERRSTATE

(bloFill 'b 1123)

bloFill: аргумент, задающий BLOB не является BLOBом
==>  ERRSTATE

Создается BLOB размером 45 байтов и заполняется кодом 55 (&H37). Дамп BLOB-а показывает, что заполнение прошло успешно. Далее BLOB заполняется кодом 11 (&H0B) и снова дампируется. В заключение приводятся два ошибочных обращения к функции BloFill

BLOFSTR

Функция BLOFSTR выполняет поиск строки в BLOB-е. Функция ожидает три аргумента, из которых второй не обязателен. Первый аргумент, как всегда, задает BLOB. Второй (необязательный) аргумент задает смещение внутри BLOB-а, с которого начинается поиск. Третий аргумент задает строку поиска (значение типа STRING). Если строка поиска найдена, функция возвращает смещение этой строки от начала BLOB-а; в противном случае возвращается нуль.Вот развернутый пример использования Функции BLOFSTR:

(filOpen 'fi "testLisp.exe" _BINARY_READ)

==> fi

(bloCreate 'buf 300)

==> buf

(filGetBlob 'fi 'buf)

==> T

(bloDump* 'buf)

00000001 | 4D 5A 90 00 03 00 00 00 | 04 00 00 00 FF FF 00 00 | MZђ.........яя.. |
00000017 | B8 00 00 00 00 00 00 00 | 40 00 00 00 00 00 00 00 | ё.......@....... |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00 B8 00 00 00 | ............ё... |
00000065 | 0E 1F BA 0E 00 B4 09 CD | 21 B8 01 4C CD 21 54 68 | .є..ґ.Н!ё.LН!Th |
00000081 | 69 73 20 70 72 6F 67 72 | 61 6D 20 63 61 6E 6E 6F | is program canno |
00000097 | 74 20 62 65 20 72 75 6E | 20 69 6E 20 44 4F 53 20 | t be run in DOS  |
00000113 | 6D 6F 64 65 2E 0D 0D 0A | 24 00 00 00 00 00 00 00 | mode....$....... |
00000129 | 89 96 F9 DB CD F7 97 88 | CD F7 97 88 CD F7 97 88 | ‰–щЫНч—€Нч—€Нч—€ |
00000145 | 4E EB 99 88 CC F7 97 88 | 82 D5 9E 88 CC F7 97 88 | Nл™€Мч—€‚Хћ€Мч—€ |
00000161 | C9 D4 9A 88 CC F7 97 88 | 52 69 63 68 CD F7 97 88 | ЙФљ€Мч—€RichНч—€ |
00000177 | 00 00 00 00 00 00 00 00 | 50 45 00 00 4C 01 03 00 | ........PE..L... |
00000193 | AA 12 82 47 00 00 00 00 | 00 00 00 00 E0 00 0F 01 | Є.‚G........а... |
00000209 | 0B 01 06 00 00 20 00 00 | 00 20 00 00 00 00 00 00 | ..... ... ...... |
00000225 | C8 11 00 00 00 10 00 00 | 00 30 00 00 00 00 40 00 | И........0....@. |
00000241 | 00 10 00 00 00 10 00 00 | 04 00 00 00 01 00 00 00 | ................ |
00000257 | 04 00 00 00 00 00 00 00 | 00 50 00 00 00 10 00 00 | .........P...... |
00000273 | FA 0C 01 00 02 00 00 00 | 00 00 10 00 00 10 00 00 | ъ............... |
00000289 | 00 00 10 00 00 10 00 00 | 00 00 00 00             | ............     |

==> T

(blofstr 'buf "MZ")

==> 1

(blofstr 'buf "PE")

==> 185

(blofstr 'buf "DOS")

==> 109

(blofstr 'buf 120 "DOS")

==> 0

(blofstr 'buf (strCat (strchr 12) (strchr 1)))

==> 274

Здесь двоичный файл (формата EXE) открывается на чтение. Создается 300-байтовый буфер и в него читается первые 300 байт EXE-файла. Для наглядности BLOB дампируется. Затем делается попытка найти строки "MZ" (она, естественно, располагается в самом начале) и "PE" (она находится по смещению 185. Строка "DOS" находится по смещению 109. Попытка повторного поиска строки "DOS", начиная со смещения 120, возвращает нуль.

Следует отметить, что если требуется найти строку, состоящую из неграфических символов, то ее следует предварительно сформировать, используя строковые функции strCat и strChr (как показано в последнем примере). Из дампа буфера хорошо видно, что строка байтов 0C 01 действительно располагается по смещению 274.

BLOREPLACE

Функция BLOREPLACE выполняет глобальную замену в BLOB-е одной строки на другую. У функции четыре аргумента: первый аргумент задает BLOB, второй (необязательный, типа FIXED) - смещение в BLOB-е, с которой начинается замена. Третий аргумент (STRING) задает заменяемую строку, а последний аргумент (STRING) - заменяющую строку. Если заменяемая строка не найдена, BLOB не меняется. Если длины заменяемой и замещающей строк разные, происходит изменение размеров самого BLOB-а. Все это выглядит так:

(bloCreate 'b 40)

==> b

(proplist 'b)

==> (BLOB Size 40 BLOBHANDLE 1)

(str2blo 'b "**" 39)

==> T

(bloDump* 'b)

00000001 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 2A 2A |                         | ......**         |

==> T

(str2blo 'b "zz" 19)

==> T

(bloDump* 'b)

00000001 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000017 | 00 00 7A 7A 00 00 00 00 | 00 00 00 00 00 00 00 00 | ..zz............ |
00000033 | 00 00 00 00 00 00 2A 2A |                         | ......**         |

==> T

(bloReplace 'b "zz" "22222222")

==>  T

(bloDump* 'b)

00000001 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000017 | 00 00 32 32 32 32 32 32 | 32 32 00 00 00 00 00 00 | ..22222222...... |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 2A 2A       | ............**   |

==>  T

(proplist 'b)

==>  (BLOB Size 46 BLOBHANDLE 1)

Здесь создается 40-байтовый BLOB. Запрашивается его список свойств; видно, что размер BLOB равен 40. Далее в два последних байта заносится строка "**", а в середину - строка "zz". И, наконец, производится замена строки "zz" строкой "22222222" (замещающая строка на 6 байтов длинее заменяемой). Последующий дамп показывает успешность замены строки и сопутствующее увеличение длины BLOB-а на 6 байтов. Обращение к списку свойств подтверждает увеличение размеров BLOB-а.

FIX2BLO

Функция FIX2BLO заносит число типа FIXED в BLOB. Функция имеет четыре обязательных аргумента. Первый, как обычно, идентификатор BLOB-а. Второй аргумент задает число типа FIXED, которое будет заноситься в BLOB. Третий аргумент задает смещение, начиная с которого будет заноситься число. Последний аргумент задает длину в байтах, которую будет занимать заносимое значение. Этот аргумент может принимать значения 1, 2 или 4 (соответственно число будет занимать один байт, два или четыре байта). При занесении одного байта значение второго аргумента должно быть в диапазоне [0,255]; для двухбайтовой величины значение второго аргумента должно принадлежать диапазону [-32768,32767], а для четырехбайтовой величины значение второго аргумента должно принадлежать диапазону [-2147483647,2147483647]. При удачном завершении операции функция возвращает значение T. Вот примеры вызова Функции FIX2BLO:

(bloCreate 'buf 60)

==> buf

(fix2blo 'buf 45 12 1)

==> T

(fix2blo 'buf 455 32 1)

blo2fixed: значение 2-го аргумента вне диапазона, заданного 4-м аргументом
==> ERRSTATE

(fix2blo 'buf -55 32 1)

blo2fixed: значение 2-го аргумента вне диапазона, заданного 4-м аргументом
==> ERRSTATE

(fix2blo 'buf -55 32 2)

==> T

(blodump* 'buf)

00000001 | 00 00 00 00 00 00 00 00 | 00 00 00 2D 00 00 00 00 | ...........-.... |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 C9 | ...............Й |
00000033 | FF 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | я............... |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00             | ............     |

==> T

Здесь создается BLOB длиной 60 байтов. Затем в BLOB заносится байтовая величина 45 по смещению 12. Две последующие попытки заканчиваются неудачей - предлагаемое значение оказывается вне допустимого диапазона. После замены размера области, отводимого под число, операция завершается успешно.

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

FLO2BLO

Функция FLO2BLO заносит число типа FLOAT в BLOB. Функция имеет четыре обязательных аргумента. Первый, как обычно, идентификатор BLOB-а. Второй аргумент задает число типа FLOAT, которое будет заноситься в BLOB. Третий аргумент задает смещение, начиная с которого будет заноситься число. Последний аргумент задает длину в байтах, которую будет занимать заносимое значение. Этот аргумент может принимать значения 4 или 8. Соответственно число будет занимать четыре байта (короткое целое) или восемь байтов (длинное целое). При удачном завершении операции функция возвращает значение T. Вот примеры вызова Функции FLO2BLO:

(bloCreate 'buf 60)

==> buf

(flo2blo 'buf 0.5 2 4)

==> T

(flo2blo 'buf 456.789 12 8)

==> T

(blodump* 'buf)

00000001 | 00 00 00 00 3F 00 00 00 | 00 00 00 B4 C8 76 BE 9F | ....?......ґИvѕџ |
00000017 | 8C 7C 40 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | Њ|@............. |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00             | ............     |

==> T

После дампирования видно, как заносимая информация располагается внутри BLOB-а. Знакомые с внутренним числа с плавающей точкой, могут убедиться, что числа представлены верно.

STR2BLO

Функция STR2BLO заносит в BLOB строку. Функция имеет три обязательных аргумента. Первый, как обычно, идентификатор BLOB-а. Второй аргумент задает строку (STRING), которая будет заноситься в BLOB. Третий аргумент задает смещение, начиная с которого будет заноситься строка. При удачном завершении операции функция возвращает значение T. Вот примеры вызова Функции STR2BLO:

(bloCreate 'buf 60)

==> buf

(str2blo 'buf "Проба пера" 6)

==> T

(blodump* 'buf)

00000001 | 00 00 00 00 00 CF F0 EE | E1 E0 20 EF E5 F0 E0 00 | .....Проба пера. |
00000017 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00             | ............     |

==> T

После дампирования видно, как заносимая информация располагается внутри BLOB-а.


Описание встроенных функций "классического" Лиспа

Описание встроенных функций HomeLisp

В этом разделе описываются встроенные функции HomeLisp, принадлежащие "классическому" Лиспу. Следует отметить, что разработчик использовал известные понятия SUBR/FSUBR в несколько ином смысле, нежели Лавров и Силагадзе в своей замечательной книге [1]. В HomeLisp у функций типа SUBR все аргументы вычисляются, но допускаются функции с переменным числом аргументов, тогда как у Лаврова и Силагадзе количество аргументов функций типа SUBR должно быть фиксированным. У функций типа FSUBR вычисляются не все аргументы (какие именно - зависит от конкретной функции). Таким образом, в HomeLisp функции PLUS, TIMES, MAX принадлежат к типу SUBR (тогда, как у Лаврова и Силагадзе эти функции принадлежат к типу FSUBR, т.к. они имеют неопределенное число аргументов).

Функции, возвращающие логические значения T или Nil, называются предикатами. Некоторые предикаты предназначены для проверки типа своего аргумента (FIXEDP, FLOATP и т.д.). В Лиспе принято, что имена таких предикатов оканчиваются буквой p.

Ниже довольно часто используется термин "квотирование". Смысл его отличается от общеупотребительного: квотировать - это не выдавать строго отмеренное количество чего-либо, а применять к S-выражению функцию QUOTE. Квотирование атома a, означает запись (QUOTE a) или, что равнозначно - 'a.

Далее приводится описание всех встроенных в HomeLisp функций. При этом звездочкой (*) выделены функции, отсутствующие в реализации, описанной в книге Лаврова и Силагадзе. Если та или иная функция реализована только в 11-м или только в 13-м ядре, то номер ядра указывается в скобках красным цветом.

Возможно также, что некоторые функции ведут себя по-разному в составе 11-го и 13-го ядер. Эти моменты комментируются явным образом.

Имя функции | Тип функции | К-во аргументов | Тип аргументов | Выполняемое действие
ABS | SUBR | 1 | FIXED, FLOAT | Вычисляет абс. Величину значения аргумента
ADD1 | SUBR | 1 | FIXED, FLOAT | Прибавляет к значению аргумента единицу
AND | FSUBR | переменное | ANY | Последовательно вычисляет свои аргументы. Если значение аргумента равно Nil, то функция возвращает Nil. Если значение всех аргументов отлично от Nil, то функция возвращает значение последнего аргумента.
APPLY * | SUBR | 2 | 1-ATOM или лямбда-выражение; 2-Список | Применяет первый аргумент (как функцию) к элементам списка, составляющим второй аргумент.
ATOM | SUBR | 1 | ANY | Проверяет, является ли значение аргумента атомом
BACKQUOTE * (13) | FSUBR | 1 | Список | Обратная блокировка (квотирование с частичным вычислением)
BITSP | SUBR | 1 | BITS | Если значение аргумента имеет тип BITS, функция возвращает T; в противном случае возвращается Nil.
BIT2FIX * | SUBR | 1 | BITS | Преобразует битовое значение в целое (FIXED)
CAAR | SUBR | 1 | Список | (Car (car *))
CADAR | SUBR | 1 | Список | (Car (Cdr (car *)))
CADDDR | SUBR | 1 | Список | (Car (Cdr (Cdr (Cdr *))))
CADDR | SUBR | 1 | Список | (Car (Cdr (Cdr *)))
CADR | SUBR | 1 | Список | (Car (Cdr *))
CAR | SUBR | 1 | Список | Вычисление головы списка
CDAR | SUBR | 1 | Список | (Cdr (Car *))
CDDDR | SUBR | 1 | Список | (Cdr (Cdr (Cdr *)))
CDDR | SUBR | 1 | Список | (Cdr (Cdr *))
CDR | SUBR | 1 | Список | Вычисление хвоста списка
COND FSUBR переменное - Обеспечивает вычисление условных выражений
CONS | SUBR | 2 | ANY | Объединяет свои аргументы в пару
CONTEXT * (13) | SUBR | 1 | ANY | Показвает лексический контекст функции или лямбда-выражения. При вызове без параметров показывает текущий контекст.
CSETQ | FSUBR | 2 | 1-Атом, 2-ANY | Создает константу
DEFDYN * (13) | FSUBR | 1 | АТОМ | Создает динамическую переменную (не присваивая ей значения)
DEFGLOB * (13) | FSUBR | 2 | 1 - АТОМ; 2 - ANY | Создает глобальную переменную
DEFMACRO * | FSUBR | 3 | 1-Атом, 2,3-Списки | Создает функцию типа MACRO (упрощенная форма SMACRO)
DEFUN * | FSUBR | 3 | 1-Атом, 2,3-Списки | Создает функцию типа EXPR (упрощенная форма SEXPR)
DEFUNF * | FSUBR | 3 | 1-Атом, 2,3-Списки | Создает функцию типа FEXPR (упрощенная форма SFEXPR)
DEFVAR * (13) | FSUBR | 2 | 1-Атом, 2 - ANY | Создает динамическую переменную (с возможным присвоением значения)
DIFFERENCE (-) | SUBR | переменное >= 2 | FIXED, FLOAT | Вычисляет разность первого и всех остальных аргументов
DIVIDE (/) * | SUBR | 2 | FIXED/FLOAT | Вычисляет частное первого и второго аргументов
DO/DO* * (13) | FSUBR | >= 2 | Списки | Общий оператор цикла по группе переменных.
DOLIST * (13) | FSUBR | >= 2 | Списки | Цикл по заданному списку.
DOTIMES * (13) | FSUBR | 2 | Списки | Цикл заданное число раз.
DOTPRINT * | SUBR | 1 | ANY | Печатает значение своего аргумента в точечной записи.
EQ | SUBR | 2 | ANY | Проверяет идентичность атомов, заданных первым и вторым аргументами.
EQL * | SUBR | 2 | ANY | Проверяет идентичность объектов, заданных значениями первого и второго аргумента.
EVAL | SUBR | 1 | ANY | Вычисляет S-выражение
ERRORMESSAGE * SUBR - - Возвращает сообщение об ошибке в виде строки
EXPLODE * | SUBR | 1 | ATOM | "Взрывает" атом, разбивая имя атома на отдельные символы
EXPT | SUBR | 2 | FIXED, FLOAT | Вычисляет степень
FIX | SUBR | 1 | FIXED, FLOAT | Если значение аргумента имеет тип FLOAT, функция возвращает значение аргумента, округленное до ближайшего целого. Тип результата - FLOAT
FIXEDP | SUBR | 1 | ANY | Если значение аргумента имеет тип FIXED, функция возвращает T; в противном случае возвращается Nil.
FIX2BIT * | SUBR | 1 | FIXED | Переводит целое FIXED в соответствующую строку битов
FIX2FLO * | SUBR | 1 | FIXED | Переводит целое FIXED в тип FLOAT.
FLO2FIX * | SUBR | 1 | FLOAT | Переводит число FLOAT в тип FIXED.
FLO2STR * | SUBR | 1 | FLOAT | Переводит число FLOAT в строку.
FLOATP * | SUBR | 1 | ANY | Если значение аргумента имеет тип FLOAT, функция возвращает T; в противном случае возвращается Nil.
FORMAT * | SUBR | 2 | 1-й - FIXED или FLOAT, 2-й - STRING | Превращает значение первого аргумента в строку, выполняя форматирование по строке формата (2-й аргумент).
FUNCALL * | SUBR | Переменное | 1-й - ATOM (или лямбда-выражение); Остальные - произвольны. | Применяет первый аргумент (как функцию) к остальным элементам списка параметров. Аналогична функции APPLY, но аргументы задаются не списком, а по отдельности.
FUNCTION | FSUBR | 1 | Атом или Список | Задает функциональный аргумент
GC SUBR 0 - Вызывает сборку мусора
GENSYM (13) | SUBR | 1 | Атом | Строит уникальный (еще не существующий) символ
GETD | FSUBR | 1 | Атом | Выдает тело функции и с именем, заданным аргументом-атомом
GETEL * SUBR 2 1 - Список;
2 - FIXED 		Возвращает элемент списка-значения первого аргумента с номером, заданным значением второго аргумента.
GEVAL * (11) | SUBR | 1 | ANY | Выполняет действия, аналогичные функции EVAL, но в глобальном контексте.
GO | FSUBR | 1 | Атом | Перход на атом-метку
GREATERP | SUBR | 2 | ANY | Предикат, проверяющий, что значение первого аргумента больше второго
GREQP | SUBR | 2 | FIXED, FLOAT | Предикат, проверяющий, что значение первого аргумента больше или равно значению второго
GSET * (11) | SUBR | 2 | 1-й - атом; 2-й - ANY. | Выполняет действия, аналогичные функции SET, но атому присваивается значение в глобальном контексте.
IF * (13) | FSUBR | 3 | Формы | Условный оператор If (...) then (...) else (...)
IMPLODE * (13) | SUBR | 1 | список | "Сжимает" одноуровневый список в атом
LABELS * (13) | SUBR | 2 | ANY | Создает и выполняет локальные функции
LAMBDA | FSUBR | 2 | ANY | Задает лямбда-выражение с предварительным вычислением аргументов
LEFTSHIFT | SUBR | 2 | 1-BITS; 2-FIXED | Логический левый сдвиг значения первого аргумента на значение второго
LESSP | SUBR | 2 | FIXED, FLOAT | Предикат, проверяющий, что значение первого аргумента меньше второго
LET/LET* * (13) | FSUBR | Переменное | ANY | Вычисление в заданном лексическом контексте
LIST | SUBR | Переменное | ANY | Строит список из значений своих аргументов
LOGAND | SUBR | Переменное | BITS | Вычисляет логическое AND значений всех аргументов
LOGOR | SUBR | Переменное | BITS | Вычисляет логическое OR значений всех аргументов
LOGXOR | SUBR | Переменное | BITS | Вычисляет логическое XOR значений всех аргументов
LOOP * (13) | FSUBR | Переменное | ANY | Бесконечный цикл
MACROEXPAND * | FSUBR | Переменное >= 1 | 1-Атом, … | Вычисляет макрорасширение от вызова MACRO с именем, заданным 1-м аргументом, и списком параметров, заданным остальными аргументами
MAX | SUBR | Переменное | FIXED, FLOAT | Вычисляет максимум своих аргументов
MIN | SUBR | Переменное | FIXED, FLOAT | Вычисляет минимум своих аргументов
MINUS | SUBR | 1 | FIXED, FLOAT | Меняет знак значения своего аргумента
MINUSP | SUBR | 1 | FIXED, FLOAT | Проверяет, что значение аргумента отрицательно
NLAMBDA * (13) | FSUBR | 2 | Списки | Задает лямбда-выражение без предварительного вычисления аргументов
NOT(NULL) | SUBR | 1 | ANY | Проверяет, что аргумент является пустым списком
NUMBERP | SUBR | 1 | FIXED, FLOAT | Проверяет, является ли значение аргумента числом
ONEP | SUBR | 1 | ANY | Проверяет является ли значение аргумента единицей
OR | FSUBR | переменное | ANY | Вычисляет логическое OR значений всех аргументов. Если значение аргумента отлично от Nil, оно считается равным T.
PARSE * | SUBR | 1 | STRING | Выполняет разбор алгебраического выражения на лексемы (переменные, константы, знаки операций, скобки). Результат представляется в виде списка.
PLUS (+) | SUBR | переменное >= 2 | FIXED, FLOAT | Вычисляет сумму всех аргументов
PRINT/PRINTLINE * | SUBR | 1 | ANY | Печатает значение своего аргумента
PRINTS/PRINTSLINE * | SUBR | 1 | ANY | Печатает значение своего аргумента, но не заключает строки в объемлющие кавычки.
PROG | FSUBR | переменное | ANY | Задает PROG-конструкцию
PROG1/PROG2/PROGN * (13) | FSUBR | переменное | ANY | Организация программного блока
PROPLIST | SUBR | 1 | АТОМ | Возвращает список свойств атома-аргумента
PUTD * | FSUBR | 1 | Список | Устанавливает у функции с именем, заданным значением 1-го аргумента новое определяюще выражение, заданное вторым аргументом. Тип функции (EXPR, FEXPR или MACRO) не меняется.
PUTEL * SUBR 3 1 - Список;
2 - Атом;
3 - Any. 		Возвращает список, заданный значением первого аргумента, у которого элемент, с номером, заданным значением второго аргумента, установлен равным значению третьего аргумента.
QUOTE | FSUBR | 1 | ANY | Блокирует вычисление аргумента
QUOTIENT (\) | SUBR | 2 | FIXED | Вычисляет целочисленное частное первого и второго аргументов
REMAINDER (%) | SUBR | 2 | FIXED | Вычисляет остаток от деления первого и второго аргументов
RETURN | SUBR | 1 | ANY | Завершает выполнение PROG-конструкции с возвратом значения 1-го аргумента
RIGHTSHIFT * | SUBR | 1 | ANY | Логический правый сдвиг первого аргумента на значение второго
RPLACA | SUBR | 2 | 1-Спиок, 2-ANY | Устанавливает значение a-указателя, записав в него значение 2-го аргумента
RPLACD | SUBR | 2 | 1-Спиок, 2-ANY | Устанавливает значение d-указателя, записав в него значение 2-го аргумента
SET | SUBR | 2 | 1-ATOM, 2-ANY | То же самое, что SETQ, но 1-й аргумент вычисляется
SETQ | FSUBR | 2 | 1-ATOM, 2-ANY | Присваивает значению атома, заданному 1-м аргументом, значиения, заданному значением 2-го. 1-й агрумент не вычисляется.
SEXPR | FSUBR | 2 | 1-АТОМ, 2-Список | Определяет функцию типа EXPR с определяющим выражением, заданным 2-м аргументом (которым должно быть лямбда-выражение)
SFEXPR | FSUBR | 2 | 1-АТОМ, 2-Список | Определяет функцию типа FEXPR с определяющим выражением, заданным 2-м аргументом (которым должно быть лямбда-выражение)
SMACRO * | FSUBR | 2 | 1-АТОМ, 2-Список | Определяет функцию типа MACRO с определяющим выражением, заданным 2-м аргументом (которым должно быть лямбда-выражение)
SPECIALP * (13) | FSUBR | 1 | АТОМ | Проверяет, является ли аргумент динамической переменной
SPROPL | SUBR | 2 | 1-Атом, 2-Список | Устанавливает у атома, заданного значением 1-го аргумента, новый список свойств, заданным значением 2-го аргумента
STRINGP * | SUBR | 1 | ANY | Если значенение аргумента имеет тип STRING, возвращается T, в противном случае - Nil
STR2FIX * | SUBR | 1 | STRING | Превращает значение аргумента в число типа FIXED
STR2FLO * | SUBR | 1 | STRING | Превращает значение аргумента в число типа FLOAT
SUB1 | SUBR | 1 | FIXED, FLOAT | Отнимает от значения аргумента единицу
SWAPEL * | SUBR | 3 | LIST, FIXED, FIXED | Меняет местами элементы списка, заданного значением первого аргумнта. Номера обмениваемых элементов задаются значением второго и третьего аргументов.
TERPRI SUBR 0 - Пропускает строку в области вывода
TIMES (*) | SUBR | переменное >= 2 | FIXED, FLOAT | Вычисляет произведение всех аргументов
TRY * | SUBR | 3 | 1-S-выражение; 2-Атом "EXCEPT"; 3-S-выражение; | Вычисление S-выражения с перехватом ошибки.
WHEN * (13) | FSUBR | переменное | формы | Вычислить, если истино
UNLESS * (13) | FSUBR | переменное | формы | Вычислить, если ложно
UNSET * | SUBR | 1 | Атом | Уничтожает переменную, заданную значением аргумента.
ZEROP | SUBR | 1 | ANY | Проверяет является ли значение аргумента нулем
ABS

Функция ABS принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет абсолютною величину аргумента. Тип результата совпадает с типом аргумента. Примеры:

(ABS -7.0)

==> 7.0

(PROPLIST (ABS -7.0))

==> (FLOAT)

(ABS 54)

==> 54

(PROPLIST (ABS 54))

==> (FIXED)
ADD1

Функция ADD1 принимает единственный аргумент числового типа (FIXED или FLOAT). Она прибавляет единицу к значению аргумента. Тип результата совпадает с типом аргумента. Примеры:

(ADD1 -7.0)

==> -6.0

(PROPLIST (ADD1 -7.0))

==> (FLOAT)

(ADD1 54)

==> 55

(PROPLIST (ADD1 54))

==> (FIXED)
AND

Функция AND принимает произвольное число аргументов. Первый же аргумент, имеющий значений NIL вызывает прекращение вычисление, а функция возвращает значение NIL. Если значение всех аргументов функции отлично от NIL, то функция принимает значение, равное значению последнего аргумента. Примеры:

(AND 1 2 3)

==> 3

(AND 1 (CDR '(1)) (/ 3 0))

==> NIL

Обратите внимание, во втором примере третий аргумент (/ 3 0) даже не вычислялся (в противном случае возникла бы ошибка "деление на нуль"). Это происходит вследствие того, что AND - функция класса FSUBR. Ее аргументы не вычисляются (точнее вычисление происходит не перед, а после вызова в теле функции от первого к последнему). Второй аргумент после вычисления дает Nil, и вычисление прекращается. Если во втором аргументе заменить CDR на CAR, то возникнет ошибка "деление на нуль".

APPLY

Функция APPLY принимает ровно 2 аргумента. Первым аргументом может быть атом или лямбда-выражение. Вторым аргументом должен быть список. Функция APPLY применяет первый аргумент к элементам списка, которые составляют второй аргумент.
Так, вызов:

(Apply fn Список)

полностью эквивалентен вызову:
(fn 'x1 'x2 ... 'xn)

При этом предполагается, что:

Список = (x1 x2 ... xn)

Примеры:

(apply 'plus '(1 6))

==>  7

(apply 'cons '(язык (программирования)))

==>  (язык программирования)

(setq f 'Plus)

==>  PLUS

(apply f '(7 8))

==>  15

(apply 'apply '(plus (5 7)))

==>  12

(apply '(lambda (x y) (* x y)) '(9 8))

==>  72

(apply 'plus 1 6)

Третий аргумент APPLY - не список
==>  ERRSTATE

В последнем примере допущена ошибка - аргументы функции PLUS, которая вызывается через APPLY, не сгруппированы в список. Функция APPLY отсутствует в реализации Лаврова и Силагадзе, она (как и описанная ниже функция FUNCALL) введена в HomeLisp для совместимости с современнымы версиями Лиспа.

ATOM

Функция ATOM принимает единственный аргумент. Она проверяет, является ли аргумент атомом (в этом случае возвращается значение T. В противном случае возвращается значение Nil. Примеры:

(ATOM 1)

==> T

(ATOM '(1 2 3))

==> NIL

(ATOM (+ 2 3))

==> T

Третий пример может вызвать недоумение: ведь аргументом здесь является список (+ 2 3), а не атом! Тем не менее функция возвращает T. Ведь функция ATOM принадлежит классу SUBR; ее аргументы вычисляются. Это означает, что перед тем, как вычислять выражение (ATOM ...) будет вычислено выражение (+ 2 3). Значение этого выражения - атом 5. После вычисления (+ 2 3) будет, таким образом, вычисляться выражение (ATOM 5), значение которого, естественно равно T. Новичку полезно вычислить выражение (ATOM (+ 2 3)), включив режим дампирования. По дампу будет видно, как происходит вычисление выражения:

.EVAL вход:  (ATOM (PLUS 2 3))
..CAR вход: A= (ATOM (PLUS 2 3))
..CAR выход: Рез= ATOM
..CDR вход: A= (ATOM (PLUS 2 3))
..CDR выход: Рез= ((PLUS 2 3))
..APPLY вход: A1= ATOM A2= ((PLUS 2 3))
...EVFUN вход: A1= ATOM A2= ((PLUS 2 3))
....LIST вход: A= ((PLUS 2 3))
.....CAR вход: A= ((PLUS 2 3))
.....CAR выход: Рез= (PLUS 2 3)
.....EVAL вход:  (PLUS 2 3)
......CAR вход: A= (PLUS 2 3)
......CAR выход: Рез= PLUS
......CDR вход: A= (PLUS 2 3)
......CDR выход: Рез= (2 3)
......APPLY вход: A1= PLUS A2= (2 3)
.......EVFUN вход: A1= PLUS A2= (2 3)
........LIST вход: A= (2 3)
.........CAR вход: A= (2 3)
.........CAR выход: Рез= 2
.........EVAL вход:  2
..........EVATOM вход:  2
..........EVATOM выход:  2
.........EVAL выход:  2
.........CDR вход: A= (2 3)
.........CDR выход: Рез= (3)
.........CAR вход: A= (3)
.........CAR выход: Рез= 3
.........EVAL вход:  3
..........EVATOM вход:  3
..........EVATOM выход:  3
.........EVAL выход:  3
.........CDR вход: A= (3)
.........CDR выход: Рез= NIL
........LIST выход: Рез= (2 3)
........PLUS вход: A= (2 3)
.........CAR вход: A= (2 3)
.........CAR выход: Рез= 2
........PLUS Пром. Рез=2
.........CDR вход: A= (2 3)
.........CDR выход: Рез= (3)
.........CAR вход: A= (3)
.........CAR выход: Рез= 3
........PLUS Пром. Рез=5
.........CDR вход: A= (3)
.........CDR выход: Рез= NIL
........PLUS выход: Рез= 5
.......EVFUN выход:  5
......APPLY выход:  5
.....EVAL выход:  5
.....CDR вход: A= ((PLUS 2 3))
.....CDR выход: Рез= NIL
....LIST выход: Рез= (5)
....CAR вход: A= (5)
....CAR выход: Рез= 5
....ATOM вход: A= 5
....ATOM выход: Рез= T
...EVFUN выход:  T
..APPLY выход:  T
.EVAL выход:  T
BACKQUOTE

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

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

Если перед атомом или списком, входящим в состав аргумента стоит ,@, то элемент также вычисляется, но "присоединяется" к результату на том же уровне вложенности.

Вызов функции BACKQUOTE можно записывать с помощью символа "`" обратный апростроф (по аналогии с записью QUOTE с помощью обычного апострофа. Вот поясняющие примеры:

(setq p '(1 2 3))

==> (1 2 3)
Создана глобальная переменная p

`(1 2 ,p 3 4)

==> (1 2 (1 2 3) 3 4)

`(1 2 ,p 3 4 ,@p)

==> (1 2 (1 2 3) 3 4 1 2 3)

`(111 222 ,(fact 10) 333)

==> (111 222 3628800 333)

`(111 222 ,@(fact 10) 333)

BackQoute: Конструкция ",@" стоит перед атомом
==> ERRSTATE

`(111 222 ,@(mklist1 10) 333)

==> (111 222 10 9 8 7 6 5 4 3 2 1 333)

Здесь создана глобальная переменная p и ей присвоено значение (1 2 3). При вычислении `(1 2 ,p 3 4) конструкция ,p заменяется на список (1 2 3), причем этот список оказывается на более глубоком уровне вложенности (т.н. включающая обратная блокировка).

Далее показана разница в поведении ,ANY и ,@ANY: конструкция ,p превращается во вложенный список, тогда как список, получаемый при обработке ,@p присоединяется к текущему на том же уровне (т.н. присоединяющая обратная блокировка).

В следующем примере ,(fact 10) заменяется атомом 3628800. А вот попытка использовать конструкцию ,@(fact 10) вызывает ошибку - присоединяющая обратная блокировка может применяться только к спискам. Следующий пример показывает, что список присоединяется успешно.

Наиболее частое применение обратная блокировка находит при программировании macro. Вот пример, иллюстрирующий применение обратной блокировки в макро. Построим макрос, который по заданному списку строит новый "удвоенный" список (каждый элемент верхнего уровня исходного списка в результирующем списке заменяется двумя такими же элементами). Вот как выглядит такой макрос c использованием обратной блокировки:

(defmacro double-list (a-list)
   `(let ((ret nil))
         (dolist (x ,a-list)
             (setq ret (append ret (list x x))))
     ret)
)

==> double-list

(double-list '(a b c (d e f)))

==> (a a b b c c (d e f) (d e f))

(macroexpand double-list '(1 2 3))

==> (LET ((ret NIL))
          (DOLIST (x (QUOTE (1 2 3)))
          (SETQ ret (append ret (LIST x x)))) ret)

В сущности, макро строит предложение LET, в единственное место которого нужно подставить значение параметра (это место выделено красным). С использованием обратной блокировки задача решается очень наглядно и изящно: мы просто пишем нужное выражение, предварив его обратным апострофом, а в нужных местах перед элементами, которые требутся вычислить ставим запятую или пару ",@". Хорошо видно, что тело макро почти не отличается от результата макрорасширения!

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

(defmacro dbl-lst (a-list)
     (list 'let (list (list 'ret nil))
                (list 'dolist (list 'x a-list)
                (list 'setq 'ret
                      (list 'append 'ret (list 'list 'x 'x))))
           'ret
     )
)

==> dbl-lst

(dbl-lst '(a b c (d e f)))

==> (a a b b c c (d e f) (d e f))

(macroexpand dbl-lst '(1 2 3))

==> (LET ((ret NIL))
          (DOLIST (x (QUOTE (1 2 3)))
          (SETQ ret (append ret (LIST x x)))) ret)

Как можно убедиться, тело макроса dbl-lst воспринимается значительно труднее, чем тело макроса double-list. Связь тела макроса с результатом макрорасширения в данном случае совсем не очевидна (хотя результат макрорасширения макросов dbl-lst и double-list одинаков.

BITSP

Функция-предикат BITSP принимает единственный аргумент. Если значением аргумента является битовое значение, функция возвращает T. В противном случае функция возвращает Nil Примеры:

(BITSP '(1 2 3))

==> Nil

(BITSP &H55)

==> T

(BITSP (FIX2BIT 678))

==> T

В последнем примере сначала вычисляется выражение (FIX2BIT 678), имеющее значение &H2A6. Это значение имеет битовый тип, поэтому окончательный результат равен T.

BIT2FIX

Функция BIT2FIX, отсутствующая у Лаврова и Силагадзе, принимает единственный аргумент типа BITS. функция возвращает значение аргумента, преобразованное к типу FIXED. При этом, если старший бит аргумента установлен, то результатом будет отрицательное число (в этом случае предполагается, что отрицательные числа хранятся в дополнительном коде). Если аргумент функции BIT2FIX имеет тип, отличный от BITS, возникает ошибка. Примеры:

(BIT2FIX &H55)

==> 85

(BIT2FIX &HFFFFFFF7)

==> -9

(BIT2FIX 678)

==> Аргумент BIT2FIX должен иметь тип BITS
CAR

Функция CAR принимает единственный аргумент - список или точечную пару. Она возвращает голову списка (первый элемент списка на верхнем уровне). Если аргумент - не список и не пара (атом), то возникает ошибка. Примеры:

(CAR '(1 2 3))

==> 1

(CAR '((1 2) 3))

==> (1 2)

(CAR '(a . b))

==> a

(CAR 'a)

==> Аргумент CAR - атом (a)

Во втором примере результат получается (1 2), а не 1, как могло бы показаться при поверхностном взгляде. Аргумент CAR это список из ДВУХ элементов: списка (1 2) и атома 3. Функция CAR возвращает ПЕРВЫЙ элемент - список (1 2).

CDR

Функция CDR принимает единственный аргумент - список или точечную пару. Она возвращает хвост списка - список без первого элемента. Если аргумент не список и не пара (атом), то возникает ошибка. Примеры:

(CDR '(1 2 3))

==> (2 3)

(CDR '((1 2) 3))

==> (3)

(CDR '(a . b))

==> b

(CDR 'a)

==> Аргумент CDR - атом (a)

(CDR '(3))

 ==> Nil

Во втором примере результат получается (3), а не 3, как могло бы показаться новичку. Аргумент CDR это список из ДВУХ элементов: списка (1 2) и атома 3. Функция CDR возвращает список (3) - исходный список без первого элемента. Ситуацию может прояснить то обстоятельство, что в терминах точечных пар исходный список записывается так: ((1 . (2 . Nil)) . (3 . Nil)); применяя функцию CDR к этой конструкции, мы получим (3 . Nil), а это есть в точности (3). Вполне очевиден результат и последнего примера.

Комбинации CAR и CDR

Поскольку функции CAR и CDR применяются для выделения частей списков (разбора списков на составные части), в язык встроено несколько комбинаций CAR и CDR:

(CAAR ... )  это (CAR (CAR ...))

(CADAR ...)  это (CAR (CDR (CAR ...)))

(CADDR ...)  это (CAR (CDR (CDR ...)))

(CADDDR ...) это (CAR (CDR (CDR (CDR ...))))

(CADR ...)   это (CAR (CDR ...))

(CDAR ...)   это (CDR (CAR ...))

(CDDR ...)   это (CDR (CDR ...))

(CDDDR ...)  это (CDR (CDR (CDR ...)))

Естественно, что эти функции можно вычислить не для всякого значения аргумента. Так, при попытке вычислить (CAAR '(1 2)) возникнет ошибка, поскольку (CAR '(1 2)) равен 1, а попытка вычислить (CAR 1) приведет к ошибке, т.к. здесь аргумент CAR - не список и не точечная пара, а атом.

COND

Функция COND обеспечивает вычисление условных выражений. Общий вид функции COND таков:

(COND
     ((Условие-1) Результат-1)
     ((Условие-2) Результат-2)
     ...
     ((Условие-n) Результат-n)
     (T           Результат)
)

Вычисление функции COND происходит следующим образом: вычисляется (Условие-1); если получается результат, отличный от Nil, то в качестве результата выдается значение выражения Результат-1. Если (Условие-1) ложно (= Nil), то вычисляется (Условие-2). Если снова получается результат, отличный от Nil, то в качестве результата выдается значение выражения Результат-2 и т.д. Если в конструкции COND присутствует часть (T Результат) и ни одно из условий не оказалось истиным, то в качестве результата выдается значение выражения Результат. В случае отсутствия части (T Результат) при ложности всех условий, результатом COND будет Nil. Легко видеть, что функция COND похожа на оператор множественного выбора SELECT языка Visual Basic, а конструкция (T Результат) играет роль CASE ELSE.

Вот пример вызова функции COND.

Пусть были введены команды (SETQ x 6) , (SETQ y 7) и (SETQ z 1) . Другими словами, переменная x имеет значение 6, переменная y - значение 7, а переменная z имеет значение 1. Тогда:

(COND
      ((EQ x y) "Равно")
      (T        "Не равно")
 )

 ==> "Не равно"

 (COND
      ((EQ (+ x z) y) "Равно")
      (T              "Не равно")
 )

 ==> "Равно"
CONS

Функция CONS принимает два аргумента и объединяет их в точечную пару. Таким образом, если оба аргумента CONS - атомы, отличные от Nil, то функция вернет точечную пару, составленную из атомов. Если первый аргумент - атом, а второй - список, то результатом CONS будет новый список, получающийся добавлением первого аргумента к началу списка из второго аргумента. В частности, если первый аргумент - атом, а второй аргумент - Nil, то в результате получится одноэлементный список, состоящий из первого аргумента. Это происходит потому, что в Лиспе Nil считается пустым списком. А если оба аргумента CONS - списки, то в результате получится новый двухуровневый список, голова которого есть значение первого аргумента, а хвост - значение второго аргумента. Все это иллюстрируется следующими примерами:

(CONS 'A 'B)

==> A . B

(CONS 'A '(1 2 3))

==> (A 1 2 3)

(CONS 'A Nil)

==> (A)

(CONS '(A B C) '(1 2 3))

==> ((A B C) 1 2 3)
CONTEXT

Функция CONTEXT (реализованная только в 13-й версии ядра) принимает принимает один необязательный аргумент - атом. Если этот атом есть имя функции типа EXPR/FEXPR/MACRO и функция определена с сохранением лексического окружения (создано замыкание), то функция CONTEXT вернет лексическое окружение в виде списка пар ((Переменная Значение) (Переменная Значение) ... ). Аналогичный результат будет получен в случае, когда значением аргумента CONTEXT является замыкание, созданное на основе лямбда-выражения или FUNCTION. В остальных случаях функция CONTEXT возвращает Nil.

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

Рассмотрим некоторые примеры:

(context)

==> (Nil Nil)

(setq p '(1 2 3))

==> (1 2 3)
Создана глобальная переменная p

(context)

==> (NIL ((p (1 2 3))))

(defun fff (x y) (printline (context)) (list x y))

==> fff

(fff 666 777)

(((y 777) (x 666)) ((p (1 2 3))))

==> (666 777)

(defun ggg (x y &aux z) (printline (context)) (list x y))

==> ggg

(ggg 666 777)

(((z NIL) (y 777) (x 666)) ((p (1 2 3))))

==> (666 777)

(prog (x y)
      (setq x 111)
      (setq y 222)
      (printline (context))
      (return (+ x y))
)
(((y 222) (x 111)) ((p (1 2 3))))

==> 333

Выше показано, что вызов функции CONTEXT без параметров на верхнем уровне при отсутствии глобальных и динамических переменных возвращает "пустой" контекст - (Nil Nil).

После создания глобальной переменной p эта переменная попадает в глобальный контекст.

Далее создается простая функция двух переменных, в тело которой встроен вызов функции CONTEXT. Кстати, пусть читатель обратит внимание на то, что значение контекста печатается. Если опустить печать, функция просто вернет список значений своих аргументов, а контекст увидеть не удастся. Видно, что в теле функции появляется лексический контекст - параметрам присваиваются соответствующие значения.

В следующем примере показана функция ggg с дополнительным параметром. Этот параметр тоже попадает в лексический контекст. Наконец, последний пример показывает, что в лексический контекст попадают и PROG-переменные.

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

(setq z 111)

==> 111
Создана глобальная переменная z

(let ((c 0)) (defun next-c nil (setq c (add1 c))))

==> next-c

(next-c)

==> 1

(next-c)

==> 2

(context 'next-c)

==> ((c 2))

Видно, что когда функция CONTEXT отображает контекст замыкания, то отображается только лексический контекст (глобальная переменная z не отображается).

В следующем примере создается замыкание на основе лямбда-выражения. В контекст этого замыкания входят две переменные (a и b). Их значения показывает функция CONTEXT. Впрочем, значения лексических замыкания можно увидеть, просто напечатав значение переменной, которой присвоено замыкание.

(let ((a 111) (b 222)) (setq f (lambda (x) (list a b x))))

==> (CLOSURE (x) ((LIST a b x)) ((b 222) (a 111)))
Создана глобальная переменная f

f

==> (CLOSURE (x) ((LIST a b x)) ((b 222) (a 111)))

(context f)

==> ((b 222) (a 111))

(funcall f 333)

==> (111 222 333)

Пусть читатель обратит внимание на то, что в приведенном выше примере при вызове CONTEXT аргумент не квотируется. Следующий далее пример в общем аналогичен предыдущему (только замыкание здесь создается с помощью конструкции FUNCTION):

(defun g (x) (+ a b x))

==> g

(g 1)

Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

(setq ff (let ((a 111) (b 222)) (function g)))

==> (CLOSURE g ((a 111) (b 222)))
Создана глобальная переменная ff

(funcall ff 333)

==> 666

(context ff)

==> ((a 111) (b 222))
CSETQ

Функция CSETQ принимает два аргумента; первый аргумент должен быть атомом (он не вычисляется), а второй - произвольным S-выражением. Функция превращает атом (первый аргумент) в константу; при этом константа наследует тип вычисленного S-выражения. Значение константы впоследствии не может быть изменено (в отличие от переменной, создаваемой фунциями SET/SETQ). Если атом-аргумент является переменной, возникает ошибка. При попытке изменить значение константы повторным вызовом CSETQ возникает ошибка c диагностикой "Csetq - попытка изменить значение константы". То же будет иметь место при попытке изменить значение встроенной константы ( Nil, T, ERRSTATE, _Pi, _E, _Ver ). Так же закончится и попытка присвоить одному числу значение другого. Если же попытаться создать переменную, одноименную с константой с помощью функций SETQ/SET, то будет выдана диагностика "Попытка превратить константу ... в переменную". Ниже приводятся примеры на эту тему.

(CSETQ K 12)

==> 12

(CSETQ K -5)

==> Csetq - попытка изменить значение константы

(SETQ kk 3)

==> 3

(CSETQ kk 8)

==> Csetq - попытка превратить переменную в константу

(SETQ _Pi 5)

==> Попытка превратить константу _Pi в переменную

(CSETQ 1 2)

==> Csetq - попытка изменить значение константы

(CSETQ "a" "b")

==> Csetq - попытка изменить значение константы

(CSETQ Nil T)

==> Csetq - попытка изменить значение константы

HomeLisp располагает встроенным механизмом, позволяющим защитить значение константы (произвольное S-выражение) от модификации. Казалось бы, что если значением константы является сложное S-выражение, то его части могут быть изменены например вызовом функций RPLACA/RPLACD. В действительности же такая попытка завершится ошибкой:

(csetq AA '(a b c))

==> AA

AA

==> (a b c)

(rplaca AA 'd)

RPLACA: попытка модификации защищенной структуры
==> ERRSTATE

AA

==> (a b c)

(setq BB '(a b c))

==> (a b c)

(rplaca BB 'd)

==> (d b c)

BB

==> (d b c)

Из приведенного примера видно, что значения переменной может модифицироваться с помощью "разрушающих" функций rplaca/rplacd, а попытка модифицировать значение константы вызывает ошибку.

Замечание. В 13-й редакции ядра константа не может использоваться в качестве формального параметра функций. При попытке такого использования возникает ошибка:

(csetq n 'a)

==> n

(defun f (x n) (cons x (cons n nil)))

SEXPR: Неверна структура списка параметров (-13)
==> ERRSTATE

(defun f (t x) (cons t (cons x nil)))

SEXPR: Неверна структура списка параметров (-13)
==> ERRSTATE

Здесь видно, что невозможно определить функцию с формальным параметром-константой, неважно, встроенной (t) или определенной пользователем (n).

Приведенные далее сведения этого раздела относятся к 11-й редакции ядра HomeLisp.

Как отмечают Лавров и Силагадзе, связь, накладываемая на именования констант функцией CSETQ сильнее связи, накладываемой при именовании переменных лямбда-выражением. Константы нельзя (точнее - бесполезно!) использовать в качестве связанных переменных. Вот пример на эту тему из книги Лаврова и Силагадзе:

(csetq n 'a)

==> a

(defun f (t n) (cons t (cons n nil)))

==> f

(f 'b 'c)

==> (t a)

Казалось бы, вызов функции f должен дать список (b c), однако, поскольку в списке параметров функции стоят две константы - встроенная T и созданная пользователем N, связь с фактическими параметрами не срабатывает. Поучительно посмотреть, как отажается в дампе процесс вычисления:

.EVAL вход:  (f (QUOTE b)(QUOTE c))
..APPLY вход: A1= f A2= ((QUOTE b)(QUOTE c))
...EVFUN вход: A1= f A2= ((QUOTE b)(QUOTE c))
....LIST вход: A= ((QUOTE b)(QUOTE c))
.....EVAL вход:  (QUOTE b)
......APPLY вход: A1= QUOTE A2= (b)
.......EVFUN вход: A1= QUOTE A2= (b)
.......EVFUN выход:  b
......APPLY выход:  b
.....EVAL выход:  b
.....EVAL вход:  (QUOTE c)
......APPLY вход: A1= QUOTE A2= (c)
.......EVFUN вход: A1= QUOTE A2= (c)
.......EVFUN выход:  c
......APPLY выход:  c
.....EVAL выход:  c
....LIST выход: Рез= (b c)
....EVLAM вход: A1= ((T n)(CONS T (CONS n NIL))) A2= (b c)
.....PAIRLIS вход: ptrAasso=0 A1= (T n) A2= (b c)
.....
..... Ассоциативный список:
.....
.....  2  ->  (n . c)
.....  1  ->  (T . b)
.....
.....PAIRLIS выход: ptrAsso=2
.....EVAL вход:  (CONS T (CONS n NIL))
......APPLY вход: A1= CONS A2= (T (CONS n NIL))
.......EVFUN вход: A1= CONS A2= (T (CONS n NIL))
.......
....... Ассоциативный список:
.......
.......  2  ->  (n . c)
.......  1  ->  (T . b)
.......
........LIST вход: A= (T (CONS n NIL))
.........EVAL вход:  T
..........EVATOM вход:  T
..........EVATOM выход:  T
.........EVAL выход:  T
.........EVAL вход:  (CONS n NIL)
..........APPLY вход: A1= CONS A2= (n NIL)
...........EVFUN вход: A1= CONS A2= (n NIL)
...........
........... Ассоциативный список:
...........
...........  2  ->  (n . c)
...........  1  ->  (T . b)
...........
............LIST вход: A= (n NIL)
.............EVAL вход:  n
..............EVATOM вход:  n
..............EVATOM выход:  a
.............EVAL выход:  a
.............EVAL вход:  NIL
..............EVATOM вход:  NIL
..............EVATOM выход:  NIL
.............EVAL выход:  NIL
............LIST выход: Рез= (a NIL)
............CONS вход: A1= a A2= (NIL)
............CONS выход: Рез= (a)
...........EVFUN выход:  (a)
..........APPLY выход:  (a)
.........EVAL выход:  (a)
........LIST выход: Рез= (T (a))
........CONS вход: A1= T A2= ((a))
........CONS выход: Рез= (T a)
.......EVFUN выход:  (T a)
......APPLY выход:  (T a)
.....EVAL выход:  (T a)
....EVLAM выход: Рез= (T a)
...EVFUN выход:  (T a)
..APPLY выход:  (T a)
.EVAL выход:  (T a)

Видно, что несмотря на образование в ассоциативном списке правильных связей, при вычислении значений атомов T и n (выделено красным) ассоциативный список просто игнорируется. Вместе с тем, если ввести аналогичную функцию, формальные аргументы которой не являются константами, результат будет другой:

(defun f (x y) (cons x (cons y nil)))

==> f

(f 'b 'c)

==> (b c)
DEFDYN

Функция DEFDYN реализована только в 13-й редакции ядра. Функция (типа FSUBR) принимает один параметр параметр-атом и один необязательный - произвольное S-выражение. Функция DEFDYN является полным синонимом функции DEFVAR.

Если задан только один параметр-атом, то функция превращает этот символ в динамическую переменную (не присваивая нового значения и не меняя существующего).

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

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

Все сказанное иллюстрируется врезкой:

(defdyn v)

==> v

v

Assoc: Символ v не имеет значения (не связан).
==> ERRSTATE

(defdyn w 111)

==> w

w

==> 111

(defdyn w 222)

==> w

w

==> 111

Создана динамическая переменная v (точнее, с этого момента символ v будет задавать динамическую переменную). Но символ v пока не получил никакого значения. Далее создается динамическая переменная w с присвоением ей значения 111. Последующая попытка сменить значение переменной w с помощью DEFDYN не имеет успеха.

Ниже приведен пример, показывающий разницу в поведении лексических и динамических переменных.

(defun f (x) (* x a))

==> f

(let ((a 1)) (f 5))

Внутри LET: Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

(let ((a 1)) (printline (+ a 7)) (f 5))
8

Внутри LET: Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

(defdyn 'a)

==> a

(let ((a 1)) (printline (+ a 7)) (f 5))
8

==>5

Переменная a (по умолчанию - лексическая) становится динамической после вызова (defdyn 'a). Теперь значение переменной a доступно в теле функции f.

DEFGLOB

Функция DEFGLOB принадлежит к типу FSUBR и реализована только в 13-й редакции ядра HomeLisp Функция создает глобальную переменную и присваивает ей значение Nil. Другой способ создания глобальной переменной состоит в вызове SET/SETQ. Если переменная, заданная первым параметром SET/SETQ, не существует в текущем лексическом контексте, будет создана глобальная переменная. Поскольку вызовы SET/SETQ на верхнем уровне выполяются в пустом лексическом контексте, то такой вызов всегда создает или модифицирует глобальные переменные. В общем использование SET/SETQ и DEFGLOB совершенно равноправно; без DEFGLOB можно было бы обойтись. DEFGLOB введена в HomeLisp для "синтаксической симметрии". Рассмотрим примеры:

(defglob v)

==> v
Создана глобальная переменная v

v

==> NIL

(setq vv 777)

==> 777
Создана глобальная переменная vv

(varlist)

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|vv                |Глобальн.|777       |
|v                 |Глобальн.|NIL       |
+------------------+---------+----------+

==> T

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

(setq v 111)

==> 111
Создана глобальная переменная v

(defun f (x) (* x v))

==> f

(defun g (v) (f 7))

==> g

(g 222)

==> 777

(defdyn v)

==> v

(g 222)

==> 1554

Создана глобальная переменная v и функция f, относительно которой переменная v свободна. Далее создается функция g, у которой переменная v входит в список параметров. При вычислении (g 222) переменная v является лексической и значение 222 не попадает в функцию f, - берется глобальное значение 111 (результат = 777 = 7 * 111). Далее переменная v делается динамической; теперь при вычислении (g 222) в функции f будет взято значение 222 (фактический параметр при вызове g. Результат получается равным 1554 ( 222 * 7).

DEFMACRO

Функции DEFMACRO является упрощенной формой функции SMACRO. Понятие макро в Лиспе подробно рассмотрено в разделе, посвященном функции SMACRO. Функцию DEFMACRO разработчик ввел в HomeLisp для совместимости с современными версиями Лиспа.

Функция DEFMACRO принимает три параметра: имя создаваемой функции, список ее формальных параметров и тело функции. При удачном завершении функция возвращает атом-имя, а в системе появляется новая макрофункция.

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

(defmacro arlist (L OP) (cons OP L))

==> arlist

(arlist (1 2 3 4 5 6) *)

==> 720

(macroexpand arlist (1 2 3 4 5 6) *)

==> (* 1 2 3 4 5 6)

(arlist (1 2 3 4 5) +)

==> 15

(macroexpand arlist (1 2 3 4 5 ) +)

==> (+ 1 2 3 4 5)

Макро работает очень просто: создает новый список, в котором заданная операция является первым элементом. Впоследствии результат макро-расширения вычисляется (и получается верный результат).

DEFUN

Функция DEFUN добавлена исключительно для совместимости с более поздними версиями Лиспа. DEFUN позволяет создавать функции типа EXPR несколько проще, чем базовая функция SEXPR. Функция принимает три аргумента: имя вновь создаваемой функции (атом); список формальных параметров (список) и тело функции без атома LAMBDA (тоже список). При успехе DEFUN возвратит имя вновь созданной функции (первый аргумент). Создадим функцию, вычисляющую сумму элементов списка произвольной длины, состоящего из чисел или констант, имеющих числовое значение. Это будет рекурсивная функция, работающая следующим образом: если список пуст, функция должна вернуть нуль. Если аргумент функции - атом, то функция должна вернуть этот атом в качестве значения (предполагаем, что на вход функции подается список из чисел). Наконец, если аргумент - список, то функция берет значение головы списка и прибавляет к нему значение функции, взятой от хвоста списка (стандартное использование рекурсии). Все это выглядит так:

(defun Сумма (x)
             (cond ((null x) 0)
                   ((Atom x) x)
                   (T (+ (car x) (Сумма (Cdr x))))
              )
)

Если вычислить (Сумма '(1 2 3 4)), получим результат 10 - функция работает.

DEFUNF

Функция DEFUNF (как и функция DEFUN) добавлена для совместимости с более поздними версиями Лиспа. DEFUNF позволяет создавать функции типа FEXPR несколько проще, чем базовая функция SFEXPR. Функция принимает три аргумента: имя вновь создаваемой функции (атом); список формальных параметров (список) и тело функции (тоже список). При успехе DEFUNF возвратит имя вновь созданной функции (первый аргумент).

Создадим (следуя книге Лаврова и Силагадзе) функцию PAIRQ, которая объединяет два своих аргумента в список:

(defunf pairq (x y) (cons x (cons y nil)))

==> pairq

(pairq 1 2)

==> (1 2)

(pairq (1 2) (3 4))

==> ((1 2) (3 4))

Видно, что функция работает. Пользоваться ею, на первый взгляд, удобно (не нужно квотировать аргументы). Но при попытке вычислить функцию с выражениями на месте аргументов, мы получим обескураживающий результат:

(pairq (+ 2 3) (+ 7 8))

==> ((+ 2 3) (+ 7 8))

Вместо списка (5 15) мы получили совсем другой результат (чего бы не случилось, если бы функция принадлежала классу EXPR).

DEFVAR

Функция DEFVAR, реализованная только в 13-й версии ядра, является полным синонимом функции DEFDYN, описанной выше.

DIFFERENCE (-)

Функция DIFFERENCE вычисляет разность значений первого и всех остальных аргументов. Аргументов может быть произвольное количество. Допустимы только значения аргументов FIXED и FLOAT. Если все аргументы DIFFERENCE имеют тип FIXED, то и результат будет иметь тип FIXED. Если же среди аргументов DIFFERENCE встретился аргумент типа FLOAT, то и результат будет иметь тип FLOAT. Для удобства записи допустимо вместо (DIFFERENCE ...) писать (- ...). Вот примеры,иллюстрирующие сказанное:

(- 1 2 3)

==> -4

(PROPLIST (- 1 2 3))

==> (FIXED)

(DIFFERENCE 3 1.0)

==> 2.0

(PROPLIST (DIFFERENCE 3 1.0))

==> (FLOAT)
DIVIDE (/)

Функция DIVIDE (отсутствующая у Лаврова и Силагадзе) вычисляет частное от деления значения первого аргумента на значение второго. Аргументов должно быть ровно два. Тип аргументов может быть FIXED или FLOAT. При вычислении частного значения типа FIXED автоматически приводится к типу FLOAT. Результат всегда имеет тип FLOAT. Если абсолютная величина делителя оказывается меньше 1.0D-300, то возникает ошибка "Деление на нуль". В целях упрощения вместо вызова (DIVIDE ...) допустимо писать (/ ...). Следует обратить внимание на то, что функция DIVIDE не предназначена для целочисленного деления; для этого существуют функции QUOTIENT (частное) и REMAINDER (остаток). Примеры вызова:

(/ 1 2)

==> 0.5

(/ -6 0.5)

==> -12.0
DO/DO*

Функции DO и DO* - это самые общие функции организации циклов в Лиспе. Тип функций, естественно - FSUBR. Функции отсутствуют у Лаврова и Силагадзе, а в HomeLisp реализованы только в 13-й редакции ядра. Общий синтаксис функций таков:

     (DO/DO*  (
               (переменная1 значение1 шаг1)
               (переменная2 значение2 шаг2)
               …
               (переменнаяn значениеn шагn)
              )

              (условие_окончания форма_ок1 форма_ок2 … форма_окm)

              (форма1) (форма2) … (формаk)

     )

Функции выполняется следующим образом:

1. В текущий лексический контекст добавляются переменные цикла (переменная1, переменная2 ... переменнаяn). Им присваиваются соответствующие значения. При этом для функции DO переменные 1 - n вычисляются "одновременно" (как в LET), а для функции DO* - "параллельно" (как в LET*).

2. Вычисляется условие_окончания. Если оно истинно (т.е. не есть Nil), то вычисляются формы окончания - форма_ок1 , форма_ок2 … форма_окm Результат вычисления последней формы возвращается как результат DO/DO*.

3. Если условие окончания ложно, вычисляется тело цикла - формы: (форма1) (форма2) … (формаk).

4. Всем переменным (переменная1 , переменная2 … переменнаяn) присваиваются новые значения путем вычисления соответствующих форм (шаг1 , шаг2 … шагn) и происходит переход к п.2.

Все это проиллюстрируем примерами:

(DO (
      (i 1 (+ i 1))
      (j 1 (+ j 1)))
      ((> j 5) (printline 'ok) (printline 'ok) (printline 'ok))
      (print 'i=) (print i) (prints " ") (print 'j=) (printline j)
)

i=1 j=1
i=2 j=2
i=3 j=3
i=4 j=4
i=5 j=5
ok
ok
ok

==> ok

Еще один пример:

(DO (
      (i 1 (+ i 1))
      (j (+ 1 i) (+ j 1)))
      ((> j 5) (printline 'ok))
      (print 'i=) (print i) (prints " ") (print 'j=) (printline j)
)

Символ i не имеет значения (не связан).
==> ERRSTATE

Приведенный выше пример оканчивается ошибкой, поскольку при вычислении начального значения переменной j, переменная i еще недоступна. Но если вместо DO вызвать DO*, то ошибки не возникнет:

(DO* ((i 1 (+ i 1))
      (j (+ 1 i) (+ j 1)))
     ((> j 5) (printline 'ok))
     (print 'i=) (print i) (prints " ") (print 'j=) (printline j)
)

i=1 j=2
i=2 j=3
i=3 j=4
i=4 j=5
ok

==> ok

Если при вызове функции DO форма инициализации одной из переменных цикла использует предыдущую переменную, а эта переменная создана ранее (до вызова DO), то ошибка не возникнет, а будет использовано значение этой существующей переменной.

Более того, если функции DO/DO* используют существующие переменые (лексические или глобальные), то после возврата из функции их значения сохраняются. Вот пример, демонстрирующий, причину сохранения лексических переменных:

(let ((i 111) (j 222))
   (do ((i 1 (+ i 1))
        (j 1 (+ j 1)))
       ((> j 3) (printline 'ok))
       (varlist)
   )
   (printline i)
   (printline j)
)

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|i                 |Лексич.  |1         |
|j                 |Лексич.  |1         |
|j                 |Лексич.  |222       |
|i                 |Лексич.  |111       |
+------------------+---------+----------+

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|i                 |Лексич.  |2         |
|j                 |Лексич.  |2         |
|j                 |Лексич.  |222       |
|i                 |Лексич.  |111       |
+------------------+---------+----------+

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|i                 |Лексич.  |3         |
|j                 |Лексич.  |3         |
|j                 |Лексич.  |222       |
|i                 |Лексич.  |111       |
+------------------+---------+----------+
ok
111
222

==> 222

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

(setq i 111)

==> 111
Создана глобальная переменная i

(setq j -111)

==> -111
Создана глобальная переменная j

(do ((i 1 (+ i 1))
     (j 1 (+ j 1)))
     ((> j 3) (printline 'ok))
     (varlist)
)

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|i                 |Лексич.  |1         |
|j                 |Лексич.  |1         |
|j                 |Глобальн.|-111      |
|i                 |Глобальн.|111       |
+------------------+---------+----------+

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|i                 |Лексич.  |2         |
|j                 |Лексич.  |2         |
|j                 |Глобальн.|-111      |
|i                 |Глобальн.|111       |
+------------------+---------+----------+

+------------------+---------+----------+
| Имя переменной   | Тип     | Значение |
+------------------+---------+----------+
|i                 |Лексич.  |3         |
|j                 |Лексич.  |3         |
|j                 |Глобальн.|-111      |
|i                 |Глобальн.|111       |
+------------------+---------+----------+

ok

==> ok

i

==> 111

j

==> -111

В заключение следует сказать о том, что если при вызове DO/DO* задать две или более одинаковые переменные цикла, то возбуждается состояние ошибки (переменная выделена красным):

(do ((i 1 (+ i 1))
     (i 2 (+ i 3)))
    ((> i 10) 'ok) (varlist)
    (printline i)
)

DO: повторение имени локальной переменной

==> ERRSTATE

Разработчик полагает, что вызовы DO/DO*, подобные приведенному выше, являются бессмысленными. Отчасти это подтверждается тем обстоятельством, что аналогичный вызов в двух известных системах XLISP и LispWorks работают по-разному. А именно вот так:

LispWorks:

(do ((i 1 (+ i 1))
     (i 1 (+ i 3)))
    ((> i 10) 'ok)
    (print i)
)

1
4
7
10
OK

XLISP:

(do ((i 1 (+ i 1))
     (i 1 (+ i 3)))
    ((> i 10) 'ok)
    (print i)
)

1
2
3
4
5
6
7
8
9
10
OK

Очевидно, что в LispWorks приращение переменной цикла выполняет вторая форма приращения, а в XLISP - первая. Поскольку непонятно, какая схема "правильная", то проще предотвратить задание двух одинаковых переменных цикла...

DOLIST

Функция DOLIST (типа FSUBR), отсутствующая у Лаврова и Силагадзе, реализована только в 13-м ядре HomeLisp. Функция организует "пробег" переменной по заданному списку. В современных языках программирования такую организацию цикла принято называть циклом "for each". Синтаксис вызова функции DOLIST :

(dolist (итератор (список) результат)
                (форма1)
                (форма2)
                …
                (формаn)
)

Функция последовательно присваивает переменной-итератору значения элементов списка, после чего выполняется тело цикла (форма1) - (формаn). После завершения функция DOLIST возвращает результат, заданный формой результат. Вот пример, поясняющий все сказанное:

(dolist (ite '(1 2 3 4 5 6 7 8 9 10) 'Ok)
        (printline (fact ite))
)

1
2
6
24
120
720
5040
40320
362880
3628800

==> ok

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

(dolist (ite '(1 2 3) 'ok)
   (dolist (ite '(4 5 6) 'ok)
     (printline ite)))

4
5
6
4
5
6
4
5
6

==> ok

Тот же пример с двумя различными переменными цикла дает совершенно предсказуемый результат:

(dolist (ite1 '(1 2 3) 'ok)
   (dolist (ite2 '(4 5 6) 'ok)
     (print ite1) (prints " ") (printline ite2)))

1 4
1 5
1 6
2 4
2 5
2 6
3 4
3 5
3 6

==> ok
DOTIMES

Функция DOTIMES (реализованная только в 13-м ядре и отсутствующая у Лаврова и Силагадзе) принадлежит к классу FSUBR. Эта функция обеспечивает выполнение блока кода заданное число раз. Синтаксис вызова функции следующий:

(dotimes (переменная_цикла
          число_повторений
          результат)

         (форма1)
         (форма2)
         …
         (формаn)
)

Вычисление происходит так. Переменная цикла последовательно получает значения от 0 до значения число_повторений-1. При каждом значении переменной цикла последовательно вычисляются формы (форма1) - (формаn). По завершении цикла вычисляется форма-результат и возвращается в качестве результата DOTIMES. Вот как это выглядит:

(dotimes
   (i 5 t)
      (print 'i=)
      (printline i)
)

i=0
i=1
i=2
i=3
i=4

==> T

Достаточно интересно то, что переменную цикла в теле цикла можно менять! Никакого зацикливания при этом не произойдет:

(dotimes
   (i 5 t)
     (setq i -1)
     (print 'i=)
     (printline i)
)

i=-1
i=-1
i=-1
i=-1
i=-1

==> T

Для досрочного выхода из цикла можно использовать функцию RETURN:

(dotimes (i 10 t)

         (print 'i=)
         (printline i)
         (if (= i 7) (return 'ok) nil)
)

i=0
i=1
i=2
i=3
i=4
i=5
i=6
i=7

==> ok

В этом случае в качестве результата возвращается результат вызова RETURN.

DOTPRINT

Функция DOTPRINT (отсутствующая у Лаврова и Силагадзе) позволяет напечатать внутреннюю (точечную) форму значения своего аргумента. Если аргумент - атом, поведение функции не отличается от поведения функции PRINT. Примеры вызова:

(dotprint '(a b c d))

(a . (b . (c . (d . NIL))))

==> (a b c d)

(dotprint '(a))

(a . NIL)

==> (a)
EQ

Функция EQ сравнивает два своих аргумента. Если один или оба аргумента функции EQ не являются атомами, функция вернет NIL. Если же оба аргумента функции - атомы, то функция сравнивает их. Если атомы совпадают, возвращается T, в противном случае возвращается NIL. Вот несколько примеров:

(eq 1 2)

==>  NIL

(eq 1 1)

==>  T

(eq 2 (+ 1 1))

==>  T

(eq 'a 'b)

==>  NIL

(eq _pi 3.14)

==>  NIL

(eq _pi 3.1415926535)

==>  T

(eq 'a 'a)

==>  T

(setq x 1)

==>  1

(eq x 1)

==>  T

Функция EQ принадлежит к классу SUBR, поэтому аргумент (+ 1 1) перед передачей в функцию EQ будет вычислен (и заменен атомом 2). В последнем примере x - переменная, имеющая значение 1. Вот почему в обоих случаях функция EQ дает T.
Следует обратить особое внимание на то, что функция EQ не пригодна для сравнения произвольных S-выражений на идентичность. Для этого служит функция EQUAL, принадлежащая библиотеке дополнительных функций. Разумеется, функция EQUAL использует функцию EQ.

EQL

Функция EQL, отсутствующая у Лаврова и Силагадзе, требует двух аргументов. Если оба аргумента представляют собой один и тот же объект (неважно, атом или список), то функция возвращает T. В противном случае функция возвращает Nil. Вот несколько примеров вызова функции EQL:

(eql 1 1)

==> T

(eql 1 2)

==> NIL

(eql '(1 2) '(1 2))

==> NIL

(equal '(1 2) '(1 2))

==> T

(setq z '(1 2))

==> (1 2)

(setq w (list z z))

==> ((1 2) (1 2))

(eql z (car w))

==> T

(eql z (cadr w))

==> T

Первые два примера воспринимаются вполне естественно. А вот третий пример может вызвать удивление. Действительно, ведь значения обоих аргументов EQL представлены одним и тем же списком - (1 2). Почему же функция показывает, что списки различаются? Дело в том, функция EQL не проверяет, состоят ли списки-аргументы из одних и тех же элементов (как приведеная ниже функция EQUAL). Функция EQL проверяет факт совпадения адресов значений первого и второго аргумента. Чтобы лучше уяснить происходящее, читателю предлагается рассмотреть внутреннюю структуру списка аргументов вызова (EQL '(1 2) '(1 2)).

Функция EQL сравнивает адреса списочных ячеек, залитых желтым и зеленым цветом на идентичность. Из рисунка видно, что хотя содержимое этих ячеек одинаково, но адреса их будут различаться (отюда и результат). А вот функция EQUAL (написанная на Лиспе) рекурсивно сравнивает именно содержимое, поэтому результат (EQUAL '(1 2) '(1 2)) оказывается равным T.

Совсем по-другому будет обстоять дело в том случае, когда сначала переменной z присваивается значение (1 2), затем переменной w присваивается значение выражения (LIST z z). Значение переменной z будет иметь такой вид:

Теперь (CAR w) и (CADR w) представляют собой одну и ту же ячейку. В этом случае функция EQL вернет T.

EVAL

Функция EVAL вычисляет свой аргумент (который может быть атомом или списком). Вычисление заключается в следующем:

Если аргументом является числовая и строковая константа {1, 2.5, &H15, "abc"}, имеющих в списке свойств индикаторы FIXED, FLOAT, BITS или STRING соответственно, значением является сама константа.

Если аргументом является атом с индикатором APVAL, то в качестве значения атома выдается значение его определяющего выражения. В частности, для встроенных констант (_Pi, _E, _Ver) значением являются 3.1415926535, 2.718281828 и "HomeLisp Вер. N.M.KK (Файфель Б.Л.)" соответственно. Полезно заглянуть в список объектов и найти там указанные выше константы. Будет отчетливо видно, что у этих констант в списке свойств имеется индикатор APVAL, а определяющее выражение представляет собой константу соответствующего типа.

Если аргументом является атом-переменная (она создается вызовом SETQ), в качестве значения EVAL выдается значение выражения, присвоенного атому.

Если аргументом является список, то его первый элемент рассматривается как имя функции, в все остальные элементы - как список аргументов этой функции. Отсюда следует, что не каждый список можно "вычислить" с помощью функции EVAL. Например, список (a b c) можно вычислить только если a - функция c двумя аргументами. Вот необходимые примеры:

(eval 1)

==> 1

(eval _Pi)

==> 3.1415926535

(eval a)

==> Символ "a" не имеет значения (не связан).

(setq a '(x y z))

==> (x y z)

(eval a)

==> Символ "x" не имеет значения (не связан).

(setq a ''(1 2 3))

==>  (QUOTE (1 2 3))

(eval a)

==>  (1 2 3)

Действие функции EVAL противоположно действию функции QUOTE. Это означает, что значение выражения (EVAL 'Нечто) равно выражению Нечто.

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

(setq myFunct 777)

==> 777

(defun myFunct (x y) (+ (* x x) (* y y)))

==> myFunct

(eval myFunct)

==> 777

(myFunct 3 4)

==> 25

Замечание. В 13-й редакции ядра функция EVAL имеет важную особенность: вычисление EVAL всегда происходит в пустом лексическом и текущем глобальном контекстах. Это соответствует спецификации Common Lisp. Вот пример, поясняющий сказанное:

(let ((a 111)) (varlist) (eval 'a))

+-----------------+---------+------------+
| Имя переменной  | Тип     | Значение   |
+-----------------+---------+------------+
|a                |Лексич.  |111         |
+-----------------+---------+------------+

Внутри LET: Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

(setq a 222)

==> 222
Создана глобальная переменная a
(let ((a 111)) (varlist) (eval 'a))

+-----------------+---------+------------+
| Имя переменной  | Тип     | Значение   |
+-----------------+---------+------------+
|a                |Лексич.  |111         |
|a                |Глобальн.|222         |
+-----------------+---------+------------+

==> 222

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

ERRORMESSAGE

Функция ERRORMESSAGE не требует аргументов. Она возвращает активное сообщение об ошибке в виде текстовой строки. Функцию имеет смысл использовать только совместно с функцией TRY в блоке, размещенном после атома EXCEPT. Если вызвать функцию ERRORMESSAGE в другом контексте (даже сразу же после вычисления, завершившегося ошибкой), то она вернет пустую строку. Вот примеры вызова ERRORMESSAGE:

(/ 1 0)

DIVIDE - деление на нуль
==> ERRSTATE

(errormessage)

==> ""

(try (/ 1 0)

     except

    (prog nil (say (strcat "Ошибка: " (errormessage))) (return 0))
)

==> 0

Здесь при вызове ERRORMESSAGE сразу после ошибки, выдается пустая строка. А при вызове ERRORMESSAGE посредством конструкции TRY-EXCEPT выводится диалоговое окно:

EXPLODE

Функция EXPLODE(отсутствующая у Лаврова и Силагадзе), принимает один обязательный аргумент, значение которого должно быть атомом. Функция возвращает одноуровневый список, состоящий из символов, состоавлющих имя атома. Атом как бы "врывается", разлетаясь на отдельные симвлы (отсюда и название).

Имеется единственная тонкость - некоторые символы, приведенные в расположенной ниже таблице, заменяются на особые атомы.

Символ в исходном атоме | Заменяющий символ
Двойная кавычка (") | &DQ
Точка (.) | &DT
Апостроф (') | &AP
Пробел | &BL
Обратный апостроф (`) | &BQ

Вот примеры вызова:

(explode 'explode)

==> (e x p L O d e)

(explode 1234)

==> (1 2 3 4)

(explode (fact 8))

==> (4 0 3 2 0)

(explode (/ 1 3))

==> (0 &DT 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3)

(explode "abc")

==> (&DQ a b c &DQ)

(explode 'abc)

==> (a b c)

Функция EXPLODE обеспечивает чисто лисповский подход к анализу символьной информации (без привлечения строковых функций).

EXPT (^)

Функция EXPT принимает два аргумента x и y и вычисляет выражение xy. При этом действуют следующие соглашения:

Если оба аргумента имеют тип FIXED и показатель степени (y) положителен, то результат будет иметь тип FIXED и соответствующий знак;

(^ 2 3)

==> 8

(^ 2 100)

==> 1267650600228229401496703205376

(^ -2 100)

==> 1267650600228229401496703205376

(^ -2 101)

==>  -2535301200456458802993406410752

Если оба аргумента имеют тип FIXED и показатель степени (y) отрицателен, то выдается сообщение "Попытка возведения числа типа FIXED в отрицательную степень" и функция не вычисляется. Это соглашение может показаться несколько странным, но любое целое число в целой отрицательной степени есть число рациональное (не FIXED), поэтому принято решение разрешить возведение в целую отрицательную степень только чисел типа FLOAT:

(^ 4 -2)

==> Попытка возведения числа FIXED в отрицательную степень

(^ 4.0 -2)

==>  0.0625

Положительное число типа FLOAT можно возвести в степень любого типа и любого знака. В обоих случаях результат будет иметь тип FLOAT:

(^ 625.0 0.5)

==>  25.0

(^ 625.0 -0.5)

==>  0.04

Отрицательное число типа FLOAT можно возвести только в целую положительную или отрицательную степень. В обоих случаях результат будет иметь тип FLOAT:

(^ -625.0 -4)

==>  6.5536E-12

(^ -625.0 4)

==>  152587890625.0

Попытка возвести отрицательное число в вещественную степень любого знака вызывает ошибку:

(^ -625 4.0)

==> Попытка возведения отрицательного числа в вещ. степень

(^ -625 -4.0)

==> Попытка возведения отрицательного числа в вещ. степень

(^ -625.0 -4.0)

==> Попытка возведения отрицательного числа в вещ. степень

И, разумеется, попытка возвести ноль в нулевую или отрицательную степень вызовет ошибку. Ненулевое число в нулевой степени равно единице.

FIX

Функция FIX принимает один аргумент. Если значение аргумента имеет тип FIXED, функция возвращает значение аргумента. Если же значение аргумента имеет тип FLOAT, то функция возвращает значение аргумента, округленное до ближайшего целого. В любом случае тип результата - всегда FIXED. Вот примеры, иллюстрирующие сказанное:

(FIX 67)

==> 67

(FIX -6.6)

==> -7

(FIX '(1 2 3))

Aргумент FIX - не атом
==> ERRSTATE

(FIX (+ 1 2.5))

==> 4
FIXEDP

Функция-предикат FIXEDP (у Лаврова и Силагадзе она называется FIXP) принимает один аргумент. Если значение аргумента имеет тип FIXED, функция возвращает T, в противном случае - Nil

(FIXEDP 67)

==> T

(FIXEDP (+ 5 9))

==> T

(FIXEDP '(1 2 3))

==> Nil

(FIXEDP (+ 1 2.5))

==> Nil

В последнем случае результат вычисления (+ 1 2.5) будет иметь тип FLOAT.

FIX2BIT

Функция FIX2BIT (отсутствующая у Лаврова и Силагадзе) принимает один аргумент, значение которого должно иметь тип FIXED. Если значение аргумента (как знаковое целое) может быть преобразовано к типу BITS (уместиться в 4 байта), то функция возвращает значение аргумента, как битовую константу. В противном случае выдается сообщение о невозможности преобразования.

(FIX2BIT 67)

==> &H43

(FIX2BIT -1)

==> &HFFFFFFFF

(FIX2BIT 123456789123456789)

==> Значение аргумента FIX2BIT за пределами допустимого диапазона

(FIX2BIT (+ 1 2 3 4.5))

==> Аргумент FIX2BIT - не имеет тип FIXED

В последнем случае результат вычисления (+ 1 2 3 4.5) будет иметь тип FLOAT; это вызывает сообщение об ошибке.

FIX2FLO

Функция FIX2FLO (отсутствующая у Лаврова и Силагадзе) принимает один аргумент, значение которого должно иметь тип FIXED. Если значение аргумента не превышает по модулю 10300, функция преобразует значение аргумента к типу FLOAT. Если же значение аргумента превышает по модулю 10300, то выдается сообщение об ошибке (переполнение). Поскольку в HomeLisp диапазон значений данных типа FIXED значительно шире, чем типа FLOAT, то в процессе преобразования происходит потеря большего количества значащих цифр (остется 15). Вот несколько примеров:

(FIX2FLO 67)

==> 67.0

(FIX2FLO (fact 100))

==> 9.33262154439442E+157

(FIX2FLO (fact 200))

Переполнение при вызове FIX2FLO
==>  ERRSTATE

В предпоследнем случае промежуточный результат вычисления (fact 100) имеет вид:

93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697 920827223758251185210916864000000000000000000000000

Это одно длинное число! После его преобразования в тип FLOAT получается результат 9.33262154439442E+157. А в последнем случае промежуточный результат вычисления (fact 200) слишком велик для типа FLOAT.

FLO2FIX

Функция FLO2FIX (отсутствующая у Лаврова и Силагадзе) принимает один аргумент, значение которого должно иметь тип FLOAT. Функция преобразует результат к типу FIXED. При этом дробное число округляется до ближайшего целого. Для чисел с большим десятичным порядком функция оставляет 15 значаших цифр значения аргумента и добавляет нужное количество нулей. Вот несколько примеров:

(FLO2FIX 67.6)

==> 68

(FLO2FIX (* 1000000.0 _pi))

==> 3141593

(FLO2FIX (FIX2FLO (FACT 100)))

==> 93326215443944200000000000000000000000000000000000
    00000000000000000000000000000000000000000000000000
    00000000000000000000000000000000000000000000000000
    00000000

В последнем примере отчетливо видно, как после цепочки преобразований FIXED -> FLOAT -> FIXED у результата остается только 15 верных значащих цифр.

FLO2STR

Функция FLO2STR (отсутствующая у Лаврова и Силагадзе) принимает один аргумент, значение которого должно иметь тип FLOAT. Функция преобразует результат к типу STRING. Делается это очень просто - символьное изображение числа заключается в двойные кавычки. Вот несколько примеров:

(flo2str _Pi)

==> "3.14159265358979323846"

(flo2str (sin _pi))

==> "1.22460635382238E-16"

(flo2str 1)

Аргумент FLO2STR не имеет тип FLOAT
==> ERRSTATE

(flo2str 1.0)

==> "1.0"

(flo2str 6.02E+23)

==> "6.02E+23"

Видно, что попытка дать на вход функции FLO2STR число типа FIXED, вызывает ошибку.

FLOATP

Функция-предикат FLOATP (отсутствующая у Лаврова и Силагадзе) принимает один аргумент. Если значение аргумента имеет тип FLOAT, функция возвращает T, в противном случае - Nil Вот набор примеров вызова FLOATP:

(FLOATP 67)

==> Nil

(FLOATP 67.0)

==> T

(FLOATP (+ 5 9))

==> Nil

(FLOATP (+ 5.0 9))

==> T

(FLOATP '(1 2 3))

==> Nil

В третьем случае результат вычисления (+ 5 9) будет иметь тип FIXED.

FORMAT

Функция FORMAT (отсутствующая у Лаврова и Силагадзе) принимает два аргумента. Первый аргумент должен быть числового типа (FIXED или FLOAT). Второй аргумент должен иметь строковый тип. Функция преобразует значение первого аргумента в строку символов, используя значение второго аргумента как строку формата. Строка формата может содержать следующие символы:

Символ формата | Вызываемое действие
0 | В выходной строке в соответствующем знакоместе будет выведена цифра. Если в этом знакоместе располагается пробел, то он заменяется нулем.
# | В выходной строке в соответствующем знакоместе будет выведена цифра или пробел (если цифры первого аргумента "не достают" до соответствующей позиции.
E+ | В этом месте выводится десятичный порядок, причем, если порядок положителен, то перед значением порядка ставится плюс.
E- | В этом месте выводится десятичный порядок, причем, если порядок положителен, то перед значением порядка плюс не ставится.

Вот набор примеров вызова FORMAT:

(format 6000.06 "######0.00")

==>  "6000.06"

(format 6000.06 "000000.00")

==>  "006000.06"

(format 6000.06 "0.00E+")

==>  "6.00E+3"

По сути дела функция FORMAT выполняет обратное действие по отношению к функциям STR2FIX и STR2FLO, но с дополнительными возможностями форматирования.

FUNCALL

Функционал FUNCALL (отсутствующий у Лаврова и Силагадзе) принимает произвольное количество аргументов. Первый аргумент должен быть атомом или лямбда-выражением. Последующие аргументы могут быть произвольными. Выполнение функции FUNCALL заключается в применении первого аргумента (как функции) к значениям последующих аргументов. Вызов FUNCALL вида:

(FUNCALL fn x1 x2 ... xn)

эквивалентен вызову:

(fn x1 x2 ... xn)

Вот примеры вызова FUNCALL:

(funcall 'times 1 2 3 4 5)

==>  120

(setq sum 'Plus)

==>  PLUS

(funcall sum 1 2 3 4)

==>  10

(funcall (car '(plus difference times divide)) 66 55)

==>  121

(funcall (cadr '(plus difference times divide)) 66 55)

==>  11

(funcall (caddr '(plus difference times divide)) 66 55)

==>  3630

(setq cons 'divide)

==>  DIVIDE

(funcall cons 2 3)

==>  0.666666666666667

(funcall 'cons 2 3)

==>  (2 . 3)

(funcall '(lambda (x y) (+ (* x x) (* y y))) 3 4)

==>  25

Следует обратить внимание на то, что, поскольку все аргументы функции FUNCALL вычисляются, то можно вводить синонимы имен функций. Так, в одном из приведенных выше примеров, атому CONS присвоено значение DIVIDE. Это имеет следствием то, что при подстановке первым аргументом переменной CONS выполняется деление. Если же первым аргументом этого же вызова подставить не переменную, а атом CONS, то происходит консолидация (образование точечной пары). Функция FUNCALL похожа на описанную выше функцию APPLY; отличие заключается в том, что аргументы, которые у функции FUNCALL обрабатываются "россыпью", у функции APPLY должны быть объединены в список.

FUNCTION

Конструкция FUNCTION предназначена для задания т.н. функционального аргумента. Следует отметить, что эта конструкция имеет существенные отличия в разных редакциях ядра HomeLisp. Далее сначала описывается применение FUNCTION в 11-й редакции ядра, а затем - применение FUNCTION в 13-й редакции ядра HomeLisp.
Конструкция в 11-й редакции ядра HomeLisp

Пусть читатель обратит внимание на термин "конструкция" - хотя слово FUNCTION употребляется в виде (FUNCTION имя_функционального_аргумента), эта запись лишь внешне напоминает лисповский вызов функции с одним аргументом. Цель описываемой конструкции состоит не в вычислении.

Чтобы понять назначение описываемой конструкции, рассмотрим функцию:

(defun Calc (f x)
            (f x)
)
Эта функция имеет два аргумента: первый аргумент - имя функции, второй - произвольный список. Она вызывает из своего тела функцию, имя которой задано первым аргументом, а список параметров - вторым.

Аргумент, c которым при вызове связывается функция, а не переменная, называется функциональным. А функция, использующая функциональные аргументы, называется функционалом. Таким образом, Calc - функционал.

Попробуем вызвать этот функционал так: (Calc Car '(1 2 3)). Казалось бы этот вызов должен преобразоваться в вызов (Car '(1 2 3)) и, естественно, дать в результате 1. Однако результат будет обескураживающим:

(calc car '(1 2 3))

==> Символ "CAR" не имеет значения (не связан).

Но, в сущности, все так и должно быть! Функция Calc имеет тип EXPR; ее аргументы вычисляются. Первый аргумент - атом Car. При вычислении он будет рассматриваться как переменная. Естественно, переменной Car нет в системе. Отсюда и приведенный выше удивительный результат. Вот для того, чтобы сообщить ядру языка, что тот или иной аргумент является не переменной, а именем функции (типа SUBR, FSUBR или EXPR) и служит специальный вызов (Function xxx). Таким образом, правильно вызывать функцию Calc следует так:

(calc (function car) '(1 2 3))

==> 1

(calc (function cdr) '(1 2 3))

==> (2 3)

Справедливости ради следует отметить, что функцию Calc можно вызвать и более простым способом (без использования конструкции Function). А именно, вот так:

(calc 'car '(1 2 3))

==> 1

(calc 'cdr '(1 2 3))

==> (2 3)

И действительно, второй метод (с использованием квотирования) проще. Но при его использовании может возникнуть проблема, если функция, которая связывается с функциональным аргументом, использует свободные переменные. (Так называются переменные, которые не входят в список параметров функции, но используются в теле функции). Дело в том, что функционал может сделать свободную переменную локальной (включив ее в список своих параметров или в список параметров функции PROG). Естествено, что в этом случае функционал вправе перед обращением к функциональному аргументу произвольным образом изменить эту переменную. Однако, в Лиспе принято, что при вычислении тела определяющего выражения функции, соответствующей функциональному аргументу, должны использоваться те значения свободных переменных, которыми они обладали перед началом вычисления функционала. Использование конструкции (FUNCTION ...) обеспечивает соблюдение этого условия. Рассмотрим примеры. Следуя книге Лаврова и Силагадзе, введем функцию:

(defun f1 (x) (cond ((null x) y) (t x)))

==> f1

Эта функция проверяет значение своего аргумента. Если аргумент равен Nil, функция возвращает значение свободной переменной y. В противном случае функция возвращает значение своего аргумента. Заведем переменую y:

(setq y -111)

==> -111

Переопределим функционал Calc следующим образом:

(defun calc (f x) (prog (y)
                        (setq y 888)
                        (print y)
                        (terpri)
                        (return (f x))
                   )
)

==> calc

Мы сделали переменную y локальной в теле функционала (связали переменную). При выполнении функционала переменной y сначала присваивается значение 888, затем оно выводится (для контроля), и, наконец, происходит обращение к функциональному аргументу f. Теперь вызовем функционал Calc:

(Calc (function f1) nil)

 888

 ==>  -111

Обратите внимание, что хотя значение переменной y в теле функционала равно 888, при вычислении f1 использовано старое значение (то, которое переменная y имела до входа в тело функционала).
Если же обратиться к функционалу без использования конструкции (FUNCTION ...) (с квотированием), то результат будет другой:

(Calc 'f1 nil)

 888

 ==>  888

Здесь использовано последнее (активное) значение переменной y. Чтобы понять, за счет чего при вызове (FUNCTION ...) используется неактивное значение переменной, имеет смысл заглянуть в дампы вычислений. При вычислении выражения (Calc (function f1) nil) перед обращением к функциональному аргументу ассоциативный список будет находиться в состоянии:

..................... Ассоциативный список:
.....................
.....................  6  ->  (x)
.....................  5  ->  (NIL 1)
.....................  4  ->  (y . 888)
.....................  3  ->  (x)
.....................  2  ->  (f FUNARG f1 1)
.....................  1  ->  (y . -111)

От пятого элемента переброшен "мостик" к первому элементу. При вычислении значения y ассоциативный список просматривается от последнего элемента к первому, до тех пор, пока не будет найден нужный элемент или не будет достигнуто начало списка. Если встречается "мостик" (элемент вида (NIL n), где n - номер следующего элемента), то поиск продолжается с элемента n. Из приведенного выше рисунка видно, что при поиске значения переменной y будет получено значение -111, т.к. четвертый элемент находится "под мостиком" и обходится.

Если же вызвать функционал Calc с квотированием - (Calc 'f1 nil), то перед обращением к функциональному аргументу ассоциативный список будет находиться в состоянии:

...................  Ассоциативный список:
...................
...................  5  ->  (x)
...................  4  ->  (y . 888)
...................  3  ->  (x)
...................  2  ->  (f . f1)
...................  1  ->  (y . -111)

"Мостика" нет, и переменная y получает активное значение 888. Теперь понятно, что если функциональный аргумент есть функция типа SUBR/FSUBR, то можно использовать квотирование при вызове (поскольку функция SUBR/FSUBR не используют переменных Лиспа). Аналогичным образом обстоит дело и в случае, если функциональный аргумент есть функция класса EXPR, которая не использует значения свободных переменных. Если же функциональный аргумент есть функция класса EXPR, которая использует значения свободных переменных, а функционал эти переменные локализует, то использование конструкции (FUNCTION ...) при вызове необходимо.

Если же функционал просто меняет значения уже существующих свободных переменных (без локализации их в функционале), то конструкция (FUNCTION ...) не поможет - при вычислении функционального аргумента будут взяты измененные значения переменных. Чтобы убедиться в этом, переопределим функционал Calc так:

(defun calc (f x) (prog (Nil)
                        (setq y 888)
                        (print y)
                        (terpri)
                        (return (f x))
                   )
)

==> calc

Это определение отличается от предыдущего тем, что переменная y не локализована в PROG-блоке (т.е. свободна). Теперь оба вызова (с конструкцией FUNCTION и с квотированием) дадут одинаковый результат:

(Calc (function f1) nil)

 888

 ==>  888

(Calc 'f1 nil)

 888

 ==>  888

Желающие могут заглянуть в дамп и убедиться, что поскольку в ассоциативном списке не создается локальная переменная y, то функционал и вызываемая функция используют свободную переменную (значение которой изменяется функционалом перед вызовом).

Функционал не может быть функцией класса FEXPR. Точнее, при обращении к функции типа FEXPR, не допустимо задание функционального аргумента в виде (FUNCTION F). Как следует из вышеизложенного, при обработке списка параметров функции типа EXPR появление конструкции (FUNCTION ff) сразу же порождает в ассоциативном списке запись вида (f FUNARG ff n), где n - фиксатор заполнения ассоциативного списка на момент задания функционального аргумента. После этого ассоциативный список может продолжать заполняться, но перед вычислением f возникнет "мостик" и будут взяты правильные значения свободных переменных.

Если же попытаться выполнить функцию класса FEXPR с функциональным аргументом (FUNCTION f), то конструкция (FUNCTION f) будет вычисляться уже в процессе обработки тела функции. При этом момент фиксации состояния ассоциативного списка будет упущен. В реализации HomeLisp при попытке выполнить функционал класса FEXPR c аргументом вида (FUNCTION F) выдается соответствующее сообщение.

Конструкция FUNCTION в 13-й редакции ядра HomeLisp

В 13-й редакции ядра HomeLisp конструкция FUNCTION порождает замыкание (определение функции с зафиксированными связями свободных переменных). Чтобы убедиться в этом, рассмотрим следующий пример:

(defun f (x) (* x y))

==> f

(f 7)

Assoc: Символ y не имеет значения (не связан).
==> ERRSTATE

(setq ff (let ((y 111)) (function f)))

==> (CLOSURE f ((y 111)))
Создана глобальная переменная ff
(funcall ff 7)

==> 777

Здесь создается функция f с параметром x и свободной переменной y. Если вызвать функцию f "просто так", то возникает ошибка, поскольку переменная y не имеет значения. Далее переменной ff присваивается замыкание функции f с зафиксированным значением свободной переменной y (равным 111). Последующий вызов ff дает предсказуемый результат.

Если вместо вызова (function f) употребить обычное квотирование, то результат окажется другим:

(setq gg (let ((y 111)) 'f))

==> f
Создана глобальная переменная gg
(funcall gg 7)

Assoc: Символ y не имеет значения (не связан).
==> ERRSTATE

Видно, что квотирование не обеспечивает сохранение значений свободных переменных. Если же завести глобальную переменную y, то вызов (funcall gg 7) завешится без ошибки, но при вычислении значения y будет взято "глобальное" значение:

(setq y 222)

==> 222
Создана глобальная переменная y
(funcall gg 7)

==> 1554

Если перевычислить выражение (funcall ff 7), то можно убедиться, что существование глобальной переменной y не окажет никакого влияния на результат - будет взято ранее зафиксированное значение:

(funcall ff 7)

==> 777

В этом и состоит суть конструкции FUNCTION в 13-й редакции ядра HomeLisp. Осталось сказать, что по аналогии с использованием апострофа вместо полной записи функци QUOTE, запись (FUNCTION any) можно записывать в виде #'any.

GC

Функция GC вызывает т.н. уборку мусора. Мусор - это области памяти, которые заняты недоступными ядру Лиспа или бесполезными структурами. Ниже будет объяснено, что имеется в виду под недоступностью и бесполезностью. Если читатель не хочет вникать в суть этих (не слишком простых) вопросов, он может не читать дальше этот раздел и просто принять к сведению, что функция GC освобождает память, занятую мусором и обычно вызывается автоматически, но может быть вызвана и вручную (для чего предусмотрен пункт меню и соответствующая кнопка). При автоматическом вызове сборки мусора пользователь может задать граничный процент увеличения количества занятых ячеек памяти, по достижении которого сборка мусора будет включаться автоматически. По умолчанию это 35% (число достаточно произвольное). Если после выполнения очередной команды пользователя количество атомов в системе увеличится на 35 или более процентов, то после завершения команды и вывода результата будет автоматически вызвана сборка мусора.

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

Любой список (или точечная пара) в конечном итоге состоит из атомов. Каждому атому соответствует особый объект - информационная ячейка атома. Свойства атома HomeLisp (важнейшим из которых является его имя) реализованы как свойства объекта (поля). Создать атом - значит создать его информационную ячейку и занести в ее поля необходимую информацию. Далее мы будем употреблять понятия "атом" и "информационная ячейка атома" как синонимы.

Одних информационных ячеек атомов недостаточно для создания списков произвольной структуры. Ведь список это не просто неупорядоченная совокупность атомов. Для задания порядка соединения атомов в списке служат т.н. списочные ячейки. Списочная ячейка - это тоже объект, важнейшими свойствами которого являются два указателя. По историческим причинам они называются a-указатель и d-указатель. Эти указатели могут ссылаться на информационные ячейки атомов или другие списочные ячейки. Таким образом, при создании списков списочные ячейки играют роль коннекторов.

Для изображения списков обычно используется графическая нотация, в которой списочные ячейки изображаться прямоугольниками, разделенными пополам. Будем считать, что левая половинка прямоугольника содержит a-указатель, а правая - d-указатель. Стрелка, исходящая из соответствующей клетки, будет указывать на тот или иной объект (атом или списочную ячейку). При этом информационные ячейки атомов не рисуются; стрелки просто указывают на атом. Так, точечная пара (A . B) графически изображается так:

Внутренее строение точечной пары

Список (A B C D) устроен так:

Внутренее строение простого списка

А вот как изображается список более сложной структуры ( (A B) C D):

Внутренее строение сложного списка

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

Так изображают список, содержащий одинаковые атомы.

более правильно изображать этот список так:

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

Информационные ячейки атомов объединены в коллекцию - список объектов. В списке объектов располагаются все атомы Лиспа. После запуска программы в списке объектов располагаются "стандартные" атомы - Nil (этот атом обычно бывает первым в списке), T, а также все индикаторы (APVAL, FIXED, FLOAT, STRING, BITS ), имена всех встроенных функций (PLUS, DIFFERENCE и т.д.), и ряд служебных атомов. Кроме того, в списке объектов располагаются имена Лисп-констант и ссылки на их значения. Информационная ячейка атома (в системе HomeLisp) устроена заметно сложнее списочной ячейки. Информационная ячейка включает поле имени атома, поле стандартных флагов, поле ссылки на определяющее выражение, поле ссылки на список дополнительных свойств (а также еще ряд дополнительных полей). Схематично информационную ячейку можно изобразить так:

Внутреннее строение информационной ячейки

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

Флаг | Битовый индикатор
SUBR | 0000000000000001
FSUBR | 0000000000000010
EXPR | 0000000000000100
FEXPR | 0000000000001000
APVAL | 0000000000010000
FIXED | 0000000000100000
BITS | 0000000001000000
STRING | 0000000010000000
FLOAT | 0000000100000000
MACRO | 0000001000000000
WINDOW | 0000010000000000
FILE | 0000100000000000
BLOB | 0001000000000000
COM | 0010000000000000
DIALOG | 0100000000000000
CONTROL | 1000000000000000

Шесть последних флагов используются для объектов, которые не являются лисповскими объектами, а служат для работы с графикой (GRAPHIC WINDOW), файлами (FILE), большими двоичными объектами (BLOB), ActiveX-компонентами (COM), диалогов (DIALOG) и элементов управления (CONTROL). Все эти средства HomeLisp описываются в соответствующих разделах руководства.

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

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

(defun квадрат (x) (* x x))

==> квадрат

При обработке S-выражения (defun квадрат (x) (* x x)) в списке объектов будет создан атом квадрат; у этого атома будет установлен флаг EXPR. Кроме того, будет построен список ((x)(TIMES x x)) и ссылка на этот список будет занесена в поле "определяющее выражение" информационной ячейки атома "квадрат". В памяти все это займет вот такую конфигурацию:

Внутреннее представление функции

Другой важнейшей внутренней структурой Лиспа помимо списка объектов является ассоциативный список, состоящий из пар (имя . значение). Имя - это имя переменной, создаваемой вызовами SET/SETQ. Значение - это произвольное S-выражение. Если, например, пользователь выполнил команду:

(Setq A '((B C) D E))

 ==>  ((B C) D E)

то в ассоциативный список будет добавлена пара (A . ((B C) D E)) (совпадающая, разумеется, со списком (A (B C) D E). В графической нотации это будет выглядеть так:

Вершина ассоциативного списка после вызова (Setq A '((B C) D E))

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

(setq a1 '(a b c))

 ==>  (a b c)

(setq a1 1)

 ==>  1

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

- в списке объектов образуется три новых атома - a, b и c;

- в области списочной памяти образуется список (a b c);

- в ассоциативном списке образуется пара a1 . ссылка на список (a b c)

После выполнения второй команды в списке объектов возникнет атом 1 (если его там не было), а пара a1 . ссылка на список (a b c) будет заменена парой a1 . ссылка на атом 1. А куда же делся список (a b c)? Он располагается на том же месте, что и после выполнения первой команды, но теперь недоступен, поскольку имя a1 связано с атомом 1.

Подобные структуры, которые занимают память, но недоступны, называют мусором.

Все сказанное можно проиллюстрировать рисунками:

Результат вычисления (setq a1 '(a b c))
Результат вычисления (setq a1 1). Список (a b c) стал мусором.

При длительной работе мусор может занять значительную область памяти или даже всю свободную память, что сделает дальнейшую работу невозможной.

Здесь нужно обратить внимание на одну особенность реализации системы HomeLisp: атомы и списочные ячейки являются объектами языка реализации (Visual Basic 6.0). Те, кто знаком с ООП-средствами языка Visual Basic, знают, что объект автоматически уничтожается, когда унитожается последняя ссылка на этот объект. Отсюда следует, что в приведенном выше случае, список (a b c d) будет автоматически уничтожен: уничтожение ссылки на первый элемент вызовет уничтожение первого элемента списка (ссылки на атом a и ссылки на подсписок (b c d)) вызовет уничтожение второго элемента (b) и т.д. Все это немного напоминает "эффект домино".

Так может быть, никаких специальных средств для утилизации мусора придумывать и не нужно? К сожалению, не все так просто... Использование функций rplaca и rplacd позволяет создавать циклические списки. Циклический список невозможно распечатать (печать займет бесконечное время) и его невозможно изобразить в скобочной или точечной нотации (по этой же причине). Однако его можно создать и нарисовать, используя стрелки. Ниже приводится внутренняя структура линейного и циклического списков:

Внутренее строение линейного списка
Внутренее строение циклического списка

Красной стрелка на обоих рисунках обозначена ссылка из ассоциативного списка.

Теперь ясно, что если уничтожается связь, обозначенная красной стрелкой, на верхнем рисунке, то будет унитожена первая списковая ячейка. Это приведет к удалению ссылки на вторую ячейку. Поскольку это единственная ссылка на вторую ячейку, то будет уничтожена и вторая ячейка и т.д. - "косточки домино" обрушатся полностью. Все списковые ячейки будут уничтожены. Следует обратить внимание на то, что уничтожатся только списковые ячейки (на рисунке это прямоугольники, разделенные пополам). Атомы a, b, c и d никуда не денутся; они останутся в списке объектов.

Если же уничтожается связь, обозначенная красной стрелкой, на нижнем рисунке, то вообще ничего не произойдет - ведь на первую списковую ячейку имеется две ссылки (красная стрелка и ссылка от последней списковой ячейки). Уничтожив первую ссылку мы ничего не добьемся - вторая ссылка не позволит первой ячейке самоуничтожться. "Косточки домино" устоят. При уничтожении ссылки на циклический список возникает мусор!

Эта ситуация демонстрируется на врезке, приводмой ниже:

(setq L '(a b c))

==>  (a b c)

(setq L nil)

==>  NIL

(gc)

Утилизировано ячеек: 0; атомов: 0.

(setq L '(a b c))

==>  (a b c)

(setq L (rplacd (cdr (cdr L)) L))

==>  (c a b c a b c a b c a b c a b c a b c a b c a b c a b
      c a b c a b c a b c a b c a b c a b c a b c a b c a b
      c a b c a b c a b c a b c a b c a b c a b c a b c a b
      c a b c a b c . ...)

(setq L nil)

==>  NIL

(gc)

Утилизировано ячеек: 3; атомов: 0.

Здесь создается линейный список (a b c), и присваивается переменой L. Потом переменной L присваивается значение Nil, и список (a b c) удаляется из памяти автоматически, - вызов функции GC не фиксирует наличие мусора. Затем линейный список (a b c) вновь создается и с помощью функции rplacd "закольцовывается". Видно, как HomeLisp пытается его распечатать. А затем переменной L присваивается значение Nil. Будь список линейным, он бы при этом уничтожился автоматически. Но, поскольку список циклический, он остается в памяти в виде мусора. И видно, что сборщик мусора нашел три списковые ячейки.

Мусор может образоваться и в списке объектов. Убедиться в этом совсем просто. Предположим, пользователь ввел команду и получил соответствующий ответ:

(+ 111 222 333 444)

 ==>  1110

Если атомы 111, 222, 333, 444 и 1110 (все или некоторые) не существовали, они будут созданы. Но ни одна из существующих списочных структур не ссылается на эти "свежесозданные" атомы. Таким образом, следует признать, что такие атомы хотя и доступны, но бесполезны - их можно удалять из списка объектов. Может показаться, что таких бесполезных атомов образуется немного. Вычислим, однако, число 100!. (для этого есть функция fact, входящая в библиотеку дополнительных функциий). Можно убедиться, что после вычисления образуется 194 новых атома! Это происходит потому, что вычисление арифметических выражений в Лиспе принципиально отличается от вычисления в традиционных языках (Бэйсик, Си, Паскаль).

Чтобы понять суть отличий, рассмотрим простейший оператор присвоения в традиционном языке программирования (например, в Бэйсике):

x=x+1

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

Аналогичный оператор в Лиспе запишется так:

(setq x (+ x 1))

Выполнение его пройдет в два этапа: сначала вычисляется (+ x 1); при этом результат будет равен атому 6. Этот атом занесется в список объектов. На втором этапе d-указатель ячейки ассоциативного списка, хранящей переменную x будет исправлен. Теперь этот указатель будет указывать на атом 6. Но атом 5 останется в списке объектов, - его оттуда никто не удалял. Если какое-либо из загруженных в память S-выражений ссылается на атом 5, то этот атом и не нужно удалять. А вот если ни одно из загруженных S-выражений не ссылается на атом 5, то этот атом становится мусором.

Возвращаясь к расчету 100!, легко видеть, что любой алгоритм рассчета факториала (рекурсивный или итерационный) будет вынужден вычислять 2, 3, ... 100, а также 2!, 3!, ... 100! и результаты всех этих промежуточных вычислений (атомы 1, 2, 3 ... и 2!, 3!, ... 100!) попадут в список объектов! Если какие-либо из этих атомов не нужны никаким S-выражениям, то их можно безболезненно удалить из системы.

Осталось сказать несколько слов о том, как реализована уборка мусора. Реализация функции GC в системе HomeLisp выполнена несколько проще, чем описано в книге Лаврова и Силагадзе. Выполнение проходит в два этапа: сначала отмечаются все списочные ячейки и информационные ячейки атомов, входящие в какое-либо S-выражение, находящееся в памяти. Доступ к заголовкам S-выражений осуществляется через ассоциативный список и список объектов. Затем просматриваются все созданные списочные ячейки и информационные ячейки атомов и удаляются те из них, которые непомечены. Для отметки списочных и информационных ячеек, предусмотрены соответствующие флаги. Упрощение по сравнению с реализацией Лаврова и Силагадзе заключается в том, что, во-первых, процедура разметки сделана рекурсивной, и, во-вторых освобождаемую память не требуется собирать в пул свободной памяти (это делает менеджер памяти языка реализации - Visual Basic).

GENSYM

Функция GENSYM (реализованная только в 13-й редакции ядра) создает символ (атом), которого еще гарантировано нет в текущем состоянии Лисп-машины. Функция принимает один необязательный параметр строкового типа (префикс имени символа). Если параметр задан ("PPPP"), то будет построен символ PPPPnnn; если параметр опущен, то строится символ Gnnn. В обоих случаях nnn - число, обеспечивающее уникальность созданного символа.

Все сказанное иллюстрируется примерами:

(gensym)

==> g1

(gensym "q")

==> q2

(gensym "qwerty")

==> qwerty3

(gensym)

==> g4

(setq g5 2)

==> 2
Создана глобальная переменная g5
(gensym)

==> g6

Логика работы функции GENSYM вполне понятна - когда пользователь создал глобальную переменную g5, следующий вызов функции GENSYM вернул символ g6.

GETD

Функция GETD принимает единственный аргумент - имя функции и возвращает тело этой функции. Если запрашивается тело функции, отсутствующей в системе, то функция GETD возвращает Nil:

(getd fact)

 ==>  (EXPR (x)(COND
                    ((MINUSP x) ERRSTATE)
                    ((EQ x 0) 1)
                    ((EQ x 1) 1)
                    (T (TIMES x (fact (DIFFERENCE x 1))))))

(getd чушь)

 ==>  NIL

На приведенной выше врезке тело функции fact для удобства отформатировано. Следует обратить внимание на то, что тело функции возвращается c флагом типа функции (EXPR/FEXPR/MACRO).

Главное назначение функции GETD состоит не в просмотре тел функций (проще это сделать, просматривая список объектов), а в модификации тела функции во время выполнения. Поскольку тело функции представляет собой S-выражение, то пользователь может модифицировать его во время выполнения, что позволяет фактически создать из одной функции другую. Чтобы занести модифицированное тело функции "на место" служит парная функция PUTD.

GETEL

Функция GETEL (отсутствующая у Лаврова и Силагадзе) принимает два аргумента. Значением первого аргумента должен быть непустой список, а значением второго - число типа FIXED. Функция возвращает элемент списка (заданого значением первого аргумента) с номером, равным значению второго аргумента. Если значение второго аргумента неположительно - возбуждается состояние ошибки с соответствующей диагностикой. Если значение второго аргумента выводит за пределы списка, заданого значением первого аргумента, то функция возвращает атом OutOfRange.

Все это продемонстрировано ниже:

(getel '(a b c) 2)

==> b

(getel '(a b (c d e f)) 3)

==> (c d e f)

(getel '(a b (c d e f)) 4)

==> OutOfRange

(getel '(a b (c d e f)) -6)

GETEL: Значение номера элемента неположительно
==> ERRSTATE

Функция GETEL легко может быть написана на Лиспе; реализация этой функции, как функции типа SUBR, выполнена исключительно для эффективности.

GEVAL

Функция GEVAL по своему действию полностью аналогична функции EVAL. Отличие заключается в вычислении значений переменных, входящих в S-выражение, являющееся аргументом. Если функция GEVAL вызвана в теле PROG-конструкции с локальными переменными, и какие-либо из локальных переменных входят в аргумент GEVAL, то в ассоциативном списке ищется одноименная переменная не входящая в список локальных. Если такая переменная найдена - используется ее значение. Если свобдная переменная не найдена - возбуждается состояние ошибки. Другими словами, функция GEVAL не видит локальных переменных (вычисляется в глобальном контексте). Более подробно поведение функции GEVAL рассматривается в разделе, посвященному контексту вычислений.

На приведенной ниже врезке показана разница в поведении функций EVAL и GEVAL:

(setq z 11111)

==> 11111

(defun f1 (n) (prog (z) (setq z 22222) (return (eval n))))

==> f1

(f1 'z)

==> 22222

(defun f2 (n) (prog (z) (setq z 22222) (return (geval n))))

==> f2

(f2 'z)

==> 11111

(unset 'z)

==> 0

(f1 'z)

==> 22222

(f2 'z)

Символ не имеет значения (не связан).
==> ERRSTATE

Здесь происходит следующее: заводится глобальная переменная z и две модельные функции - f1 и f2. В обоих функциях заводится локальная PROG-переменная z, которой присваивается значение, отличное от значения свободной переменной z. Функция f1 использует EVAL; ей недоступно значение глобальной переменной. Функция f2 использует GEVAL, поэтому ей доступно значение глобальной переменной.

Если уничтожить глобальную переменную вызовом UNSET, то поведение функции f1 не изменится. А вот функция f2 теперь не найдет глобальную переменную z; выполнение завершится ошибкой.

GO

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

GREATERP

Функция-предикат GREATERP принимает два аргумента числового типа. Если значение первого аргумента больше значения второго, то функция возвращает T, в противном случае функция возвращает Nil. Примеры вызова GREATERP приводятся ниже:

(greaterp 777.0 999.0)

 ==>  NIL

(greaterp 999.0 777.0)

 ==>  T

(greaterp 999 777)

 ==>  T

(greaterp 777 999)

 ==>  NIL
GREQP

Функция-предикат GREQP принимает два аргумента типа FIXED или FLOAT. Если значение первого аргумента больше или равно значению второго, то функция возвращает T, в противном случае функция возвращает Nil. Примеры вызова GREQP приводятся ниже:

(greqp 111 222)

 ==>  NIL

(greqp 222 111)

 ==>  T

(greqp 111 111)

 ==>  T

(greqp 222 111.0)

 ==>  T

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

GSET

Функция GSET ведет себя аналогично функции SET, но не действует на значения локальных PROG-переменных (работает в глобальном контексте). Если глобальной переменной, заданной значением аргумента GSET не существует, то переменная будет создана. При вызове вне PROG-конструкции GSET эквивалентна обычной SET. Другими словами, функция GSET вычисляется в глобальном контексте.

Более подробно поведение функции GSET рассматривается в разделе, посвященному контексту вычислений, а ниже приведен пример, поясняющий разницу в поведении функций SET и GSET:

(prog (z) (set 'z 1111) (print z) (return z))
1111
==> 1111

z

Символ z не имеет значения (не связан).
==> ERRSTATE

(prog (z) (gset 'z 1111) (print z) (return z))
NIL
==> NIL

z

==> 1111

В первом случае вызов (set 'z 1111) присваивает значение локальной переменной. После завершения PROG-конструкции попытка вычислить значение глобальной переменной z вызывает ошибку. Использование GSET вместо SET дает другой эффект: локальная переменная не получает значения, зато создается переменная z в глобальном контексте.

IF

Функция IF, реализованная только в 13-й редакции ядра HomeLisp и принадлежащая к классу FSUBR, принимает два обязательных параметра и один необязательный. Все параметры должны быть формами. Работа функции заключается в том, что сначала вычисляется значение первого параметра. Если результат отличен от Nil, то вычисляется второй параметр и функция возвращает результат вычисления. Если же значение первого параметра есть Nil, то вычисляется значение третьего параметра (при его наличии) и результат возвращается в качестве результата. Если же третий параметр не задан, а значение первого параметра есть Nil, то функция IF вернет Nil.

Легко видеть, что программная логика вычисления IF в точности соответствует программной логике классического оператора IF (в его краткой или полной форме)

     IF (...) Then Полная форма
         ...
     Else
         ...
     End if

     или

     IF (...) Then Краткая форма
        ...
     End if

без "синтаксического сахара" (Then, Else, End if). Естественно, вызовы функции IF могут быть вложены.

Примеры:

(let ((x 111)) (if (= 0 (% x 2)) 'Четное 'Нечетное))

==> Нечетное

(let ((x 222)) (if (= 0 (% x 2)) 'Четное 'Нечетное))

==> Четное

(let ((x 333)) (if (= 0 (% x 2)) 'Четное ))

==> NIL

В последнем случае использована краткая форма вызова IF.

IMPLODE

Функция IMPLODE, реализованная только в 13-й редакции ядра HomeLisp и принадлежащая к классу SUBR, принимает единственный обязательный параметр - список, состоящий из атомов. Функция "сжимает" все элементы списка и возвращает получившийся символ (атом).

Функция является в некотором смысле обратной по отношению к функции EXPLODE. Особые атомы (&DT,&DQ, &AP, &BL и &BQ) функция при "сжатии" заменяет на точку, двойные кавычки, апостроф, пробел и обратный апостроф соответственно.

Примеры:

(implode '(a b c))

==> abc

(implode '(a b &bl c))

IMPLODE: Попытка внести пробел в имя атома
==> ERRSTATE

(implode '(&dq a b &bl c &dq))

==> "ab c"

(implode '(a b &dt c))

==> ab.c

Видно, что функция IMPLODE не позволит занести пробел в имя атома (при условии, что этот атом - не строка).

LABELS

Конструкция LAMBDA позволяет задавать безымянные функции, которые затем можно использовать, напрмер, как функциональные аргументы. Однако у подобных функций есть один недостаток: такие функции не могут быть рекурсивными. В самом деле, рассмотрим простую рекрсивную функцию:

(defun f (x) (... (f ...) (f ...) ...))

Здесь видно, что функция f дважды вызывает сама себя. Если у функции нет имени, то непонятно, как синтаксически оформить вызов самой себя.

Отмеченную проблему решает функция LABELS, реализованная только в 13-й редакции ядра HomeLisp и принадлежащая к классу FSUBR. Эта функция обеспечивает создание вычисление локальных функций. Общая схема вызова LABELS такова:

(LABELS | ((опр_1) (опр_2) ... (опр_n))
 | (вызов_1) (вызов_2) ... (вызов_m))

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

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

Теперь можно рассмотреть примеры:

(labels ((fa (n) (if (= n 0) 1 (* n (fa (sub1 n)))))) (fa 6))

==> 720

(fa 5)

EVFUN: Не найдена функция FA
==> ERRSTATE

(defun f1 (x) (* x x x))

==> f1

(labels ((f1 (x) (* x x))
         (f2 (x) (+ x 4)))
        (labels ((f3 (x y) (* x y))) (+ (f1 6) (f2 7) (f3 11 22))))

==> 289

(f1 6)

==> 216

В первом примере задана локальная функция вычисления факториала. Следующий вызов (fa 5) сделан исключительно для того, чтобы убедиться, что локальная функция fa "живет" в теле LABELS.

Далее определяется обычная функция f, вычисляющая куб своего аргумента. Затем вычисляется сложная конструкция, состоящая из двух вложенных вызовов LABELS. В вызове верхнего уровня создаются две локальные функции f1 и f2. Во вложенном вызове создается локальная функция f3 и все три локальные функции используются при вычисления результата. Получается, как и положено, 6*6+7+4+11*22=289. Последующий вызов (f1 6), что функция f1, созданная ранее, нисколько не пострадала.

LAMBDA

Конструкция LAMBDA позволяет задавать т.н. безымянные функции. LAMBDA не является функцией - если посмотреть список свойств атома LAMBDA, то можно убедиться, что список включает единственный стандартный флаг APVAL (а, скажем, у CAR в списке свойств будет флаг SUBR). Безымянная функция представляет собой следующую конструкцию:

(LAMBDA список_формальных_параметров определяющее_выражение)

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

((LAMBDA список_формальных_параметров определяющее_выражение) список_фактических_параметров)

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

Вот несколько примеров:

((lambda (x) (* x x)) 6)

==> 36

((lambda (x) (* x x)) 6 7)

==> Список значений и список переменных имеют разную длину

((lambda (x) 1234) 7)

==> 1234

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

Остается сказать, что название LAMBDA ведет свою историю с т.н. лямбда-исчисления, придуманного логиком и математиком Черчем в 20-30-е годы прошлого века. Греческая буква лямбда уже давно является символом функциональных языков.

Все сказанное выше относится к обеим редакциям ядра HomeLisp. Конструкция LAMBDA в 13-й редакции ядра имеет важное отличие от таковой в в 11-й редакции. Если подать на вход Лисп-машины лямбда-выражение само по себе, то произойдет ошибка:

_ver

==> "HomeLisp Вер=1.11.2 Date=01.11.2010 Time=14:28:02"

(lambda (x y) (* x y))

Не найдена функция LAMBDA
==> ERRSTATE

Видно, что 11-я редакция ядра HomeLisp воспринимает лямбда-выражение, как вызов функции, а фунции LAMBDA в системе нет.

Совсем по-другому поведет себя предыдущий пример, если выполнить его в среде 13-го ядра HomeLisp:

_ver

==> "HomeLisp Вер=1.13.1 Date=20.05.2011 Time=19:48:02"

(lambda (x y) (* x y))

==> (CLOSURE (x y) ((* x y)) NIL)

Видно, что в среде 13-го ядра HomeLisp лямбда-выражение порождает новую конструкция - замыкание (CLOSURE). Замыкания, порождаемые лямбда-выражениями, сходны с замыканиям, порождаемыми вызовами DEFUN в теле LET. Ниже будут рассмотрены лексические замыкания, реализуемые с помощью LAMBDA-конструкции.

Лексические замыкания 2.

Замыкание - это агрегат, состоящий из определения функции и зафиксированных связей свободных переменных. Причем, доступ к зафиксированным связям свободных переменных должна иметь только эта функция (принцип инкапсуляции). Одним из способом создания замыкания является вызов DEFUN/SEXPR в теле LET-конструкции. Связи свободных переменных при этом сохраняются в специальном поле информационной ячейки атома-имени функции. Другой способ создания замыкания состоит в вычислении лямбда-выражения (без задания значений праметров). Результат вычисления лямбда-выражения в HomeLisp есть список, состоящий из:

специального атома CLOSURE;

списка параметров исходного лямбда-выражения;

тела исходного лямбда-выражения;

списка значений лексических переменных контекста вызова.

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

(let ((c 0)) (setq counter (lambda nil (setq c (add1 c)))))

==> (CLOSURE NIL ((SETQ c (ADD1 c))) ((c 0)))
Создана глобальная переменная counter

(funcall counter)

==> 1

(funcall counter)

==> 2

(funcall counter)

==> 3

counter

==> (CLOSURE NIL ((SETQ c (ADD1 c))) ((c 3)))

Из приведенного примера хорошо видно, как работает генератор. При следующем вызове (посредством FUNCALL) генератор вернет 4 и значение внутренней локальной переменной станет равно четырем.

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

(setq z '(CLOSURE NIL ((SETQ c (ADD1 c))) ((c 0))))

==> (CLOSURE NIL ((SETQ c (ADD1 c))) ((c 0)))
Создана глобальная переменная z

(funcall z)

Apply_: Недопустимое употребление CLOSURE
==> ERRSTATE

В HomeLisp встроен механизм защиты, запрещающий "конструировать" замыкания, как списки и модифицировать существующие замыкания. В других системах (например, в XLisp) пользователь вообще лишен возможности увидеть структуру замыкания. Поскольку HomeLisp предназначается для обучения Лиспу, разработчик принял решение "показывать" замыкания. Однако возможность конструировать замыкания, как списки или модифицировать существующие замыкания нарушило бы принцип инкапсуляции данных. Поэтому попытка модифицировать значение "замкнутой" переменной вызовет ошибку:

(let ((c 0)) (setq counter (lambda nil (setq c (add1 c)))))

==> (CLOSURE NIL ((SETQ c (ADD1 c))) ((c 0)))
Создана глобальная переменная counter

(funcall counter)

==> 1

(rplaca (cdar (cadddr counter)) 6)

RPLACA: попытка модификации защищенной структуры
==> ERRSTATE

(funcall counter)

==> 2

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

LEFTSHIFT

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

Примеры вызова LEFTSHIFT приводятся ниже:

(leftshift &h77 3)

 ==>  &H3B8

(leftshift &h77 -3)

 ==>  &HE

(leftshift &hEEEEEEEE -3)

 ==>  &HFDDDDDDD
LESSP

Функция-предикат LESSP принимает два аргумента числового типа и возвращает T если значение первого аргумента меньше значения второго. В противном случае возвращается Nil. LESSP допускает аргументы типа FIXED и FLOAT. Примеры вызова LESSP приводятся ниже:

(lessp 1 2)

 ==>  T

(lessp 2 1)

 ==>  NIL

(lessp 2.0 1)

 ==>  NIL

(lessp 2.0 1.0)

 ==>  NIL

(lessp 1.0 2.0)

 ==>  T
LET/LET*

Функции LET/LET* (типа FSUBR), реализованные только в 13-й редакции ядра, предназначены для управления лексическим контекстом (создания локальных переменных). Функции имеют следующую синтаксическую форму:

(LET/LET* (список переменых) форма1 форма2 … формаn)

Список переменных является списком, состоящим из духэлементных списков (имя форма_вычисления_значения). Функция вычисляется следующим образом: создается новый лексический контекст и в него заносятся переменые из списка переменных, значения которых вычисляются.

Если LET вычисляется в непустом лексическом контексте, переменные из списка переменных заносятся в начало нового контекста.

Далее последовательно вычисляются формы форма1, форма2 … формаn. Вычисления происходят в созданном лексическом контексте. Результат вычисления последней формы возвращается как результат LET, а лексический контекст вычислений восстанавливается.

Функция LET* отличается от LET только тем, что при вычислении значения очередной переменной из списка доступны значения уже ранее вычисленных переменных (последовательное вычисление). При вычислении LET переменные вычисляются "сразу все" - значения уже вычисленных переменных не доступны при вычислении последующих. В остальном функции не различаются.

Примеры:

(let ((a 111) (b 222)) (printline a) (printline b))

111
222

==> 222

a

Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

b

Assoc: Символ b не имеет значения (не связан).
==> ERRSTATE

Здесь созданы две локальные переменные, доступные в теле LET. Попытка получения этих переменных вне контекста их определения вызывает ошибку.

Еще пример:

(let ((a 111) (b (* a 222))) (printline a) (printline b))

Внутри LET: Символ a не имеет значения (не связан).

==> ERRSTATE

(let* ((a 111) (b (* a 222))) (printline a) (printline b))

111
24642

==> 24642

Первый вызов завершается ошибкой, поскольку при вычислении локальной переменной b требуется значение переменной a, а оно еще не доступно. Если же вместо LET использовать LET*, то значение переменной a будет доступно и вычисления завершаются предсказуемым результатом.

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

(setq a 333)

==> 333
Создана глобальная переменная a
(let ((a 111) (b (* a 222))) (printline a) (printline b))
111
73926

==> 73926

Понятно, почему это происходит: при вычислении формы (* a 222) будет использовано не значение локальной переменной (оно еще не доступно), а значение глобальной переменной a.

Вызовы LET могут быть вложенными. Вот пример вложенных вызовов LET:

(let ((a 111) (b 222))

     (printline (context))

     (let ((a 333) (c (* 2 a)))

          (printline (context))

          'ok
     )
)

(((b 222) (a 111)) NIL)

(((c 222) (a 333) (b 222) (a 111)) NIL)

==> ok

При первом вызове создается контекст из двух переменных a и b со значениями 111 и 222 соответственно. Этот контекст печатается. Далее вызывается внутренняя LET и к текщему контексту в начало добавляется переменная а со значением 333 и переменная с со значением, равным 2*a, но берется значение из внешего LET (оно доступно). После чего снова печатается текущий контекст. Кстати, все сказанное означает, что во внутреннем LET значением a будет 333, а не 111 (при вычислении значений контекст просматривается от начала к концу и сначала попадется значение 333.
Лексические замыкания 1.

Функции LET и LET* позволяют строить лексические замыкания, рассматриваемые ниже. Лексическое замыкание (или просто замыкание) это определение функции с зафиксированными связями свободных переменных. Замыкание можно построить, если вызвать функцию DEFUN или SEXPR в контексте, созданным вызовом LET:

(let ((c 0)) (defun next nil (setq c (add1 c))))

==> next

(next)

==> 1

(next)

==> 2

(context 'next)

==> ((c 2))

Здесь у функции next переменная c является свободной. Если бы определение функции next вводилось на верхнем уровне и отсутствовала бы глобальная переменная c, то функция была бесполезной: при вызове возникала бы ошибка при вычислении (add1 c). В приведенном же выше случае переменная c входит в лексический контекст, который запоминается в предназначенном для этого поле информационной ячейки атома next. Каждый вызов next увеличивает значение докальной переменной с на единицу и возвращает значение этой переменной. Подобная конструкция называется генератором. Созданный выше генератор позволяет только "шагать вперед" (нет возможности сбросить значение внутренней переменной). Чтобы обеспечить такую возможность, нужно определить функцию сброса в том же контексте, что и функцию next:

(let ((c 0))
     (defun next nil (setq c (add1 c)))
     (defun reset nil (setq c 0))
)

==> reset

(next)

==> 1

(next)

==> 2

(next)

==> 3

(reset)

==> 0

(next)

==> 1

(eql (context 'next) (context 'reset))

==> T

Здесь создано два замыкания - next и reset, причем, лексический контекст у них общий. Это приводит к тому, что обнуление переменной c при вызове reset заставляет next считать с нуля.

LIST

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

(list 1 2 3)

 ==>  (1 2 3)

(list 1 2 (3 4))

 ==>  Не найдена функция 3

(list 1 2 '(3 4))

 ==> (1 2 (3 4))

Во втором примере возникает ошибка, поскольку предпринимается попытка вычислить список (3 4). Чтобы предотвратить нежелательное вычисление в третьем примере применено квотирование.

LOGAND

Функция LOGAND принимает произвольное количество аргументов типа BITS и возвращает их логическое произведение. Логическое произведение - это битовая операция AND, которой соответствует таблица истиности:

X | Y | X AND Y
0 | 0 | 0
1 | 0 | 0
0 | 1 | 0
1 | 1 | 1

Примеры вызова LOGAND приводятся ниже:

(logand &H11 &H22)

 ==>  &H0

(logand &H7734 &HFF00)

 ==>  &H7700

(leftshift (logand &H7734 &HFF00) -8)

 ==> &H77

Функция применяется для выделения одного или более битов в шкале.

LOGOR

Функция LOGOR принимает произвольное количество аргументов типа BITS и возвращает их логическую сумму. Логическая сумма - это битовая операция OR, которой соответствует таблица истиности:

X | Y | X OR Y
0 | 0 | 0
1 | 0 | 1
0 | 1 | 1
1 | 1 | 1

Примеры вызова LOGOR приводятся ниже:

(logor &H11 &H22)

 ==>  &H33

(logor &H7734 &HFF00)

 ==>  &HFF34

(leftshift (logor &H7734 &HFF00) -8)

 ==> &HFF

Функция применяется для поднятия (установки) одного или более битов в шкале.

LOGXOR

Функция LOGXOR принимает произвольное количество аргументов типа BITS и возвращает их побитовое отрицание равнозначности - результат битовой операции XOR, которой соответствует таблица истиности:

X | Y | X XOR Y
0 | 0 | 0
1 | 0 | 1
0 | 1 | 1
1 | 1 | 0

Примеры вызова LOGXOR приводятся ниже:

(logxor &H11 &H22)

 ==>  &H33

(logxor &H7734 &HFF00)

 ==>  &H8834

(leftshift (logxor &H6634 &HFF00) 8)

 ==> &H993400
LOOP

Фунция LOOP (типа FSUBR) отсутствует в реализации Лаврова и Силагадзе. В HomeLisp она реализована только в 13-м ядре. Это простейшая функция организации циклов - она организует бесконечный цикл (тело которого может составлять из произвольного числа форм). Цикл прерывается вызовом RETURN. Контроль числа повторений и выход из цикла - обязанность программиста. Пример:

(let ((i 0))
     (loop (setq i (add1 i))
           (printline i)
           (if (>= i 23) (return 'ok) nil)
     )
)

1
2
3

…

22
23

==> ok
MACROEXPAND

Функция MACROEXPAND предназначена для генерация промежуточного S-выражения, являющегося результатом макрорасширения (первого этапа выполнения) функции типа MACRO. Такие функции вычисляются в два этапа: на первом этапе строится S-выражение, которое вычисляется на втором этапе (передается на вход EVAL). Функция MACROEXPAND, позволяющая увидеть это промежуточное S-выражение, ожидает на входе список переменной длины, первым элементом которого является атом - имя тестируемой функции типа MACRO, а последующими элементами - параметры тестируемой функции.
Если, например, есть макро-функция MF, требующая на входе список из двух элементов, то, чтобы увидеть промежуточное S-выражение, возникающее при вызове (MF a1 a2), следует задать (macroexpand MF a1 a2).

Рассмотрим (следуя замечательному двухтомнику Э. Хювенен и И. Сеппянен [6]), следующую макро-функцию:

(smacro setqq
              (lambda
                 (x y)
                 (list 'setq x (list 'quote y))
              )
)

Эта функция очень похожа на встроенную функцию setq, но, посколько аргументы макро не вычисляются, она предполагает обращение без квотирования второго аргумента: (setqq a (1 2 3)):

(setqq x (1 2 3))

 ==>  (1 2 3)

x

 ==>  (1 2 3)

Чтобы увидеть результат первого этапа вычисления, следует вызвать функцию macroexpand:

(macroexpand setqq x (1 2 3))

 ==>  (SETQ x (QUOTE (1 2 3)))
MAX

Функция MAX в HomeLisp принимает произвольное число аргументов типа FIXED или FLOAT и возвращает максимальное значение из значений всех аргументов. Если все аргументы имеют тип FIXED, то и результат будет иметь тип FIXED. Если среди аргументов есть хоть один типа FLOAT, то и результат будет иметь тип FLOAT. Все сказанное можно проиллюстрировать следующим:

(max 1 2 34 88 4 5)

 ==>  88

(proplist (max 1 2 34 88 4 5))

 ==>  (FIXED)

(max 1 2 34 88 4 5.0)

 ==>  88.0

(proplist (max 1 2 34 88 4 5.0))

 ==>  (FLOAT)
MIN

Функция MIN в HomeLisp принимает произвольное число аргументов типа FIXED или FLOAT и возвращает минимальное значение из значений всех аргументов. Если все аргументы имеют тип FIXED, то и результат будет иметь тип FIXED. Если среди аргументов есть хоть один типа FLOAT, то и результат будет иметь тип FLOAT. Все сказанное можно проиллюстрировать следующим:

(min 88 2 34 1 4 5)

 ==>  1

(proplist (min 88 2 34 1 4 5))

 ==>  (FIXED)

(min 88 2 34 1 4 5.0)

 ==>  1.0

(proplist (min 88 2 34 1 4 5.0))

 ==>  (FLOAT)
MINUS

Функция MINUS ожидает ровно один аргумент типа FIXED или FLOAT и возвращает значение этого аргумента с противоположным знаком. Тип результата совпадает с типом аргумента. Примеры приводятся ниже.

(minus 6)

 ==>  -6

(proplist (minus 6))

 ==>  (FIXED)

(minus 5.0)

 ==>  -5.0

(proplist (minus 5.0))

 ==>  (FLOAT)
MINUSP

Функция-предикат MINUSP ожидает ровно один аргумент типа FIXED или FLOAT и возвращает T, если значение этого аргумента отрицательно. В противном случае функция возвращает Nil. Примеры приводятся ниже.

(minusp 6)

 ==>  Nil

(minusp 0)

 ==>  Nil

(minusp -5)

 ==>  T

(minusp -5.0)

 ==>  T
NLAMBDA

Конструкция NLAMBDA (реализованная только в 13-й редакции ядра), порождает анонимную функцию, но отличается от конструкции LAMBDA ) двумя моментами:

аргументы NLAMBDA не вычисляются;

NLAMBDA не порождает замыканий.

((nlambda (x y) (list x y)) (1 2 3) (4 5 6))

==> ((1 2 3) (4 5 6))

(nlambda (x y) (list x y))

EVFUN: Не найдена функция NLAMBDA
==> ERRSTATE

(lambda (x y) (list x y))

==> (CLOSURE (x y) ((LIST x y)) NIL)

Первый запуск показыает, что NLAMBDA задает анонимную функцию с невычисляемыми при вызове аргументами. Второй вызов, показывает, что NLAMBDA не порождает замыканий (в отличие от LAMBDA).

NOT(NULL)

Две функции NOT и NULL выполняют одно и то же: если их единственный аргумент есть Nil, функции возвращают T; в противном случае возвращается Nil:

(not 6)

 ==>  Nil

(not Nil)

 ==>  T

(Null 'a)

 ==>  Nil

(Null Nil)

 ==>  T
NUMBERP

Функция-предикат NUMBERP проверяет, является значение ее единственного аргумента числом (FIXED или FLOAT); в этом случае возвращается T. В противном случае возвращается Nil:

(numberp 111)

 ==>  T

(numberp 'a)

 ==>  Nil

(numberp (+ 6 7))

 ==>  T

В последнем примере сначала будет вычислено выражение (+ 6 7) и получен результат 13. Далее будет вычисляться выражение (numberp 13), значение которого, естественно равно T.

ONEP

Функция-предикат ONEP проверяет, является значение ее единственного аргумента типа FIXED атомом 1; в этом случае возвращается T. В противном случае возвращается Nil. При попытке обратиться к предикату с аргументом, имеющим тип, отличный от FIXED, возникает ошибка. Вот примеры:

(onep 1)

 ==>  T

(onep 2)

 ==>  Nil

(onep 1.0)

 ==>  Аргумент ONEP не число FIXED

(onep (+ 1 0))

 ==>  T

В последнем примере сначала будет вычислено выражение (+ 1 0) и получен результат 1. Далее будет вычисляться выражение (onep 1), значение которого равно T.

OR

Функция OR класса FSUBR принимает произвольное число аргументов. В процессе вычисления первый же же аргумент, имеющий значение, отличное от Nil, вызывает прекращение вычисления, и возвращается в качестве значения функции. Если значение всех аргументов функции равно Nil, то функция принимает значение Nil. Примеры:

(OR 1 2 3)

==> 1

(or (CAR '(1)) (/ 3 0))

==> 1

Обратите внимание, во втором примере аргумент (/ 3 0) даже не вычислялся (в противном случае возникла бы ошибка "деление на нуль"). Это происходит вследствие того, что OR - функция класса FSUBR. Ее аргументы не вычисляются (точнее вычисление происходит не перед, а после вызова в теле функции от первого к последнему). Второй аргумент после вычисления дает 1, и вычисление прекращается. Если заменить CAR на CDR, то возникнет ошибка "деление на нуль".

PARSE

Функция PARSE, отсутствующая у Лаврова и Силагадзе, принимает единственный аргумент типа STRING, содержащий алгебраическое выражение в естественной нотации, а возвращет это же выражение, разбитое на лексемы (токены). Круглые скобки становятся структурными скобками результирующего S-выражения. Лексемами являются символические имена переменых, числовые константы и знаки алгебраических операций:

Знак операция | Выполняемое действие
+ (plus) | Алгебраическое сложение
- (difference) | Алгебраическое вычитание
* (times) | Умножение
/ (divide) | Деление с плавающей точкой
\ (quotient) | Целочисленное деление
% (quotient) | Остаток от деления
^ (expt) | Возведение в степень
> (greaterp) | Cравнение (больше)
< (lessp) | Cравнение (меньше)
>= (greqp) | Cравнение (больше или равно)
<= (leeqp) | Cравнение (меньше или равно)
<> (neqp) | Cравнение (не равно)

Как видно из приведенной выше таблицы, в качестве знаков операций можно применять лисповские обозначения функций (plus, difference, times и т.д.). Однако в этом случае знак операции должен быть окружен пробелами (в противном случае произойдет "склеивание" знака операции с операндами).

Рассмотрим примеры:

(parse "1+3")

 ==>  (1 + 3)

(parse "(a+b)/(a-b)")

 ==>  ((a + b) / (a - b))

(parse "(a+b)\(a-b)")

 ==>  ((a + b) \ (a - b))

(parse "(a+b)%(a-b)")

 ==>  ((a + b) % (a - b))

(parse "a - c ^ 2")

 ==>  (a - c ^ 2)

(parse "(a plus (d difference e)")

Не сбалансированы скобки во входном выражении
==> ERRSTATE

(parse "(a plus (d difference e))")

==> ((a PLUS (d DIFFERENCE e)))

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

Если приглядеться к приведенным выше примерам, то легко заметить, что функция PARSE просто преобразует алгебраическое выражение из строковой формы в инфиксную форму. Инфиксной называется форма записи алгебраического выражения, при которой знак операции располагается между операндами. Однако результат функции PARSE нельзя непосредственно передать интерпретатору Лиспа. Дело в том, что Лисп использует не инфиксную, а префиксную форму (знак операции предшествует операндам). Пусть читатель сравнит инфиксную форму (после PARSE) (a + b) и правильную Лисповскую префиксную форму (+ a b). Для перевода выражения из инфиксной формы в префиксную служит функция inf2pref, входящая в библиотеку дополнительных функций.

PLUS(+)

Функция PLUS,(вместо PLUS можно писать +) вычисляет сумму значений своих аргументов, которых может быть произвольное количество. Тип аргументов должен быть FIXED или FLOAT. При этом, если среди аргументов есть хотя бы один аргумент типа FLOAT, то и результат будет иметь тип FLOAT. Рассмотрим примеры:

(plus 1 3 5 7)

 ==>  16

(+ 1 2 3)

 ==>  6

(+ 1 2.0 3)

 ==>  6.0
PRINT/PRINTLINE

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

(print 'a)

a
 ==> a

(print (+ 7 8))

15
 ==>  15

(print "проба пера")

"проба пера"
 ==>  "проба пера"

Пусть читатель обратит внимание на то, что помимо печати значения своего аргумента, функция возвращает его в качестве результата (каждая функция должна возвращать какой-нибудь результат!)

Функция PRINTLINE (отсутствующая у Лаврова и Силагадзе) полностью аналогична функции PRINT, описанной выше, но вслед за выводимим значением выполняется перевод строки.

PRINTS/PRINTSLINE

Функции PRINTS и PRINTSLINE отличаются от функций PRINT и PRINTLINE только тем, что строки печатаются без объемлющих кавычек.

(print "a")

"a"

==> "a"

(prints "a")

a

==> "a"

(prints 56)

56

==>  56

Следует обратить внимание, что хотя функции PRINTS/PRINTSLINE печатают значение своего аргумента-строки без кавычек, результат функции все равно печатается с кавычками. Если аргумент функции PRINTS/PRINTSLINE не имеет типа STRING, действие функций полностью аналогично действию функции PRINT/PRINTLINE, описанных выше.

PROG

Функция PROG введена в Лисп с целью обеспечить в декларативном функциональном языке возможности императивного программирования. Общая схема конструкции PROG такова:

(PROG (список локальных переменных)
 [метка] Оператор
 [метка] Оператор
 [метка] Оператор
 ...     ...
)

Список локальных переменных представляет собой одноуровневый список атомов (например, (a b c)). Метка - это произвольный (но уникальный в пределах PROG-конструкции атом. То, что на приведенной выше врезке, слово "метка" заключено в кавычки, означает, что метка - объект необязательный. Она может отсутствовать. Оператор - это вызов произвольной функции, в том числе и функции PROG.

Опишем, как происходит вычисление PROG.

Вычисление начинается с того, что для каждого атома из списка локальных переменных создается одноименная переменная со значением nil. Если, например, список локальных переменных имеет вид (a b c), то при входе функцию PROG выполняются действия, эквивалентные последовательному вычислению S-выражений:

(setq a nil)

(setq b nil)

(setq c nil)

Далее начинается последовательное вычисление операторов (каждый оператор - это S-выражение). Метки при этом просто пропускаются. Среди операторов, однако, может встретиться S-выражение (GO метка). Фактически GO - это функция с единственным аргументом - атомом, вызов которой допустим только в теле PROG (попытка вызвать GO вне тела PROG приводит к ошибке). Смысл вызова GO заключается в том, что следующим после GO выполняющимся оператором, будет оператор, имеющий метку, которая является аргументом GO. Будем называть это действие "переходом на метку". Легко видеть, что GO - это аналог оператора GoTo, который, несмотря на яростные нападки "правоверных", имеется во всех нормальных языках программирования. Разумеется, переход на несуществующую метку вызывает ошибку.

Имеется еще одна функция, вызов которой допустим только в теле PROG - это функция RETURN. Функция имеет единственный аргумент, значение которого возвращается в качестве результата вычисления функции PROG. Можно сказать, что вызов RETURN приводит к прекращению вычисления PROG и возврату в качестве результата PROG значения своего аргумента.

Если вся последовательность операторов вычислена (***), то вычисление PROG завершается, а в качестве результата возвращается Nil.

Рассмотрим примеры. Выведем значения последовательных нечетных чисел и подсчитаем их сумму.

(defun sumOdd (x) (prog (i s n)

                        (setq s 0)

                        (setq i 1)

                   L    (setq n (- (* 2 i) 1))

                        (setq s (+ s n))

                        (print n)

                        (terpri)

                        (setq i (+ i 1))

                        (cond ((greaterp i x) (return s)))

                        (go L)

                    )
)

(sumOdd 6)

1
3
5
7
9
11

 ==>  36

Читатель легко увидит в этой программе цикл, образованный с помощью COND и GO.

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

(prog nil

      A (print 'Начало)

        (terpri)

        (prog nil

               (setq x 1)

            A  (print x)

               (terpri)

               (setq x (+ 1 x))

               (cond ((lessp x 10) (go A)))
        )
)

Начало
 1
 2
 3
 4
 5
 6
 7
 8
 9

 ==>  NIL

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

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

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

(setq x 111)

==> 111

(prog (x)

      (setq x -777)
      (print 'x=)
      (print x)
      (terpri)
)

x=-777

==> Nil

x

==> 111

В приведенном выше примере переменная x связанная. Хотя ее значение в теле PROG изменено, при выходе значение x остается прежним. Ну, а если связанная переменная до вызова PROG не существовала, то после вызова она и не появится:

проба

==> Символ "проба" не имеет значения (не связан).

(prog (проба)

      (setq проба -777)
      (print 'проба=)
      (print проба)
      (terpri)
)

проба=-777

==>  NIL

проба

==> Символ "проба" не имеет значения (не связан).

Переменная проба до вызова PROG не существовала. Эта переменная связана в функции PROG. В процессе выполнения функции PROG переменная проба получает значение, но после выхода из PROG переменная уничтожается (атом проба снова не имеет значения).

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

(setq x 111)

==> 111

(prog nil

      (setq x -777)
      (print 'x=)
      (print x)
      (terpri)
)

x=-777

==> Nil

x

==> -777

Поведение связанных переменных функции PROG во многом напоминает поведение параметров функций. Параметры функции тоже являются связанными переменными. Единственная разница заключается в том, что параметры при входе получают значения соответствующих S-выражений из списка фактических параметров, а переменные, связанные в теле функции PROG, получают значения Nil.

PROG1/PROG2/PROGN

Функции PROG1, PROG2 и PROGN отсутствуют у Лаврова и Силагадзе и реализованы только в 13-й редакции ядра. Все функции имеют тип FSUBR. Назначение этих функций состоит в последовательном вычислении своих аргументов. В качестве результата функция PROG1 возращает значение первого аргумента, функция PROG2 - первого аргумента, а функция PROGN - последнего аргумента.

Описываемая тройка функций отличается от функции PROG двумя моментами:

функции PROG1, PROG2 и PROGN не создают локальных переменных;

в теле функций PROG1, PROG2 и PROGN нельзя использовать функции GO и RETURN (функции задают последовательное вычисление аргументов).

Вот примеры, поясняющие все сказанное:

(let ((a 0) (b 0) (c 0)) (prog1 (setq a 111) (setq b 222) (setq c 333)))

==> 111

(let ((a 0) (b 0) (c 0)) (prog2 (setq a 111) (setq b 222) (setq c 333)))

==> 222

(let ((a 0) (b 0) (c 0)) (progn (setq a 111) (setq b 222) (setq c 333)))

==> 333

Красным выделены формы, значения которых возвращет соответствующая функция.

PROPLIST

Функция PROPLIST выводит выводит список свойств значения своего единственного аргумента. Значение аргумента функции должно быть атомом. Если это не так, возникает ошибка.

Здесь следует сказать несколько слов о том, что представляет собой список свойств. Понятие свойства является первичным. Наличие свойства означает наличие некоторого признака (или группы признаков). Например свойство "быть числом" или "быть целым числом". Свойства можно разделить на встроеные и создаваемые пользователем.
Свойства быть числом, битовой константой, функцией типа EXPR и т.д - встроенные. Когда в системе создается атом-константа (числовая или строковая) соответствующее свойство назначается ему автоматически. Когда создается переменная (вызовом SET/SETQ), то в случае, когда значение переменной есть числовая или строковая константа, соответствующее свойство назначается и переменой. То же самое имеет место, когда создается константа (вызовом CSETQ); в дополнение к типу константы добавляется еще свойство "быть константой (иметь определяющее выражение)".
Каждое свойство идентифицируется (обозначается) атомом - ведь в Лиспе нет ничего, кроме атомов и списков! Каждому встроенному свойству соответствует атом: BITS, FIXED, FLOAT, STRING, EXPR, FEXPR, MACRO, SUBR, FSUBR, APVAL. Если свойство описывает единственный признак двоичного толка (есть/нет), то какое свойство называется флагом. Если же свойство не является двоичным, то для его полного описания недостаточно одного атома: нужно обозначить наличие свойства и его значение. В этом случае атом-идентификатор свойства называется индикатором, а значением свойства может быть произвольное S-выражение.

Теперь можно объяснить, что представляет собой список свойств. Это список, существующий для каждого атома, и включающий все флаги, индикаторы и значения свойств. Пользователь может создавать свои (произвольные) свойства. Чтобы создать свойство, сначало нужно создать атом-идентификатор. А затем добавить его в список свойств (если это флаг) или добавить в список свойств этот атом-индикатор и S-выражение, задающее значение свойства. Разумеется, список свойств может быть и пустым. Функции, предназначенные для манипулирования индикаторами и флагами приводятся в описании библиотеки дополнительных функций. Из всех функций, обслуживающих список свойств, только две (PROPLIST и SPROPL) являются функциями типа SUBR; остальные реализованы на HomeLisp-е

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

Примеры работы с функцией PROPLIST:

(proplist 'h)

==> Nil

(proplist (+ 1 2))

==> (FIXED)

(setq h 111)

==> 111

(proplist 'h)

==> (FIXED)

(spropl 'h '(i1 (1 2 3) i2 (4 5 6) i3 (a b c d)))

==> (i1 (1 2 3) i2 (4 5 6) i3 (a b c d))

(proplist 'h)

==> (FIXED i1 (1 2 3) i2 (4 5 6) i3 (a b c d))

(getprop 'h 'i1)

==>  (1 2 3)

(getprop 'h 'i2)

==>  (4 5 6)

(getprop 'h 'i3)

==>  (a b c d)

(getprop 'h 'i4)

==>  NIL

(getprop 'h FIXED)

==>  FIXED

У только что созданного атома h список свойств пуст. Результат вычисления (+ 1 2) равен атому 3; его список свойств состоит из единственного атома FIXED. После присвоения атому h значения 111 (типа FIXED), атом h наследует этот флаг. Далее, с помощью встроенной функции SPROPL атому h в дополнение к стандартному флагу устаналиваются три нестандартных свойства: i1 со значением (1 2 3); i2 со значением (4 5 6); и, наконец, i3 со значением (a b c d). Вызов PROPLIST позволяет увидеть весь список свойств - сначала в нем идут стандартные флаги, а затем пользовательские свойства. Опросить наличие отдельных свойств можно с помощью функции GETPROP. Видно, что свойства i1, i2, i3 у атома h имеются, а свойство i4 - нет. Кстати, отсюда следует, что нежелательно использовать значение свойства, равное Nil: нет способа отличить такое значение свойства от отсутствия свойства (в обоих случаях GETPROP возвращает Nil).

PUTD

Функция PUTD (отсутствующая у Лаврова и Силагадзе) позволяет заменить тело функции новым. Функция PUTD не является необходимой, - всегда можно переопределить функцию, вызвав SEXPR/SFEXPR/DEFUN. Разработчик ввел функцию PUTD, руководствуясь только соображениями симметрии (есть GETD - пусть будет и PUTD!).

Как пользоваться функцией PUTD? Функция принадлежит классу FSUBR - ее аргументы не вычисляются. Первым аргументом функции должен быть атом-имя функции, тело которой будет изменено. Вторым параметром должен быть список, состоящий из двух списков: первый - новый список аргументов функции, второй - новое тело функции.

Примеры работы с функцией PUTD:

(defun f1 (x) (+ x x x))

==> f1

(f1 5 5 5)

==> 15

(proplist f1)

==> (EXPR)

(putd f1 ((x) (* x x x)))

==> ((x)(TIMES x x x))

(f1 5)

==> 125

(proplist f1)

==> (EXPR)

(putd f1 '((x y) (* x y)))

==>  ((x y)(TIMES x y))

(f1 6 7)

==>  42

(proplist f1)

==> (EXPR)

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

PUTEL

Функция PUTEL (отсутствующая у Лаврова и Силагадзе) позволяет заменить элемент списка, заданного значением первого аргумента другим значением. Функция ожидает три параметра: модифицируемый список, номер заменяемого элемента и новое значение заменяемого элемента. При успешном завершении функция возвращает модифицированный список. Если значение номера элемента превышает длину верхнего уровня списка, функция возвращает атом OutOfRange.

Вот примеры работы с функцией PUTEL:

(putEl '(a b c) 2 '(1 2 3))

==> (a (1 2 3) c)

(putEl '(a b c) 4 'd)

==> OutOfRange

(putEl '(a b c) -3 'f)

PUTEL: Значение номера элемента неположительно
==> ERRSTATE

(setq z '(1 2 3))

==> (1 2 3)

(putEl z 2 -2)

==> (1 -2 3)

Z

==> (1 -2 3)

Следует отметить, что функция PUTEL имеет побочный эффект - она модифицирует список заданный значением первого аргумента (именно поэтому переменная z при последнем вызове меняет свое значение).

QUOTE

Функция QUOTE - классическая (и, вероятно, самая странная для понимания новичка) функция Лиспа. Эта функция на самом деле не делает ничего, она просто блокирует вычисление своего единственного аргумента (который может быть любым мыслимым S-выражением). Название функции, вероятно, происходит от английского to quote - брать в кавычки. В классическом Лиспе вызов QUOTE записывается в виде (QUOTE нечто). В современных версиях Лиспа (и в HomeLisp) эта запись полностью эквивалентна записи 'нечто. Не следует путать апостроф с двойной кавычкой. Примеры работы с функцией QUOTE:

(setq a 777)

==> 777

a

==> 777

(quote a)

==> a

'a

==> a

''a

==> (QUOTE a)

(eval a)

==> 777

(eval 'a)

==> 777

(eval ''a)

==> a

Первый вызов создает переменную a с числовым значением 777. Ввод S-выражения, состоящего из атома a вызывает его вычисление. При этом печатается значение атома a, равное 777. Вызов (quote a), или, что то же самое, 'a приводит к блокировке вычисления значения a. Результат равен атому a. Следующий пример показывает двойное квотирование - запись ''a полностью эквивалентна записи (quote (quote a)). Результат, естественно, равен (quote a). В общем, функция QUOTE возвращает свой аргумент "как есть" без вычисления.

Для читателей, знакомых с языком Бэйсик, совершенно ясно, что операторы Print a и Print "a" означают разные действия: первый оператор печатает значение переменной a, а второй - букву (символ) a. Примерно так же действует в Лиспе описываемая функция QUOTE.

Два следующих примера могут вызвать недоумение. Выше отмечалось, что функция EVAL противоположна функции QUOTE - их действия "взаимно уничтожаются". Понятно, что результат вычисления (eval a) должен быть равен значению атома a (т.е. 777). Но результат вычисления (eval 'a) должен быть равен a, не так ли? А результат снова равен 777... Дело здесь в том, что когда пользователь вводит S-выражение, оно всегда передается на вход EVAL (при вводе (eval 'a) реально вычисляется (eval (eval 'a))); таким образом количество вызовов EVAL оказывается на единицу больше, и это приводит к вычислению значения a.

В последнем примере количество EVAL и QUOTE сбалансировано, что приводит к ожидаемому результату. Поучительно запросить дамп и сравнить протоколы вычисления последнего и предпоследнего примеров. Ниже показан протокол вычисления выражения: (eval a)

.EVAL вход:  (EVAL (QUOTE a))
..APPLY вход: A1= EVAL A2= ((QUOTE a))
...EVFUN вход: A1= EVAL A2= ((QUOTE a))
...
... Ассоциативный список:
...
...  1  ->  (a . 777)
...
....LIST вход: A= ((QUOTE a))
.....EVAL вход:  (QUOTE a)
......APPLY вход: A1= QUOTE A2= (a)
.......EVFUN вход: A1= QUOTE A2= (a)
.......
....... Ассоциативный список:
.......
.......  1  ->  (a . 777)
.......
.......EVFUN выход:  a
......APPLY выход:  a
.....EVAL выход:  a
....LIST выход: Рез= (a)
....EVAL вход:  a
.....EVATOM вход:  a
.....EVATOM выход:  777
....EVAL выход:  777
...EVFUN выход:  777
..APPLY выход:  777
.EVAL выход:  777

А вот протокол вычисления выражения (eval ''a):

.EVAL вход:  (EVAL (QUOTE (QUOTE a)))
..APPLY вход: A1= EVAL A2= ((QUOTE (QUOTE a)))
...EVFUN вход: A1= EVAL A2= ((QUOTE (QUOTE a)))
...
... Ассоциативный список:
...
...  1  ->  (a . 777)
...
....LIST вход: A= ((QUOTE (QUOTE a)))
.....EVAL вход:  (QUOTE (QUOTE a))
......APPLY вход: A1= QUOTE A2= ((QUOTE a))
.......EVFUN вход: A1= QUOTE A2= ((QUOTE a))
.......
....... Ассоциативный список:
.......
.......  1  ->  (a . 777)
.......
.......EVFUN выход:  (QUOTE a)
......APPLY выход:  (QUOTE a)
.....EVAL выход:  (QUOTE a)
....LIST выход: Рез= ((QUOTE a))
....EVAL вход:  (QUOTE a)
.....APPLY вход: A1= QUOTE A2= (a)
......EVFUN вход: A1= QUOTE A2= (a)
......
...... Ассоциативный список:
......
......  1  ->  (a . 777)
......
......EVFUN выход:  a
.....APPLY выход:  a
....EVAL выход:  a
...EVFUN выход:  a
..APPLY выход:  a
.EVAL выход:  a

Пусть читатель сравнит первые строки этих протоколов (выделены красным цветом). Все станет ясным.

QUOTIENT (\)

Функция QUOTIENT вычисляет частное целочисленного деления значения первого аргумента на значение второго. Для краткости выражение (QUOTIENT a b) может быть записано в виде (\ a b). Значения обоих аргументов функции QUOTIENT должны иметь тип FIXED. Примеры работы с функцией QUOTIENT:

(\ 666 222)

==> 3

(\ 6 3.0)

==> Второй аргумент QUOTIENT - не число FIXED

(\ 6 7 8)

==> Более 2-х аргументов у QUOTIENT

(\ 7 5)

==>  1

(\ 5 7)

==>  0
REMAINDER (%)

Функция REMAINDER вычисляет остаток от целочисленного деления значения первого аргумента на значение второго. Для краткости выражение (REMAINDER a b) может быть записано в виде (% a b). Значения обоих аргументов функции REMAINDER должны иметь тип FIXED. Примеры работы с функцией REMAINDER:

(% 666 333)

==>  0

(% 6 4)

==>  2

(% 6 4.0)

==> Второй аргумент REMAINDER - не число FIXED

(% 7 9)

==>  7

(% 7 9 7)

==> Более 2-х аргументов у REMAINDER
RETURN

Функция RETURN реализована в обеих редакциях ядра HomeLisp, но область использования этой функции несколько различается.

В 11-й редакции ядра функцию RETURN допустимо вызывать только находясь в теле PROG. Она возвращает значение своего единственного аргумента в качестве результата вычисления функции PROG. Если имеет место вложенность функций PROG, то вызов RETURN обеспечивает возврат только на один уровень вверх (подобно тому, как в Бэйсике оператор EXIT FOR обеспечивает выход только из самого внутреннего цикла.

Следует отметить, что если вычисление тела PROG завершается вызовом функции, отличной от RETURN, то независимо от результата вычисления этой функции, PROG вернет значение Nil. Примеры работы с функцией RETURN:

(PROG (i s)    (setq s 0.00)
               (setq i 0)
             A (setq s (+ s i))
               (setq i (+ i 1))
               (cond ((lessp i 101) (go A))
                     ( T            (return s))
               )
)

==>  5050.0

(PROG (i s)    (setq s 0.00)
               (setq i 0)
             A (setq s (+ s i))
               (setq i (+ i 1))
               (cond ((lessp i 101) (go A))
                     (T             (print s))
               )
)

5050.0

==> Nil

(PROG (i s)    (setq s 0.00)
               (setq i 0)
             A (setq s (+ s i))
               (setq i (+ i 1))
               (cond ((lessp i 101) (go A))
                     (T   (prog nil
                                    (terpri)
                                    (print 's=)
                                    (print s)
                                    (terpri)
                                    (return s)
                           )
                     )
               )
)

s=5050.0

==>  NIL

(PROG (i s)    (setq s 0.00)
               (setq i 0)
             A (setq s (+ s i))
               (setq i (+ i 1))
               (cond ((lessp i 101) (go A))
                     (T   (return (prog nil
                                           (terpri)
                                           (print 's=)
                                           (print s)
                                           (terpri)
                                           (return s)
                                   )
                          )
                     )
               )
)

s=5050.0

==>  5050.0

В качестве примера приводится "лобовое" решение задачи, которую когда-то задал учитель арифметики маленькому Карлу Гауссу - подсчитать сумму чисел от 1 до 100. Первый пример дает правильный результат: 5050 (для возврата использована функция RETURN). Во втором примере результат, накопленный в связанной переменной s, печатается из тела PROG. Однако, поскольку вызов RETURN отсутствует, функция PROG возвращает Nil. Третий пример интереснее: хотя в нем использован вызов RETURN, функция, тем не менее, снова возвращает Nil. Причина заключается в том, что вызов RETURN выполняется во вложенном вызове PROG (функция PROG введена для последовательного выполнения вызовов TERPRI, PRINT, TERPRI). Выход из внешней функции PROG осуществляется без RETURN, - результат снова Nil. В последнем примере, значение s, возвращемое из функции печати, тут же "подхватывается" функцией RETURN и обеспечивается корректный возврат значения.

Замечание. В 13-й редакции ядра функция RETURN может применяться не только для выхода из PROG-конструкции, но и для досрочного выхода из циклов, организованных с помощью функций LOOP и DOTIMES. Вот пример решения задачи подсчета суммы чисел от 1 до 100 с помощью функции LOOP:

(let ((s 0) (i 1))
     (loop
          (setq s (+ s i))
          (setq i (add1 i))
          (if (> i 100) (return s))))

==> 5050
RIGHTSHIFT

Функция RIGHTSHIFT принимает два аргумента. Первый аргумент должен иметь тип BITS, т.е. быть 32-х битовой логической шкалой. Второй аргумент должен иметь тип FIXED. Функция возвращает значение первого аргумента, сдвинутое вправо или влево на количество разрядов, равное модулю значения второго аргумента. При этом, если значение второго аргумента положительно, сдвиг происходит вправо. При отрицательном значении второго аргумента сдвиг происходит влево. При сдвиге влево освободившиеся разряды заполняются нулями. При сдвиге вправо в освободившиеся разряды заносится самый старший разряд. Функция является "парной" к функции LEFTSHIFT и добавлена в HomeLisp исключительно по соображениям симметрии.

Примеры вызова RIGHTSHIFT приводятся ниже:

(rightshift &H77 3)

==> &HE

(rightshift &H77 -3)

==> &H3B8

(rightshift &HEEEEEEEE -3)

==> &H77777770

(rightshift &HEEEEEEEE 3)

==> &HFDDDDDDD
RPLACA

Функция RPLACA принимает два аргумента. Первым аргументом должен быть список или точечная пара (атом недопустим). Вторым аргументом может быть произвольное S-выражение. Функция RPLACA заменяет значение a-указателя значения своего первого аргумента значением второго аргумента. То, что получилось из значения первого аргумента, возвращается в качестве значения RPLACA. Функция RPLACA обладает побочным эффектом - после ее вызова могут измениться значения сразу многих выражений, которые ссылаются на модифицированную ячейку. Но главное коварство функции RPLACA заключается в том, что с ее помощью можно создавать циклические списочные структуры. Такие структуры можно нарисовать, но невозможно распечатать.

Рассмотрим сначала элементарные примеры вызова функции RPLACA:

(setq w '(a . b))

==> (a . b)

(rplaca w 1)

==> (1 . b)

(setq v '((a) b c))

==> ((a) b c)

(rplaca v 'z)

==> (z b c)

Видно, что действительно происходит замена первого элемента списка или точечной пары. Казалось бы подобного эффекта можно достичь с помощью функции CONS. В частности, Замену атома a точеной пары (a . b) на единицу можно выполнить вот таким вызовом:

(setq w (cons 1 (cdr w)))

Однако вызов с использованием CONS строит новую списочную структуру, а rplaca модифицирует существующую.

Посмотрим, как с помощью rplaca можны создавать циклические списки:

(setq w '(a))

==> (a)

(setq w (rplaca w w)

==> (((((((((((((((((((((((...)))))))))))))))))))))))

Здесь сначала создан список из одного элемента a. В графическом виде этот список можно представить так:

Внутреннее представление списка (a)

После выполнения вызова (setq w (rplaca w w) a-указатель списочной ячейки будет указывать на саму списочную ячейку:

Внутреннее представление того же списка после модификации a-указателя.

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

RPLACD

Функция RPLACD принимает два аргумента. Первым аргументом должен быть список или точечная пара (атом недопустим). Вторым аргументом может быть произвольное S-выражение. Функция RPLACD заменяет значение d-указателя значения своего первого аргумента значением второго аргумента. То, что получилось из значения первого аргумента, возвращается в качестве значения RPLACD. Функция RPLACD (как и RPLACA) обладает побочным эффектом - после ее вызова могут измениться значения сразу многих выражений, которые ссылаются на модифицированную ячейку. С помощью функции RPLACD тоже можно создавать циклические списочные структуры.

Рассмотрим сначала элементарные примеры вызова функции RPLACD:

(setq w '(a . b))

==> (a . b)

(rplacd w 1)

==> (a . 1)

(setq v '((a) b c))

==> ((a) b c)

(rplaca v 'z)

==> ((a). z)

Пусть читатель обратит внимание на то, что во втором примере список превращается в точечную пару. Почему так происходит станет ясно, если представить значение переменной v в точечной записи, - оно будет иметь вид: ((a . Nil) . (B . (C . Nil))). Левая часть этой точечной пары есть (a . Nil), а правая - (B . (C . Nil)). После выполнения вызова (rplaca v 'z) произойдет замена правой части точечной пары на атом z, результат будет следующий: ((a . Nil) . z). А это есть в точности ((a) . z). Аналогичный результат можно было бы получить, сделав вызов:

(setq v (cons (car v) 'z))

Однако вызов с использованием CONS строит новую списочную структуру, а rplacd (как и rplaca) модифицирует существующую.

Посмотрим, как с помощью rplacd можны создавать циклические списки:

(setq w '(a b))

==> (a b)

(setq w (rplacd w w)

==> (a a a a a a a a a a a a a a a a a a a a a a ...)

Здесь сначала создан список из двух элементов (a b). В графическом виде этот список можно представить так:

Внутреннее представление списка (a b)

После выполнения вызова (setq w (rplacd w w) d-указатель списочной ячейки будет указывать на саму списочную ячейку:

Внутреннее представление того же списка после модификации d-указателя.

При печати такой структуры после открывающей скобки должно быть напечатано бесконечное количество букв a, а затем - закрывающая скобка. В действительности HomeLisp-у через какое-то время "надоедает" печатать одну и ту же букву, и выводится просто многоточие и закрывающая скобка.

SET

Функция SET принимает два аргумента. Значением первого аргумента должен быть атом. Функция создает переменную с именем, равным значению первого аргумента и значением, равным значению второго. Вторым аргументом может быть произвольное S-выражение. Как отмечалось выше в разделе, посвященном функции GC, переменные и их значения хранятся в ассоциативном списке системы. Функция SET в качестве результата возвращает значение второго аргумента.

Рассмотрим примеры вызова функции SET:

(set a b)

==> Символ "a" не имеет значения (не связан).

(set 'a b)

==> Символ "b" не имеет значения (не связан).

(set 'a 'b)

==> b

a

==> b

(set 'c '(1 2 3))

==> (1 2 3)

(set c 777)

==> У функции SET первый аргумент - не атом

(set 'c 777)

==> 777

(set 'xx 'yy)

==>  yy

xx

==>  yy

(set xx 888)

==>  888

xx

==>  yy

yy

==>  888

Первые два вызова приводят к ошибке, поскольку Функция SET принадлежит классу SUBR и оба ее аргумента вычисляются. В этом случае атомы a и b будут рассматриваться как переменные. Если хотя бы одна из них не имеет значения, возникает ошибка. Далее создается переменная c, со значением (1 2 3), попытка изменить значение этой переменной с помощью вызова (set c 777) снова порождает ошибку, поскольку после вычисления аргументов фактически выполняется некорректная команда (set (1 2 3) 777). Чтобы изменить значение переменной c, этот аргумент следует квотировать (как в следующем примере).

Следующая команда присваивает переменной xx значение yy. Казалось бы, вызов (set xx 888) присваивает переменной xx новое значение 888 (и результат возвращается верный!) Однако, на самом деле у переменной xx значение остается прежним, а меняется значение переменной yy. Причина та же, что и выше: после вычисления значения первого аргумента, фактически выполняемый вызов имеет вид: (set yy 888), и значение меняется у переменной yy (в чем читатель может убедиться).

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

Следует отметить, что пользователь может изменить значение переменной, но не может явно ее уничтожить. Исключения составляют связанные переменные функции PROG, но они уничтожаются (при выходе из PROG) не пользователем, а системой.

Кроме того, если атом является константой (имеет в списке свойств флаг APVAL, то такой атом не может быть именем переменной. Попытка использовать его в качестве первого аргумента SET вызовет ошибку:

(csetq a 777)

==> 777

a

==> 777

(proplist 'a)

==> (APVAL FIXED)

(set 'a 888)

==> Попытка превратить константу a в переменную
SETQ

Функция SETQ, как и функция SET, принимает два аргумента. Первый аргумент должен быть атомом. Функция создает переменную с именем, равным первому аргументу (а не его значению, как SET) и значением, равным значению второго. Вторым аргументом может быть произвольное S-выражение. Как отмечалось выше в разделе, посвященном функции GC, переменные и их значения хранятся в ассоциативном списке системы. Отличие функции SETQ от функции SET состоит в том, первый аргумент не вычисляется (и, следовательно, не нуждается в квотировании). Сама функция принадлежит к классу FSUBR. Функция SETQ в качестве результата возвращает значение второго аргумента.

Рассмотрим примеры вызова функции SETQ:

(setq a b)

==> Символ "b" не имеет значения (не связан).

(set a b)

==> b

a

==> b

(setq c '(1 2 3))

==> (1 2 3)

(setq c 777)

==> 777

(setq xx 'yy)

==>  yy

xx

==>  yy

(setq xx 888)

==>  888

xx

==>  888

Сравнивая приведенные примеры с аналогичными примерами для функции SET, следует признать, что использование SETQ более естественно (поэтому SETQ и применяется чаще).

Следует отметить, что пользователь может изменить значение переменной, но не может явно ее уничтожить. Исключения составляют связанные переменные функции PROG, но они уничтожаются (при выходе из PROG) не пользователем, а системой.

Кроме того, если атом является константой (имеет в списке свойств флаг APVAL, то такой атом не может быть именем переменной. Попытка использовать его в качестве первого аргумента SETQ вызовет ошибку:

(csetq a 777)

==> 777

a

==> 777

(proplist 'a)

==> (APVAL FIXED)

(setq a 888)

==> Попытка превратить константу a в переменную
SEXPR

Функция SEXPR предназначена для создания функций пользователя типа EXPR. Тип EXPR означает, что все аргументы перед передачей в тело функции вычисляются (к каждому неявно применяется функция EVAL).

Более просто можно создать функцию с помощью вызова DEFUN. Функция SEXPR включена в реализацию языка HomeLisp, поскольку для нее имеется "пара" - функция SFEXPR - средство создания функций типа FEXPR. DEFUN позволяет создавать только EXPR-функции.

Общий вид конструкции SEXPR приведен ниже:

(sexpr Имя_Функции

       (lambda

           (...Список_параметров...)

           ...Тело функции...

       )
)

Именем функции может быть только атом. Список параметров - это список атомов - формальных параметров создаваемой функции. Другое название списка параметров - список связанных переменных. Тело функции представляет собой S-выражение.

Замечание. Для 11-й редакции ядра тело функции может содержать только одно S-выражение. Для 13-й редакции ядра тело функции может состоять из произвольного числа S-выражений, которые вычисляются последовательно. Результатом функции будет результат вычисления последнего S-выражения.

Эти S-выражения могут использовать переменные из списка параметров или любые другие переменные (которые называются свободными). Функция SEXPR связывает имя функции с лямбда выражением. Если вызов SEXPR корректен, то пользователю становится доступной новая функция с соответствующим именем, списком параметров и определяющим выражением. Вызов SEXPR в этом случае возвращает имя вновь созданной функции.

(sexpr sum3 (lambda (x y z) (+ x y z)))

==> sum3

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

Далее вычисляется тело функции (определяющее выражение). Результат вычисления возвращается, как результат функции. Затем происходит уничтожение связанных переменных в ассоциативном списке. Для того, чтобы проследить детали вычисления функции, полезно включить режим дампирования. Вызовем, например, определенную ранее функцию sum3 следующим образом: (sum3 11 22 33). В дамп-файл попадет следующая информация:

.EVAL вход:  (sum3 11 22 33)
..APPLY вход: A1= sum3 A2= (11 22 33)
...EVFUN вход: A1= sum3 A2= (11 22 33)
....LIST вход: A= (11 22 33)
.....EVAL вход:  11
......EVATOM вход:  11
......EVATOM выход:  11
.....EVAL выход:  11
.....EVAL вход:  22
......EVATOM вход:  22
......EVATOM выход:  22
.....EVAL выход:  22
.....EVAL вход:  33
......EVATOM вход:  33
......EVATOM выход:  33
.....EVAL выход:  33
....LIST выход: Рез= (11 22 33)
....EVLAM вход: A1= ((x y z)(PLUS x y z)) A2= (11 22 33)
.....PAIRLIS вход: ptrAasso=0 A1= (x y z) A2= (11 22 33)
.....
..... Ассоциативный список:
.....
.....  3  ->  (z . 33)
.....  2  ->  (y . 22)
.....  1  ->  (x . 11)
.....
.....PAIRLIS выход: ptrAsso=3
.....EVAL вход:  (PLUS x y z)
......APPLY вход: A1= PLUS A2= (x y z)
.......EVFUN вход: A1= PLUS A2= (x y z)
.......
....... Ассоциативный список:
.......
.......  3  ->  (z . 33)
.......  2  ->  (y . 22)
.......  1  ->  (x . 11)
.......
........LIST вход: A= (x y z)
.........EVAL вход:  x
..........EVATOM вход:  x
..........EVATOM выход:  11
.........EVAL выход:  11
.........EVAL вход:  y
..........EVATOM вход:  y
..........EVATOM выход:  22
.........EVAL выход:  22
.........EVAL вход:  z
..........EVATOM вход:  z
..........EVATOM выход:  33
.........EVAL выход:  33
........LIST выход: Рез= (11 22 33)
........PLUS вход: A= (11 22 33)
........PLUS Пром. Рез=11
........PLUS Пром. Рез=33
........PLUS Пром. Рез=66
........PLUS выход: Рез= 66
.......EVFUN выход:  66
......APPLY выход:  66
.....EVAL выход:  66
....EVLAM выход: Рез= 66
...EVFUN выход:  66
..APPLY выход:  66
.EVAL выход:  66

Пусть читатель обратит внимание, что в ассоциативном списке образовались три связаные в теле функции sum3 переменные. Можно убедиться, что после возврата этих переменных в ассоциативном списке не будет.

(sum3 11 22 33)

==> 66

x

==> Символ "x" не имеет значения (не связан).

Если к моменту вызова функции уже имеется свободная переменная, одноименная со связанной переменной из списка параметров, то в теле функции будет доступно только значение связанной переменной. После возврата из функции снова будет доступно прежнее значение свободной переменной. Вот развернутый пример на эту тему:

(sexpr sum3 (lambda (x y z)
                            (Prog nil

                                  (print 'x=)
                                  (print x)
                                  (terpri)
                                  (print 'y=)
                                  (print y)
                                  (terpri)
                                  (print 'z=)
                                  (print z)
                                  (terpri)

                                  (return (+ x y z))
                              )
              )

)

==> sum3

(setq x 111)

==> 111

(setq y 222)

==> 222

(setq z 333)

==> 333

(sum3 1 2 3)

x=1
y=2
z=3

==> 6

x

==> 111

Из этого примера видно, что в теле функции значение, например, переменной x равно 1, а при выходе восстанавливается прежнее значение 111. Это происходит потому, что при связывании формальных параметров с фактическими в ассоциативном списке не проверяется существование переменных с именами из списка формальных параметров. Все новые записи добавляются в конец списка (а указатель заполнения ассоциативного списка запоминается во внутреннем стеке). А при запросе значения по имени переменной список просматривается от конца к началу. Поэтому связанная переменная встретится раньше свободной. Если включить дамп при вычислении последнего примера, то можно увидеть, как "уживаются" в ассоциативном списке свободные и связанные переменые:

Начало дампа:

.EVAL вход:  (sum3 1 2 3)
..APPLY вход: A1= sum3 A2= (1 2 3)
...EVFUN вход: A1= sum3 A2= (1 2 3)
...
... Ассоциативный список:
...
...  3  ->  (z . 333)
...  2  ->  (y . 222)
...  1  ->  (x . 111)

...  ...  ...

Вычисление sum3:

....LIST выход: Рез= (1 2 3)
....EVLAM вход: A1= ((x y z)(PROG NIL (PRINT (QUOTE x=))
                                      (PRINT x)
                                      (TERPRI)
                                      (PRINT (QUOTE y=))
                                      (PRINT y)
                                      (TERPRI)
                                      (PRINT (QUOTE z=))
                                      (PRINT z)
                                      (TERPRI)
                                      (RETURN (PLUS x y z))
                            )
                    )
                A2= (1 2 3)
.....PAIRLIS вход: ptrAasso=3 A1= (x y z) A2= (1 2 3)
.....
..... Ассоциативный список:
.....
.....  6  ->  (z . 3)
.....  5  ->  (y . 2)
.....  4  ->  (x . 1)
.....  3  ->  (z . 333)
.....  2  ->  (y . 222)
.....  1  ->  (x . 111)

Если в теле функции потребуется получить значение, скажем, y, то, поскольку просмотр списка идет от шестого элемента к первому, то сначала встретится пара (y.2). Значение y будет равно 2.

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

.EVAL вход: (sum3 111 222 333)
..APPLY_ вход: A1=sum3 A2=(111 222 333)
...EVFUN вход: A1=sum3 A2=(111 222 333)
...
...Лексический контекст:
...
...NIL
...
...Глобальный контекст:
...
...NIL
...
....LIST вход: A=(111 222 333)
.....EVAL вход: 111
......EVATOM вход: 111
......EVATOM выход: 111
.....EVAL выход: 111
.....EVAL вход: 222
......EVATOM вход: 222
......EVATOM выход: 222
.....EVAL выход: 222
.....EVAL вход: 333
......EVATOM вход: 333
......EVATOM выход: 333
.....EVAL выход: 333
....LIST выход: Рез=(111 222 333)
....EVLAM вход: A1=((x y z) (+ x y z)) A2=(111 222 333)
.....PAIRLIS вход: A1=(x y z) A2=(111 222 333) Flg=
.....
.....Лексический контекст:
.....
.....NIL
.....
.....Глобальный контекст:
.....
.....NIL
.....
.....PAIRLIS Выход:
.....
.....Лексический контекст:
.....
.....((z 333) (y 222) (x 111))
.....
.....Глобальный контекст:
.....
.....NIL
.....
.....EVAL вход: (+ x y z)
......APPLY_ вход: A1=+ A2=(x y z)
.......EVFUN вход: A1=+ A2=(x y z)
.......
.......Лексический контекст:
.......
.......((z 333) (y 222) (x 111))
.......
.......Глобальный контекст:
.......
.......NIL
.......
........LIST вход: A=(x y z)
.........EVAL вход: x
..........EVATOM вход: x
...........ASSOC вход: a1 = x
...........
...........Лексический контекст:
...........
...........((z 333) (y 222) (x 111))
...........
...........Глобальный контекст:
...........
...........NIL
...........
...........Assoc выход: 111
..........EVATOM выход: 111
.........EVAL выход: 111
.........EVAL вход: y
..........EVATOM вход: y
...........ASSOC вход: a1 = y
...........
...........Лексический контекст:
...........
...........((z 333) (y 222) (x 111))
...........
...........Глобальный контекст:
...........
...........NIL
...........
...........Assoc выход: 222
..........EVATOM выход: 222
.........EVAL выход: 222
.........EVAL вход: z
..........EVATOM вход: z
...........ASSOC вход: a1 = z
...........
...........Лексический контекст:
...........
...........((z 333) (y 222) (x 111))
...........
...........Глобальный контекст:
...........
...........NIL
...........
...........Assoc выход: 333
..........EVATOM выход: 333
.........EVAL выход: 333
........LIST выход: Рез=(111 222 333)
........PLUS вход: A=(111 222 333)
........PLUS Пром. Рез=111
........PLUS Пром. Рез=333
........PLUS Пром. Рез=666
........PLUS выход: Рез=666
.......EVFUN выход: 666
......APPLY_ выход: 666
.....EVAL выход: 666
....EVLAM выход: Рез=666
...EVFUN выход: 666
..APPLY_ выход: 666
.EVAL выход: 666

Если существуют глобальные переменные, одноименные с параметрами некоторой функции, то в качестве значений будут взяты значения параметров, а не глобальных переменных. Глобальные переменные располагаются в глобальном контексте, а параметры - в лексическом. При определении значения символа сначала просматривается текущий лексический контекст, и только потом - глобальный. Если в дополнение к определению функции sum3 завести три глобальные переменные x, y и z:

(setq x -111)

==> -111
Создана глобальная переменная x

(setq y -222)

==> -222
Создана глобальная переменная y

(setq z -333)

==> -333
Создана глобальная переменная z

а затем вычислить (sum3 111 222 333) с дампированием, то пользователь увидит:

.EVAL вход: (sum3 111 222 333)
..APPLY_ вход: A1=sum3 A2=(111 222 333)
...EVFUN вход: A1=sum3 A2=(111 222 333)
...
...Лексический контекст:
...
...NIL
...
...Глобальный контекст:
...
...((z -333) (y -222) (x -111))
...
....LIST вход: A=(111 222 333)
.....EVAL вход: 111
......EVATOM вход: 111
......EVATOM выход: 111
.....EVAL выход: 111
.....EVAL вход: 222
......EVATOM вход: 222
......EVATOM выход: 222
.....EVAL выход: 222
.....EVAL вход: 333
......EVATOM вход: 333
......EVATOM выход: 333
.....EVAL выход: 333
....LIST выход: Рез=(111 222 333)
....EVLAM вход: A1=((x y z) (+ x y z)) A2=(111 222 333)
.....PAIRLIS вход: A1=(x y z) A2=(111 222 333) Flg=
.....
.....Лексический контекст:
.....
.....NIL
.....
.....Глобальный контекст:
.....
.....((z -333) (y -222) (x -111))
.....
.....PAIRLIS Выход:
.....
.....Лексический контекст:
.....
.....((z 333) (y 222) (x 111))
.....
.....Глобальный контекст:
.....
.....((z -333) (y -222) (x -111))
.....
.....EVAL вход: (+ x y z)
......APPLY_ вход: A1=+ A2=(x y z)
.......EVFUN вход: A1=+ A2=(x y z)
.......
.......Лексический контекст:
.......
.......((z 333) (y 222) (x 111))
.......
.......Глобальный контекст:
.......
.......((z -333) (y -222) (x -111))
.......
........LIST вход: A=(x y z)
.........EVAL вход: x
..........EVATOM вход: x
...........ASSOC вход: a1 = x
...........
...........Лексический контекст:
...........
...........((z 333) (y 222) (x 111))
...........
...........Глобальный контекст:
...........
...........((z -333) (y -222) (x -111))
...........
...........Assoc выход: 111
..........EVATOM выход: 111
.........EVAL выход: 111
.........EVAL вход: y
..........EVATOM вход: y
...........ASSOC вход: a1 = y
...........
...........Лексический контекст:
...........
...........((z 333) (y 222) (x 111))
...........
...........Глобальный контекст:
...........
...........((z -333) (y -222) (x -111))
...........
...........Assoc выход: 222
..........EVATOM выход: 222
.........EVAL выход: 222
.........EVAL вход: z
..........EVATOM вход: z
...........ASSOC вход: a1 = z
...........
...........Лексический контекст:
...........
...........((z 333) (y 222) (x 111))
...........
...........Глобальный контекст:
...........
...........((z -333) (y -222) (x -111))
...........
...........Assoc выход: 333
..........EVATOM выход: 333
.........EVAL выход: 333
........LIST выход: Рез=(111 222 333)
........PLUS вход: A=(111 222 333)
........PLUS Пром. Рез=111
........PLUS Пром. Рез=333
........PLUS Пром. Рез=666
........PLUS выход: Рез=666
.......EVFUN выход: 666
......APPLY_ выход: 666
.....EVAL выход: 666
....EVLAM выход: Рез=666
...EVFUN выход: 666
..APPLY_ выход: 666
.EVAL выход: 666

Все сказанное относится к обычным (не функциональным) аргументам. Для функциональных аргументов (см FUNCTION) все происходит иначе.

Если же функция меняет значение свободной переменной то оно останется измененнным и после выхода из функции:

(sexpr sum3 (lambda (x y z)
                            (Prog nil
                                  (setq aaa 777)
                                  (return (+ x y z))
                            )
              )

)

==> sum3

(setq aaa 111)

==> 111

(sum3 1 2 3)

==> 6

aaa

==> 777

Видно, что переменная aaa - свободна. Ее значение меняется в теле функции и после возврата остается измененным.

Для 13-й редакции ядра предыдущий пример будет работать аналогично. Если свободной переменной в теле функции присваивается значение, то, при отсутствии одноименной глобальной переменной, она будет создана, а при наличии такой переменной, последняя изменит свое значение.

Замечание. В 13-й редакции ядра функции могут иметь необязательные, ключевые и вспомогательные параметры. Допустимы также функции с неопределенным числом параметров.
Необязательные (optional) параметры
Для задания функции с необязательными параметрами используется атом-маркер &optional. В нужном месте списка параметров ставится атом &optional; все элементы списка, расположенные перед &optional будут задавать обязательные параметры, а элементы, расположенные после маркера будут задавать необязательные параметры.

Необязательные параметры можно задавать двумя способами:

в виде списков вида (Имя Значение_по_умолчанию)
в виде списка атомов

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

(defun f (x &optional (y 222) (z 333)) (list x y z))

==> f

(f 111)

==> (111 222 333)

(f)

PairLis: Слишком мало фактических параметров
==> ERRSTATE

(f 111 -222)

==> (111 -222 333)

(f 111 -222 -333)

==> (111 -222 -333)

(f 111 -222 -333 444)

PairLis: Слишком много фактических параметров
==> ERRSTATE

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

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

(defun g (x &optional (y (* x x))) (varlist) (list x y))

==> g

(g 6)

+---------------------+---------+-----------+
| Имя переменной      | Тип     | Значение  |
+---------------------+---------+-----------+
|y                    |Лексич.  |36         |
|x                    |Лексич.  |6          |
+---------------------+---------+-----------+

==> (6 36)

(g 6 8)

+---------------------+---------+-----------+
| Имя переменной      | Тип     | Значение  |
+---------------------+---------+-----------+
|y                    |Лексич.  |8          |
|x                    |Лексич.  |6          |
+---------------------+---------+-----------+

==> (6 8)

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

Если все параметры функции нужно сделать необязательными, следует атом &optional поместить первым в списке формальных параметров.
Ключевые (key) параметры
Функции могут иметь ключевые параметры. Этот тип параметров задатся после списка необязательных позиционных параметров после атома &key. Задание ключевых параметров синтаксически похоже на задание необязательных параметров. Отличия появляются при вызове функции с ключевыми параметрами. При вызове функции с ключевыми параметрами значение ключевого параметра задается двумя выражениями: символом-именем параметра и формой, значение которой будет значением параметра. Символ-имя получается из имени параметра из списка параметров функции добавлением двоеточия в начало имени.

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

Рассмотрим пример:

(defun q (x &key (y (* x 2)) (z (* y 3)) ) (varlist) (list x y z))

==> q

(q 1)

+---------------------+---------+----------+
| Имя переменной      | Тип     | Значение |
+---------------------+---------+----------+
|z                    |Лексич.  |6         |
|y                    |Лексич.  |2         |
|x                    |Лексич.  |1         |
+---------------------+---------+----------+

==> (1 2 6)

(q 1 2 3)

Ключевой параметр 2 без двоеточия
==> ERRSTATE

(q 1 :z 2 :y -1)

+---------------------+---------+----------+
| Имя переменной      | Тип     | Значение |
+---------------------+---------+----------+
|y                    |Лексич.  |-1        |
|z                    |Лексич.  |2         |
|x                    |Лексич.  |1         |
+---------------------+---------+----------+

==> (1 -1 2)

У приведенной выше функции один обязательный позиционный параметр и два необязательных ключевых параметра.Хорошо видно, что ключевые параметры могут задаваться в произвольном порядке.
Вспомогательные (aux) параметры
Если функции нужны рабочие (вспомогательные) переменные, они могут быть заданы в списке пераметров, после символа &aux. Рабочие переменные при вычислении тела функции инициализируются значением Nil и могут использоваться наряду с другими видами параметров.

Вот пример функции со вспомогательными параметрами:

(defun g (x y &aux q w )
      (varlist)
      (setq q (* x x))
      (setq w (* y y))
      (varlist)
      (list x y q w))

==> g

(g 11 22)

+---------------------+---------+----------+
| Имя переменной      | Тип     | Значение |
+---------------------+---------+----------+
|w                    |Лексич.  |NIL       |
|q                    |Лексич.  |NIL       |
|y                    |Лексич.  |22        |
|x                    |Лексич.  |11        |
+---------------------+---------+----------+

+---------------------+---------+----------+
| Имя переменной      | Тип     | Значение |
+---------------------+---------+----------+
|w                    |Лексич.  |484       |
|q                    |Лексич.  |121       |
|y                    |Лексич.  |22        |
|x                    |Лексич.  |11        |
+---------------------+---------+----------+

==> (11 22 121 484)

Здесь у функци g два вспомогательных параметра. Пример, впрочем, несколько искуственный (без вспомогательных параметров в данном случае можно было бы легко обойтись). Конструкция "&aux список_символов" должна быть последней в списке параметров.
Функции с неопределенным числом параметров
Допустимы функции с неопределенным числом параметров. Чтобы функция могла обработать неопределенное число индивидуальных параметров, необходимо в конце списка параметров употребить конструкцию "&rest атом". При вычислении значений параметров после обработки обязательных и необязательных параметров (в т.ч. и ключевых) все остальные элементы списка фактических параметров группируются в один список и связываются с символом, стоящим после символа &rest.

Ниже приведен пример функции с неопределенным числом параметров:

(defun uparm (x &rest y) (cons x y))

==> uparm

(uparm 1 2 3 4 5)

==> (1 2 3 4 5)

(uparm '(1 2 3) 2 3 4 5 6 7 8 9)

==> ((1 2 3) 2 3 4 5 6 7 8 9)

Здесь у функции uparm один параметр обязательный (x), а остальные параметры собираются в список и присваиваются параметру y. Важно подчеркнуть, что конструкция "&rest символ" должна быть последней в списке параметров.Отсюда следует, что в HomeLisp нельзя одновременно использовать "&aux список_символов" и "&rest символ".

SFEXPR

Функция SFEXPR предназначена для создания функций пользователя типа FEXPR. Тип FEXPR означает, что ни один аргумент перед передачей в тело функции не вычисляется. Общий вид конструкции SEXPR приведен ниже:

(sfexpr Имя_Функции

       (lambda

           (...Список_параметров...)

           ...Тело функции...

       )
)

Именем функции может быть только атом. Список параметров - это список атомов - формальных параметров создаваемой функции. Другое название списка параметров - список связанных переменных. Тело функции представляет собой S-выражение. Это S-выражение может использовать переменные из списка параметров или любые другие переменные (которые называются свободными). Функция SFEXPR связывает имя функции с лямбда выражением. Если вызов SFEXPR корректен, то пользователю становится доступной новая функция с соответствующим именем, списком параметров и определяющим выражением. Вызов SFEXPR в этом случае возвращает имя вновь созданной функции.

(sfexpr sum3f (lambda (x y z) (+ x y z)))

==> sum3f

К вновь созданной функции следует обращаться так: (Имя_функции ...список_фактических_параметров...). Вычисление происходит следующим образом: сначала создаются переменные в соответствии со списком формальных параметров, затем каждой из этих переменных присваивается значение, равное соответствующему элементу списка фактических параметров. В отличие от функций типа EXPR, здесь не происходит вычисления элементов списка фактических параметров. Количество элементов списка формальных параметров и списка фактических параметров обязаны совпадать (в противном случае фиксируется ошибка). Этот процесс аналогичен передаче параметров в процедуру традиционного процедурного языка.

Далее вычисляется тело функции (определяющее выражение). Результат вычисления возвращается, как результат функции. Затем происходит уничтожение связанных переменных в ассоциативном списке. Для того, чтобы проследить детали вычисления функции, полезно включить режим дампирования. Вызовем, например, определенную ранее функцию sum3f следующим образом: (sum3f 11 22 33). В дамп-файл попадет следующая информация:

.EVAL вход:  (sum3f 11 22 33)
..APPLY вход: A1= sum3f A2= (11 22 33)
...EVFUN вход: A1= sum3f A2= (11 22 33)
....EVLAM вход: A1= ((x y z)(PLUS x y z)) A2= (11 22 33)
.....PAIRLIS вход: ptrAasso=0 A1= (x y z) A2= (11 22 33)
.....
..... Ассоциативный список:
.....
.....  3  ->  (z . 33)
.....  2  ->  (y . 22)
.....  1  ->  (x . 11)
.....
.....PAIRLIS выход: ptrAsso=3
.....EVAL вход:  (PLUS x y z)
......APPLY вход: A1= PLUS A2= (x y z)
.......EVFUN вход: A1= PLUS A2= (x y z)
.......
....... Ассоциативный список:
.......
.......  3  ->  (z . 33)
.......  2  ->  (y . 22)
.......  1  ->  (x . 11)
.......
........LIST вход: A= (x y z)
.........EVAL вход:  x
..........EVATOM вход:  x
..........EVATOM выход:  11
.........EVAL выход:  11
.........EVAL вход:  y
..........EVATOM вход:  y
..........EVATOM выход:  22
.........EVAL выход:  22
.........EVAL вход:  z
..........EVATOM вход:  z
..........EVATOM выход:  33
.........EVAL выход:  33
........LIST выход: Рез= (11 22 33)
........PLUS вход: A= (11 22 33)
........PLUS Пром. Рез=11
........PLUS Пром. Рез=33
........PLUS Пром. Рез=66
........PLUS выход: Рез= 66
.......EVFUN выход:  66
......APPLY выход:  66
.....EVAL выход:  66
....EVLAM выход: Рез= 66
...EVFUN выход:  66
..APPLY выход:  66
.EVAL выход:  66

Следует обратить внимание на то, что хотя результат вычисления и совпадает с результатом вычисления выражения (sum3 11 22 33), введенной выше при рассмотрении функций типа EXPR, но внутренняя логика вычислений различна. Фактические параметры сразу передаются на вход PLUS без предварительного вычисления. Результат совпадает просто потому, что значения аргументов 11, 22 и 33 совпадают с самими аргументами. Попытка вычислить (sum3f (+ 10 1) (+ 20 2) (+ 30 3)) вызвала бы ошибку:

Один из аргументов PLUS - не атом

В остальном логика вычисления функции типа EXPR и FEXPR совпадает: можно убедиться что в ассоциативном списке образовались три связаные в теле функции sum3f переменные. После возврата этих переменных в ассоциативном списке уже не будет.

(sum3f 11 22 33)

==> 66

x

==> Символ "x" не имеет значения (не связан).

При вычислении функций типа FEXPR действуют те же правила видимости переменных, что и для функций типа EXPR. Если к моменту вызова функции уже имеется свободная переменная, одноименная со связанной переменной из списка параметров, то в теле функции будет доступно только значение связанной переменной. После возврата из функции снова будет доступно прежнее значение свободной переменной. Вот развернутый пример на эту тему:

(sfexpr sum3f (lambda (x y z)
                             (Prog nil

                                   (print 'x=)
                                   (print x)
                                   (terpri)
                                   (print 'y=)
                                   (print y)
                                   (terpri)
                                   (print 'z=)
                                   (print z)
                                   (terpri)

                                   (return (+ x y z))
                               )
               )

)

==> sum3f

(setq x 111)

==> 111

(setq y 222)

==> 222

(setq z 333)

==> 333

(sum3f 1 2 3)

x=1
y=2
z=3

==> 6

x

==> 111

Из этого примера видно, что в теле функции значение, например, переменной x равно 1, а при выходе восстанавливается прежнее значение 111. Это происходит потому, что при связывании формальных параметров с фактическими в ассоциативном списке не проверяется существование переменных с именами из списка формальных параметров. Все новые записи добавляются в конец списка (а указатель заполнения ассоциативного списка запоминается во внутреннем стеке). А при запросе значения по имени переменной список просматривается от конца к началу. Поэтому связанная переменная встретится раньше свободной. Если включить дамп при вычислении последнего примера, то можно увидеть, как "уживаются" в ассоциативном списке свободные и связанные переменые:

Начало дампа:

.EVAL вход:  (sum3f 1 2 3)
..APPLY вход: A1= sum3f A2= (1 2 3)
...EVFUN вход: A1= sum3f A2= (1 2 3)
...
... Ассоциативный список:
...
...  3  ->  (z . 222)
...  2  ->  (y . 222)
...  1  ->  (x . 111)

...  ...  ...

Вычисление sum3:

....EVLAM вход: A1= ((x y z)(PROG NIL
                                     (PRINT (QUOTE x=))
                                     (PRINT x)
                                     (TERPRI)
                                     (PRINT (QUOTE y=))
                                     (PRINT y)
                                     (TERPRI)
                                     (PRINT (QUOTE z=))
                                     (PRINT z)
                                     (TERPRI)
                                     (RETURN (PLUS x y z))))
                A2= (1 2 3)

.....PAIRLIS вход: ptrAasso=3 A1= (x y z) A2= (1 2 3)
.....
..... Ассоциативный список:
.....
.....  6  ->  (z . 3)
.....  5  ->  (y . 2)
.....  4  ->  (x . 1)
.....  3  ->  (z . 222)
.....  2  ->  (y . 222)
.....  1  ->  (x . 111)
.....
.....PAIRLIS выход: ptrAsso=6

Если в теле функции потребуется получить значение, скажем, y, то, поскольку просмотр списка идет от шестого элемента к первому, то сначала встретится пара (y.2). Значение y будет равно 2. Если же функция меняет значение свободной переменной то оно останется измененнным и после выхода из функции:

(sexpr sum3f (lambda (x y z)
                            (Prog nil
                                  (setq aaa 777)
                                  (return (+ x y z))
                            )
              )

)

==> sum3f

(setq aaa 111)

==> 111

(sum3f 1 2 3)

==> 6

aaa

==> 777

Видно, что переменная aaa - свободна. Ее значение меняется в теле функции и после возврата остается измененным.

Как отмечалось выше в разделе, посвященном функциональным аргументам, для функции типа FEXPR при обращении недопустима конструкция (FUNCTION fff). Чтобы понять причину запрета, полезно вспомнить как обрабатываются функциональные аргументы функций типа EXPR.

Если среди аргументов функции типа EXPR встречается конструкция (FUNCTION fff), она вычисляется до начала вычисления тела функции. Результатом вычисления является занесение в ассоциативный список конструкции (f FUNARG fff n) (где f формальный параметр, соответствующий фактическому параметру fff, а n - указатель заполнения ассоциативного списка. Когда процесс исполнения тела функции дойдет до вычисления (f ...), ассоциативный список будет уже в другом состоянии (в него могут быть добавлены другие переменные - в том числе и связанные, но совпадающие по именам со свободными). Перед началом вычисления (f ...) в ассоциативный список добавится пара (Nil . n), где n - число из конструкции (f FUNARG fff n). Поэтому переменные, содержащиеся в списке между конструкциями (Nil . n) и (f FUNARG fff n) при поиске значений обходятся. Именно этот механизм и обеспечивает корректное использование свободных переменных функциональными аргументами.

Если же один из аргументов функции типа FEXPR будет иметь вид (FUNCTION fff), то произойдет совсем другое. Поскольку аргументы функции типа FEXPR не вычисляются, в ассоциативный список добавится конструкция (f function fff) (где f формальный параметр, соответствующий фактическому параметру fff) и состояние заполнения ассоциативного списка нигде не зафиксируется. Когда в процессе исполнения тела функции потребуется вычислить (f ...), то, поскольку атом f имеет в данном случае значение (function fff), это выражение вычислится в неподходящий момент (когда состояние ассоциативного списка уже изменилось). Поэтому использование конструкции (FUNCTION f), как аргумента функции типа FEXPR недопустимо. В HomeLisp попытка использвания аргумента вида (FUNCTION f) у функции типа FEXPR вызывает ошибку с диагностикой Недопустимое употребление FUNCTION.

Все сказанное не означает, что у функций типа FEXPR не может быть функциональных аргументов. Вот пример функции типа FEXPR с функциональным аргументом:

(sfexpr sfx (lambda (f x y) (f x y)))

 ==>  sfx

(sfx plus 3 5)

 ==>  8

(sfx times 3 5)

 ==>  15

(defun g (x y) (+ (* x x) (* y y)))

 ==>  g

(sfx g 3 4)

 ==>  25

Первый аргумент функции sfx - функциональный. Соответствующим фактическим параметром вызова в первых двух примерах являются встроенные функции типа SUBR (PLUS и TIMES). В последнем примере фактическим аргументом является функция типа EXPR, которая подсчитывает сумму квадратов двух своих аргументов. Как можно убедиться из приведенной выше врезки, все вызовы возвращают верные результаты. Однако, если попытаться перед вычислением функционального аргумента использовать в теле функции локальные переменные, одноименные со свободными, то можно убедиться, что правило Лиспа не соблюдается - возьмутся активные значения переменных, а не те, что были в момент вызова функционала. Пример приводится ниже:

(sfexpr sfx (lambda (f x y) (prog (z) (setq z 3) (return (f x y)))))

==>  sfx

(setq z 100)

==>  100

(defun g (x y) (+ z (* x y)))

==>  g

(g 3 5)

==>  115

(sfx g 3 5)

==>  18

Функция sfx типа FEXPR локализует в своем теле переменную z и присваивает ей значение 3. Следующая команда создает свободную переменную z и присваивает ей значение 100. Вновь определенная функция g вычисляет произведение своих аргументов и прибавляет к нему значение свободной переменной z. Вызов (g 3 5) возвращает, естественно, 3*5+100=115. При вычислении (sfx g 3 5) используется активное значение z и получается 3*5+3=18. Предотвратить использование активного значения z нельзя, т.к. нельзя использовать конструкцию (sfx (function g) 3 5):

(sfx (function g) 3 5)

==> Недопустимое употребление FUNCTION

Однако, если ввести соответствующую функцию типа EXPR, то может быть получен верный результат:

(sexpr sf (lambda (f x y) (prog (z) (setq z 3) (return (f x y)))))

==>  sf

(sf (function g) 3 5)

==>  115

Функции типа FEXPR используются реже функций типа EXPR. Более употребительной формой функций типа FEXPR являются функции типа MACRO, описываемые ниже.

SMACRO

Функции SMACRO нет в книге Лаврова и Силагадзе (что, пожалуй, можно считать единственным недостатком этой замечательной книги!), хотя в Лиспе идея макро присутствует достаточно давно. В связи с этим, разработчик счел своим долгом остановиться на идее макро чуть более подробно.

Однако, прежде, чем рассмотреть макро в Лиспе, следует, вероятно, объяснить, что означает сам термин "макро".

К сожалению, широкое распространение известного популярного пакета Microsoft Office исказило первоначальный смысл термина "макро". В терминах Microsoft Office макро (или макрос) - это программа на языке VBA, которая может быть написана "вручную" программистом или сгенерирована специальным средством - макрорекордером. Некоторые горячие головы стали использовать термин "макро" в уничитижительном смысле, считая макро не настоящими программами.

В действительности макро - это программный код, который выполняется во время компиляции программы до ее перевода на целевой язык. Так, в языках C/C++ макро реализовано с помощью т.н. препроцессора. Простейший пример макро в языках C/C++ дается командой #define. Если в тексте программы встретилась команда:

#define Pi  3.14159

то везде далее можно употреблять "константу" Pi. Слово "константа" взято в кавычки не случайно: перед трансляцией программы в машинный код препроцессор просто заменит все вхождения лексемы Pi во все выражения языка строкой символов 3.14159. Этот процесс называется макро-подстановкой. Так, оператор присвоения:

 y=sin(Pi*k);

препроцессор превратит в оператор:

 y=sin(3.14159*k);

"Константы", введеные командой #define, могут использоваться для т.н. условной компиляции - в зависимости от их значения трансляция некоторых блоков кода может обходиться. Сходные, но менее развитые средства имеются и в Visual Basic.

Этот экскурс был предпринят автором исключительно с целью показать, что макро в буквальном смысле созданы для Лиспа! Дело в том, что в Лиспе нет границы между программами и данными (и то, и другое - списки); поэтому макро смотрятся в Лиспе естественней, чем в любом другом языке.

Применительно к Лиспу макро - это функция, вычисляющая свое тело (или, как говорят, вычисляющая сама себя).

Макро в Лиспе похожа на функцию типа FEXPR - ее аргументы не вычисляются. Тело Макро вычисляется в два этапа: сначала проиходит вычисление результата аналогично тому, как вычисляется результат функции типа FEXPR. Результат этого первого этапа можно назвать макро-расширением; его можно увидеть, вызвав функцию MACROEXPAND. На втором этапе макрорасширение передается на вход функции EVAL и вычисляется. Результат вычисления и есть результат вычисления исходной функции типа Макро.

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

(prog nil
          (print s) (terpri)
          (print s) (terpri)
          ...       ...
          (print s) (terpri)
)

Количество строк print - terpri должно быть равно значению второго параметра - счетчика копий. Один из вариантов функции, генерирующей такой код, приводится ниже:

(smacro mprint (lambda (stri copies)

                       (Prog (u z)
                          (setq u copies)
                          (setq z '(Prog nil))
                     loop (setq z (append z (list (list 'print stri))))
                          (setq z (append z (list (list 'terpri))))
                          (setq u (- u 1))
                          (cond ((eq u 0) (return z))
                                (t (go loop))
                          )
                       )
               )
)

В локальной переменной z образуется нужное S-выражение (которое сразу же исполняется). Легко убедиться, что функция mprint работает:

(mprint '(a b c) 5)

(a b c)
(a b c)
(a b c)
(a b c)
(a b c)

==> Nil

При желании можно увидеть макрорасширение mprint:

(macroexpand mprint '(a b c) 5)

==> (PROG NIL (PRINT (QUOTE (a b c)))(TERPRI)
              (PRINT (QUOTE (a b c)))(TERPRI)
              (PRINT (QUOTE (a b c)))(TERPRI)
    )

Этот пример может показаться бесполезным - того же эффекта проще добиться обычной функцией типа EXPR:

(defun eprint (stri copies) (prog (i)
                                  (setq i copies)
                     loop         (print stri)
                                  (terpri)
                                  (setq i (sub1 i))
                                  (cond ((greaterp i 0) (go loop)))
                             )
)

==> eprint

(eprint '(a b c) 6)

(a b c)
(a b c)
(a b c)
(a b c)
(a b c)
(a b c)

==> NIL

Не следует, однако, торопиться с выводами. Рассмотрим, следуя двухтомнику Э. Хювенен и И. Сеппянен [6], следующую функцию:

(sfexpr pushf (lambda (a p) (setq p (cons a (eval p)))))

 ==>  pushf

Эта функция использует линейный список, как стек. Она добавляет свой первый аргумент в начало списка. Функция вполне успешно работает:

(setq st '(b a))

==> (b a)

(pushf c st)

==> (c b a)

Теперь попробуем создать стек с именем p и добавить в него элемент посредством pushf:

(setq p '(2 1))

==> (2 1)

(pushf 3 p)

==> (3 . p)

Функция работает неверно. В чем причина? В том, что имя переменной-стека совпадает с именем связанной переменной p. Поскольку переменная p входит в список формальных параметров функции pushf, ее внешнее значение в теле функции недоступно. При входе в функцию pushf переменная p (теперь локальная) получает значение, равное атому p, а переменная a - атому 3. Вычисление выражения (cons a (eval p)) дает в результате точечную пару (3 . p). Все это отчетливо видно в дампе:

.EVAL вход:  (pushf 3 p)
..APPLY вход: A1= pushf A2= (3 p)
...EVFUN вход: A1= pushf A2= (3 p)
...
... Ассоциативный список:
...
...  2  ->  (p 2 1)
...  1  ->  (st b c)
...
....EVLAM вход: A1= ((a p)(SETQ p (CONS a (EVAL p)))) A2= (3 p)
.....PAIRLIS вход: ptrAasso=2 A1= (a p) A2= (3 p)
.....
..... Ассоциативный список:
.....
.....  4  ->  (p . p)
.....  3  ->  (a . 3)
.....  2  ->  (p 2 1)
.....  1  ->  (st b c)
.....
.....PAIRLIS выход: ptrAsso=4
.....EVAL вход:  (SETQ p (CONS a (EVAL p)))
......APPLY вход: A1= SETQ A2= (p (CONS a (EVAL p)))
.......EVFUN вход: A1= SETQ A2= (p (CONS a (EVAL p)))
.......
....... Ассоциативный список:
.......
.......  4  ->  (p . p)
.......  3  ->  (a . 3)
.......  2  ->  (p 2 1)
.......  1  ->  (st b c)
.......
........SETQ вход: A= (p (CONS a (EVAL p)))
.........EVAL вход:  (CONS a (EVAL p))
..........APPLY вход: A1= CONS A2= (a (EVAL p))
...........EVFUN вход: A1= CONS A2= (a (EVAL p))
...........
........... Ассоциативный список:
...........
...........  4  ->  (p . p)
...........  3  ->  (a . 3)
...........  2  ->  (p 2 1)
...........  1  ->  (st b c)
...........
............LIST вход: A= (a (EVAL p))
.............EVAL вход:  a
..............EVATOM вход:  a
..............EVATOM выход:  3
.............EVAL выход:  3
.............EVAL вход:  (EVAL p)
..............APPLY вход: A1= EVAL A2= (p)
...............EVFUN вход: A1= EVAL A2= (p)
...............
............... Ассоциативный список:
...............
...............  4  ->  (p . p)
...............  3  ->  (a . 3)
...............  2  ->  (p 2 1)
...............  1  ->  (st b c)
...............
................LIST вход: A= (p)
.................EVAL вход:  p
..................EVATOM вход:  p
..................EVATOM выход:  p
.................EVAL выход:  p
................LIST выход: Рез= (p)
................EVAL вход:  p
.................EVATOM вход:  p
.................EVATOM выход:  p
................EVAL выход:  p
...............EVFUN выход:  p
..............APPLY выход:  p
.............EVAL выход:  p
............LIST выход: Рез= (3 p)
............CONS вход: A1= 3 A2= (p)
............CONS выход: Рез= (3 . p)
...........EVFUN выход:  (3 . p)
..........APPLY выход:  (3 . p)
.........EVAL выход:  (3 . p)
........SETQ выход: Рез= (3 . p)
.......EVFUN выход:  (3 . p)
......APPLY выход:  (3 . p)
.....EVAL выход:  (3 . p)
....EVLAM выход: Рез= (3 . p)
...EVFUN выход:  (3 . p)
..APPLY выход:  (3 . p)
.EVAL выход:  (3 . p)

В классическом Лиспе эту проблему решали, просто используя в качестве формальных параметров экзотические атомы типа *1 или %1. Использование макро решает проблему радикально:

(smacro pushm (lambda (a p) (list 'setq p (list 'cons a p))))

==> pushm

(pushm 3 p)

==> (3 2 1)

Эта функция работает верно, поскольку связи между формальными и фактическими параметрами действуют только в процессе макрорасширения. При вычислении результата макрорасширения становятся вновь доступны все свободные переменные. Пусть читатель обратит внимание еще на одно важное обстоятельство. Казалось бы, кто мешает рализовать функцию pushm, как функцию класса FEXPR, в которой строилось то же самое макрорасширение и тут же вычислялось (функцией EVAL)? Однако такая попытка ничего не даст. Приводимый ниже пример убеждает в этом.

(sfexpr pushm (lambda (a p) (eval (list 'setq p (list 'cons a p))) ) )

==> pushm

(setq p '(3 2 1))

==> (3 2 1)

(pushm 4 p)

==> (4 . p)

Причина - в реализации макро. Если макрорасширение вычисляется в той же функции посредством вызова EVAL, все свободные переменные, входящие в список формальных параметров, остаются "замаскированными". А при использовании макро, сгенерированное макрорасширение вычисляется, как говорят, в исходном контексте (когда значения всех свободных переменых снова доступны). В этом состоит главная особенность функций, типа SMACRO.
Кстати, по этой же причине попытка вычислить (pushm a p) вызывает ошибку: Символ a не имеет значания (не связан). Казалось бы, этого не должно быть - ведь аргументы SMACRO не вычисляются. Это так, но когда вычисляется результат макрорасширения, действуют обычные правила вычисления. Результат макрорасширения вызова (pushm a p) будет (SETQ p (CONS a p)). Переменная p имеет значение, а атом a - нет. Сообщение об ощибке, приведенное выше, возникает не при макрогенерации, а при вычислении его результата ((SETQ p (CONS a p))). А вот обращение (pushm 'a p) выполнится без проблем: здесь результат макрорасширения будет (SETQ p (CONS (QUOTE a) p)); атом a квотирован и ошибки нет.

Как отмечают Э. Хювенен и И. Сеппянен, макро могут быть и рекурсивными. Ниже приводится пример рекурсивного макро:

(smacro copym (lambda (x)
              (cond ((null x) nil)
                     (T (list 'cons (list 'quote (car x))
                                    (list 'copym (cdr x)))))))

==> copym

(copym (a b c))

==> (a b c)

Следует отметить, что в HomeLisp для рекурсивного макро вызов MACROEXPAND вернет только первый "каскад" макрорасширения:

(macroexpand copym (a b c))

==> (CONS (QUOTE a)(copym (b c)))

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

(FOR i Beg End (... Операторы ...))

Здесь i - переменная цикла; Beg - начальное значение; End - конечное значение. Операторы представляют собой список вызовов функций Лиспа. Проще всего реализовать такой оператор с помощью PROG-конструкции:

(PROG Nil

      (setq i Beg)

  L   (... Операторы ...)

      (setq i (add1 i))

      (cond ((greaterp i End) (Return End))

      (go L)
)

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

(smacro for

   (lambda

    (i ibeg iend meat)

       (prog (stmt)

        (setq stmt (list 'prog nil (list 'setq i ibeg)))

        (setq stmt (append stmt (list 'beg_loop)))

        (setq stmt (append stmt meat))

        (setq stmt (append stmt (list (list 'setq i (list 'add1 i)))))

        (setq stmt (append stmt (list (list 'cond (list (list 'greaterp i iend)

                                                  (list 'return iend) )))))

        (setq stmt (append stmt (list  (list 'go 'beg_loop))))

        (return stmt)

       )

   )

)

Остается убедиться, что "новый оператор" успешно работает:

(for i 1 10 ((print 'i=) (print i) (terpri)))

i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9
i=10

==> 10

и даже позволяет делать вложенные циклы:

(for i 1 3
          ((for j 1 3
                     ((print 'i=)
                      (print i)
                      (prints " ")
                      (print 'j=)
                      (print j)
                      (terpri)
                     )
           )
          )
)

i=1 j=1
i=1 j=2
i=1 j=3
i=2 j=1
i=2 j=2
i=2 j=3
i=3 j=1
i=3 j=2
i=3 j=3

==>  3
SPECIALP

Функция SPECIALP, реализованная только в 13-й редакции ядра, принимает один обязательный параметр-атом. Функция проверяет, является ли атом-аргумент динамической переменной. В этом случае функция возвратит T, в противном случае функция вернет Nil. Динамические переменные называются также специальными переменными (отсюда и происходит название SPECILP).

Примеры:

(defdyn 'x)

==> x

(specialp 'x)

==> T

(specialp 'y)

==> NIL

Здесь создана динамическая (специальная) переменная x. Символ y задает обычную (лексическую) переменную.

SPROPL

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

Здесь следует еще раз отметить, что стандартные индикаторы (FIXED, FLOAT, BITS, STRING, SUBR, FSUBR, EXPR, FEXPR, MACRO, WINDOW, FILE) не хранятся в списках свойств атомов и не могут быть изменены посредством функций SPROPL, PUTPROP, PUTFLAG. Стандартные индикаторы "появляются" у атомов, как следствие действий, выполняемых над атомами (создание переменных, констант, функций). Однако, функция PROPLIST возвращает полный список свойств атома, в котором сначала идут стандартные индикаторы, а потом список свойств, формируемый функцией SPROPL. На приводимой ниже врезке все это наглядно показано.

(setq a 111)

==>  111

(proplist 'a)

==>  (FIXED)

(spropl 'a '(i1 (q w e) j1 (r t y)))

==>  (i1 (q w e) j1 (r T y))

(proplist 'a)

==>  (FIXED i1 (q w e) j1 (r T y))

(setq a 11.1)

==>  11.1

(proplist 'a)

==>  (FLOAT i1 (q w e) j1 (r T y))

Пусть читатель обратит внимание на то, что при изменении типа переменной a с FIXED на FLOAT автоматически изменился и ее список свойств. Если же атом используется для создания графического окна или файлового потока, то старое содержимое списка свойств (если он имелся у атома) стирается, и заменяется новым:

(setq w 555)

==> 555

(spropl 'w '(i1 (z x c) i2 (v b n)))

==> (i1 (z x c) i2 (v b n))

(proplist 'w)

==> (FIXED i1 (z x c) i2 (v b n))

(grwCreate 'w 300 300 "Мое окно" &H5599AA)

==> w

(proplist 'w)

==> (WINDOW width 300
            height 300
            title "Мое окно"
            backcolor &H5599AA
            WINHANDLE 1)

После создания графического окна старый список свойств атома w полностью заменяется новым.

STRINGP

Функция-предикат STRINGP (отсутствующая у Лаврова и Силагадзе) принимает единственный аргумент. Если этот аргумент является атомом и имеет тип STRING, функция возвращает логическую истину (T). В противном случае функция возвращает Nil. Вот примеры вызова:

(stringp "K")

==>  T

(stringp 4)

==>  NIL

(stringp '(1 2 3))

==>  NIL
STR2FIX

Функция STR2FIX (отсутствующая у Лаврова и Силагадзе) принимает единственный аргумент. Аргумент должен быть атомом и иметь тип STRING. Функция преобразует значение аргумента в число типа FIXED. Из значения аргумента выделяется слева строка максимальной длины, которая представляет собой корректное представление целого числа со знаком. Это число возвращается в качестве результата. Лидирующие пробелы в значении первого аргумента игнорируются. Если строки, представляющей целое число, не существует, функция возвращает нуль. Примеры вызова приводятся ниже:

(str2fix "123456")

==>  123456

(str2fix "123.456")

==>  123

(str2fix "123 456")

==>  123

(str2fix "+123456")

==>  123456

(str2fix "-123456")

==>  -123456

(str2fix "a-123456")

==>  0

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

STR2FLO

Функция STR2FLO (отсутствующая у Лаврова и Силагадзе) принимает единственный аргумент. Аргумент должен быть атомом и иметь тип STRING. Функция преобразует значение аргумента в число типа FLOAT. Из значения аргумента выделяется слева строка максимальной длины, которая представляет собой корректное представление числа c плавающей точкой. Лидирующие пробелы в значении первого аргумента игнорируются. Это число возвращается в качестве результата. Если строки, представляющей вещественное число, не существует, функция возвращает нуль. Примеры вызова приводятся ниже:

(str2flo "8.8")

==>  8.8

(str2flo "8.8E12")

==>  8800000000000

(str2flo "8.8D12")

==>  8800000000000

(str2flo "8.8Q12")

==>  8.8

(str2flo "A8.8")

==>  0.0

В отличие от описанной выше функции STR2FIX функция STR2FLO всегда возвращает результат типа FLOAT. Кроме того, десятичная точка является значимым символом.

SUB1

Функция SUB1 принимает единственный аргумент числового типа (FIXED или FLOAT). Она отнимает единицу от значения аргумента. Тип результата совпадает с типом аргумента. Примеры:

(SUB1 -7.0)

==> -8.0

(PROPLIST (SUB1 -7.0))

==> (FLOAT)

(SUB1 54)

==> 53

(PROPLIST (SUB1 54))

==> (FIXED)
SWAPEL

Функция SWAPEL (отсутствующая у Лаврова и Силагадзе) принимает три аргумента. Значением первого аргумента должен быть список. Два последующих аргумента (типа FIXED) задают номера двух элементов этого списка. Если значения номеров положительны и не превышают длины списка, то элементы меняются местами, а функция возвращает T. В противном случае функция возвращает атом outOfRange, а список не меняется. Вот примеры вызова SWAPEL:

(setq z '(a b c d e f))

==> (a b c d e f)

(swapel z 3 5)

==> T

z

==> (a b e d c f)

(swapel z 3 10)

==> OutOfRange

Функция SWAPEL может быть реализована на Лиспе. Реализация ее в виде функции типа SUBR преследует исключительно цели эффективности.

TERPRI

Функция TERPRI не требует аргументов. Она всегда возвращает значение NIL. Функция обеспечивает вывод данных последующих вызовов PRINT/PRINTS с новой строки. Пусть читатель сравнит вывод двух приводимых ниже программ:

(prog (i)
          (setq i 5)
        L (print 'i=)
          (print i)
          (cond ((eq i 0) (return nil)))
          (setq i (sub1 i))
          (go L)
)

i=5i=4i=3i=2i=1i=0
==> NIL

(prog (i)
          (setq i 5)
        L (print 'i=)
          (print i)
          (terpri)
          (cond ((eq i 0) (return nil)))
          (setq i (sub1 i))
          (go L)
)

i=5
i=4
i=3
i=2
i=1
i=0

==> NIL
TIMES(*)

Функция TIMES,(вместо TIMES можно писать *) вычисляет произведение значений своих аргументов, которых может быть произвольное количество. Тип аргументов должен быть FIXED или FLOAT. При этом, если среди аргументов есть хотя бы один аргумент типа FLOAT, то и результат будет иметь тип FLOAT. Рассмотрим примеры:

(times 1 3 5 7)

==> 105

(* 1 2 3)

==> 6

(* 1 2.0 3)

==> 6.0
TRY

Выполнение программы на языке HomeLisp заключается в последовательном вычислении S-выражений. Если в процессе вычисления произошла ошибка, то устанавливается внутренний флаг состояние ошибки. После этого все вызовы главной функции Лиспа EVAL, помещенные в стек вызовов, завершаются, а в качестве значения возвращается предопределеный атом ERRSTATE. Это означает, что любая ошибка гарантированно вызывает завершение программы. Излишне распростаняться на тему, что программирование на самом выразительном и мощном языке превращается в муку, если в языке отсутствуют средства обработки ошибок.

Функция TRY, (отсутствующая у Лаврова и Силагадзе) позволяет перехватывать ошибки выполнения. Функция реализует современную концепцию обработки ошибок. Суть концепции состоит в том, что выделяется критический участок кода, который выполняется в составе конструкции:

     TRY

        Критический участок кода

     EXCEPT

        Обработчик ошибки

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

Применительно к Лиспу, критический участок и обработчик ошибки - это S-выражения. Возможности обработчика ошибки в HomeLisp скромнее, чем в C++, - он позволяет сбросить состояние ошибки, выдать сообщение и продолжить выполнение программы (если оно имеет смысл). Кроме того, в обработчике ошибок можно получить текст сообщения об ошибке. Для этого служит функция ERRORMESSAGE.

Функция TRY вызывается следующим образом:

(TRY Критический участок EXCEPT Обработчик)

Рассмотрим примеры:

(setq x 111)

==>  111

(setq z 000)

==>  0

(/ x z)

DIVIDE - деление на нуль
==>  ERRSTATE

(try (/ x z) except nil)

==>  NIL

(setq z 222)

==>  222

(try (/ x z) except nil)

==>  0.5

Здесь созданы две переменные, а затем вычисляется частное, при делителе, равном нулю. Естественно, возникает ошибка. Если же вычислить это выражение посредством функции TRY, то можно предотвратить возникновение ошибки и вернуть в качестве результата какое-либо значение (в пример - Nil). Если же делитель не равен нулю, то функция TRY возвращает правильный результат вычисления.

Критический участок представляет собой S-выражение, и, в частности - вложенный вызов функции TRY. Приводимый ниже пример как раз демонстрирует вложенные обработчики ошибок.

// Пример-1

(try
   (prog (x y)
         (setq x 12)
         (setq y 0)
         (return (try
                     (/ x y)
                  except 'err-1))
    )
 except 'err-0)

==>  err-1

// Пример-2

(try
   (prog (x y)
         (setq x 12)
         (setq t 66)
         (return (try
                     (/ x y)
                  except 'err-1))
    )
 except 'err-0)

==>  err-0

// Пример-3

(try
   (prog (x y)
         (setq x 12)
         (setq y 3)
         (return (try
                    (/ x y)
                  except 'err-1))
    )
 except 'err-0)

==>  4.0

Во всех примерах использованы вложенные обработчики ошибок. Но в первом примере ошибка возникает при делении (в области действия внутреннего вызова TRY (место ошибки выделено красным). Срабатывает внутренний обработчик и возвращается значение 'err-1. Во втором примере ошибка возникает до деления (при попытке присвоить значение константе T - место ошибки выделено красным). Срабатывает внешний обработчик и возвращается значение 'err-0. В последнем примере ошибок не возникает, и благополучно возвращается верный результат.

WHEN

Функция WHEN типа FSUBR (как и "парная" функция UNLESS) реализована только в 13-й редакции ядра. Функция обеспечивает условное вычисление. Вызов функции имеет вид:

(WHEN условие форма1 форма2 ... формаn)

Вычисление функции WHEN начинается с вычисления формы условие. Если результат отличен от Nil, то последовательно вычисляются формы форма1 форма2 ... формаn. Результатом вычисления WHEN является результат вычисления последней формы - формаn. Если же результат вычисления формы условие есть Nil, то функция WHEN сразу же возвращает Nil.

Примеры:

(defun f (x y &aux z)
    (when (<> x y)
          (setq z (list (* x x) (* y y)))
          (sumlist z)))

==> f

(f 8 9)

==> 145

(f 5 5)

==> Nil

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

Если же значения аргументов совпадат, то функция WHEN возвращает Nil.

UNLESS

Функция UNLESS типа FSUBR (как и "парная" функция WHEN) реализована только в 13-й редакции ядра. Функция обеспечивает условное вычисление. Вызов функции имеет вид:

(UNLESS условие форма1 форма2 ... формаn)

Вычисление функции UNLESS начинается с вычисления формы условие. Если результат есть Nil, то последовательно вычисляются формы форма1 форма2 ... формаn. Результатом вычисления WHEN является результат вычисления последней формы - формаn. Если же результат вычисления формы условие отличен от Nil, то функция UNLESS сразу же возвращает Nil.

Примеры:

(defun f (x y &aux z)
    (unless (<> x y)
          (setq z (list (* x x) (* y y)))
          (sumlist z)))

==> f

(f 8 9)

==> Nil

(f 5 5)

==> 50

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

Если же значения аргументов не совпадат, то функция UNLESS возвращает Nil.

UNSET

Функция UNSET принимает единственный аргумент, значением которого должен быть атом. Функция ищет в ассоциативном списке глобальную переменную (созданную вызовами SET/SETQ) с именем, заданным этим атомом, и уничтожает ее. Функция не удаляет переменные, связанные в теле функций и локальные PROG-переменные. Попытка удаления таких переменных посредством вызова UNSET вызывает ошибку. Вот примеры вызова функции UNSET:

(setq z '(1 2 3))

==> (1 2 3)

(assolist)

+-----------------------------------------+----------------------------------+
| Имя переменной                | Тип     | Значение                         |
+-------------------------------+---------+----------------------------------+
|z                              |Глобал.  |(1 2 3)                           |
+-------------------------------+---------+----------------------------------+

==> NIL

(unset 'z)

==> T

(assolist)

Ассоциативный список пуст

(prog (a b) (setq a 111) (setq b 222) (unset 'b) (return a))

UNSET: переменная b не найдена
==> ERRSTATE

(defun f (x y) (unset 'x))

==> f

(f 5 6)

UNSET: переменная x не найдена
==> ERRSTATE

Здесь сначала создается глобальная переменная z, отображается состояние ассоциативного списка, переменная удаляется и вновь отображается состояние ассоциативного списка. Все эти действия завершаются успешно. Далее делается попытка удалить PROG-переменную; при этом возникает ошибка. Аналогичная ошибка возникает при попытке удалить переменную, связанную в теле функции f.

ZEROP

Функция-предикат ZEROP проверяет, является значение ее единственного аргумента типа FIXED атомом 0; в этом случае возвращается T. В противном случае возвращается Nil. При попытке обратиться к предикату с аргументом, имеющим тип, отличный от FIXED, возникает ошибка. Вот примеры:

(zerop 1)

==>  Nil

(zerop 0)

==>  T

(zerop 0.0)

==>  Аргумент ZEROP не число FIXED

(zerop (- 1 1))

==>  T

В последнем примере сначала будет вычислено выражение (- 1 1) и получен результат 0. Далее будет вычисляться выражение (zerop 0), значение которого равно T.


Описание функций работы с COM-объектами

Описание функций работы с COM-объектами в HomeLisp

Одним из краеугольных камней операционной системы Windows является компонентная модель объекта (Component Object Model; COM). Эта модель стандартизует правила построения и манипулирования объектами. Подавляющее большинство стандартных элементов оконного интерфейса Windows на самом деле являются COM-объектами (другое название - ActiveX-компонент). Так, командная кнопка, поле ввода, список являются ActiveX-компонентами. Однако понятие COM-объекта существенно шире. COM-объект может не иметь графического визуального интерфейса, а может быть полнофункциональным Windows-приложением. Так большинство офисных продуктов Microsoft (Excel, Word, PowerPoint и др.) могут рассматриваться, как COM-объекты.

COM-объекты "живут" в файлах типа dll, exe или ocx. Чтобы источник COM-объектов мог упешно функционировать, он должен быть зарегистрирован на компьютере. Суть регистрации заключается в том, что информация о COM-объекте заносится в системный реестр. После регистрации источник COM-объектов готов к работе.

С точки зрения HomeLisp-а COM-объект - это атом, у которого установлен определенный список свойств и стандартный индикатор COM. При создании объектов или при возврате объектов из методов и свойств необходимо на месте соответствующих параметров задавать атом без списка своиств и без индикаторов. Будем далее называть такие атомы чистыми.

Все функции манипулирования COM-объектами являются функциями типа SUBR (их аргументы вычисляются).

Имена функций манипулирования COM-объектами в HomeLisp для удобства начинаются префиксом COM. Эти функции доступны во всех режимах работы HomeLisp, кроме WEB-компоненты.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
COMCREATEOBJECT | 2 | 1-Атом; 2-STRING | Создает COM-объект, имя которого представлено значением ВТОРОГО аргумента. Атом-значение первого аргумента становится ссылкой на созданный объект
COMDESTROYOBJECT | 1 | Атом | Атом-значение аргумента должен иметь стандартный индикатор COM. Функция уничтожает COM-объект, созданный ранее вызовом ComCreateObject
COMINTERFACE | 1 | STRING | Возвращает интерфейс COM-объекта, заданного значением первого параметра.
COMMETHOD | 2 и более | 1-COM; 2-STRING; 3 и далее FIXED, FLOAT, BITS, STRINGS | Вызов метода объекта
COMMETHODO | 2 и более | 1-COM; 2-STRING; 3 и далее FIXED, FLOAT, BITS, STRINGS | Вызов метода объекта (возвращающего объектную ссылку)
COMPROPGET | 2 и более | 1-COM; 2-STRING; 3 и далее FIXED, FLOAT, BITS, STRINGS | Вызов PROPERTY GET объекта.
COMPROPGETO | 2 и более | 1-COM; 2-STRING; 3 и далее FIXED, FLOAT, BITS, STRINGS | Вызов PROPERTY GET объекта (c возвратом объектной ссылки)
COMPROPLET | 2 и более | 1-COM; 2-STRING; 3 и далее FIXED, FLOAT, BITS, STRINGS | Вызов PROPERTY LET объекта.
COMCREATEOBJECT

Функция COMCREATEOBJECT служит для создания COM-объекта. Функция принимает два параметра: первым параметром должен быть "чистый" атом (т.е. атом, не содержащий в списке свойств никаких индикаторов). Вторым параметром должна быть строка вида "Имя_Сервера.Имя_Объекта". Если COM-сервер, заданный именем, зарегистрирован на компьютере, будет создан COM-объект, а атом, заданный первым параметром вызова получит в список свойств все необходимые индикаторы. В дальнейшем все действия с COM-объектом выполняются посредством ссылки на этот атом (который далее будем называть идентификатором объекта).

Если COM-объект создан успешно, функция COMCREATEOBJECT возвращает идентификатор объекта.

Вот примеры вызова функции COMCREATEOBJECT:

(comCreateObject 'xlsApp "Excel.Application")

==> xlsApp

(proplist 'xlsApp)

==> (COM Lib "Excel.Application" COMHANDLE 1)

(comPropLet 'xlsApp "Visible" -1)

==> T

(comDestroyObject 'xlsApp)

==> T

(comCreateObject 'o "a.a")

comCreateObject: Ошибка при создании COM-объекта ERRCode=429
==> ERRSTATE

Здесь создается объект-приложение Excel. Запрос списка свойст у идентификатора объекта показыает, что объект создан успешно. Далее, свойству Visible присваивается значение True (-1). После этого на экране возникает главное меню Excel. Далее вызов COMDESTROYOBJECT уничтожает приложение Excel. Последняя команда делает попытку создать незарегистрированный (несуществующий) объект. Возбуждается состояние ошибки с кодом VB, равным 429.

Следует также отметить, что попытка модифицировать список свойств идентификатора COM-объекта вызовет ошибку - список свойств защищен от модификации.

COMDESTROYOBJECT

Функция COMDESTROYOBJECT уничтожает созданный ранее COM-объект. Единственный аргумент этой функции должен быть идентификатором COM-объекта. При успешном завершении функция возвращает T, COM-объект выгружается из памяти, а идентификатор объекта лишается списка свойств. Если на вход функции COMDESTROYOBJECT подан атом, не имеющий в списке свойств флага COM, то ошибки не возникает, но возвращается Nil.

Все это иллюстрируется следующим примером:

(comCreateObject 'xlsApp "Excel.Application")

==> xlsApp

(proplist 'xlsApp)

==> (COM Lib "Excel.Application" COMHANDLE 1)

(comDestroyObject 'xlsApp)

==> T

(proplist 'xlsApp)

==> NIL

(comDestroyObject 'z)

==> NIL

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

COMINTERFACE

Функция COMINTERFACE позволяет получить в виде списка интерфейс COM-объекта (т.е. описание его свойств и методов). Функция использует стандартный Microsoft-компонент TLBInf32.dll (если этот компонент отсутствует - будет возбуждено состояние ошибки).

Функция принимает один параметр типа STRING, значение которого должно иметь вид: Имя_библиотеки.Имя_класса. Имя библиотеки - это имя файла с расширением dll/ocx/exe, который зарегистрирован на компьютере пользователя. Функция возвращает интерфейс заданного класса в виде списка сложной структуры. На верхнем уровне этого списка находятся описания элементов интерфейса (методов и свойств). В свою очередь, каждый элемент интерфейса является списком, включающим: Имя элемента, Тип (метод, свойство), тип возвращаемого значения, и описание списка параметров. Если свойство или метод не имеют параметров, то на месте списка параметров возвращается атом Nil.

Рассмотрим конкретный пример использования функции COMINTERFACE. Как отмечается в разделе, посвященном Web-компоненте, для связи web-страницы и ядра лиспа используется коммуникационный объект. Этот объект "живет" в библиотеке Tpw.dll c именем класса - clsPipes. Ниже приводится полный интерфейс этого класса (вывод для наглядности отформатирован):

(cominterface "tpw.clsPipes")

==> ((QueryInterface Method VOID ("riid" EMPTY "ppvObj" VOID))
     (AddRef Method UI4 NIL)
     (Release Method UI4 NIL)
     (GetTypeInfoCount Method VOID ("pctinfo" UINT))
     (GetTypeInfo Method VOID ("itinfo"  UINT
                               "lcid"    UI4
                               "pptinfo" VOID))
     (GetIDsOfNames Method VOID ("riid"  EMPTY
                                 "rgszNames" I1
                                 "cNames"    UINT
                                 "lcid"      UI4
                                 "rgdispid"  LONG))
     (Invoke Method VOID ("dispidMember" LONG
                          "riid"         EMPTY
                          "lcid"         UI4
                          "wFlags"       UI2
                          "pdispparams"  EMPTY
                          "pvarResult"   VARIANT
                          "pexcepinfo"   EMPTY
                          "puArgErr"     UINT)
                         )
     (SendRequest Method VOID ("Req" BSTR))
     (GetResp Method BSTR NIL)
     (InitPipe Method VOID ("ExeName" BSTR))
    )

Хорошо видно, что первыми тремя методами являются QueryIntreface, AddRef и Release соответственно. Эти три метода составляют незыблемую основу модели COM. Содержательные методы класса выделены красным цветом. Рассмотрим, например, первый из них - SendRequest. Метод не возвращает значения (VOID), и принимает один параметр с именем Req и типом BSTR. Тип BSTR это строка в кодировке UNICODE c предшествующим двухбайтовым дескриптором длины. Следующий метод GetResp возвращает значение типа BSTR и не требует параметров.

Если исследуемый класс имеет свойства, то для каждого из них в интерфейсе будет присутствовать одна или обе процедуры Property-Get/Property-Let. Интерфейс этих процедур описывается так же, как интерфейс методов (имя - возвращаемое значение - параметры). Если некоторое свойство представлено в интерфейсе класса обеими процедурами, значит это свойство допускает чтение и запись. Если присутствует только процедура Property-GET, значит свойство допускает только чтение (ReadOnly). Соответствеено, наличие только процедуры Property-LET означает, свойство допускает только запись (WriteOnly). В качестве примера класса со свойствами можно рассмотреть интерфейс класса полноэкранного календаря, который содержится в библиотеке Bservy.dll (ее можно скачать здесь). Из результата для краткости исключены стандартные методы COM и вывод для наглядности отформатирован:

(cominterface "Bservy.clsCal")

==>

( ...
  (cshow         Method       VOID    NIL)

  (intDate       Property-GET BSTR    NIL)
  (intDate       Property-LET BSTR    NIL)

  (singlDate     Property-GET BSTR    NIL)
  (singlDate     Property-LET BSTR    NIL)

  (FontSize      Property-GET REAL    NIL)
  (FontSize      Property-LET REAL    NIL)

  (MPrec         Property-GET INTEGER NIL)
  (MPrec         Property-LET INTEGER NIL)

  (SelectionMode Property-GET EMPTY   NIL)
  (SelectionMode Property-LET EMPTY   NIL)

  (ListDates     Property-GET BSTR    NIL)
  (ListDates     Property-LET BSTR    NIL)

  (MaxDates      Property-GET INTEGER NIL)
  (MaxDates      Property-LET INTEGER NIL)

  (colorUDay     Property-GET UI4     NIL)
  (colorUDay     Property-LET UI4     NIL)

  (colorHDay     Property-GET UI4     NIL)
  (colorHDay     Property-LET UI4     NIL)

  (colorSDay     Property-GET UI4     NIL)
  (colorSDay     Property-LET UI4     NIL)

  (colorIDay     Property-GET UI4     NIL)
  (colorIDay     Property-LET UI4     NIL)

  (colorMon      Property-GET UI4     NIL)
  (colorMon      Property-LET UI4     NIL)

  (colorBack     Property-GET UI4     NIL)
  (colorBack     Property-LET UI4     NIL)

  (About         Method       VOID    NIL)

  (Accept        Property-GET BOOL    NIL)

  (Title         Property-GET BSTR    NIL)
  (Title         Property-LET BSTR    NIL)
)

Видно, что свойство Accept допускает только чтение (поскольку для этого свойства не задана процедура Property-LET.

Осталось сказать несколько слов про обозначения типов значений, требуемых или возвращаемых процедурами COM. Этих типов довольно много и полное их описание читатель может найти в любом руководстве по COM-модели. Здесь отметим, что кроме описанного выше типа BSTR (UNICODE-строка с дескриптором длины) встречаются типы INTEGER (двухбайтовое целое со знаком); LONG (четырехбайтовое целое со знаком); REAL и DOUBLE (соответственно четырех- и восьмибайтовое число с плавающей точкой); BOOL (четырехбайтовое логическое; 0 - False; не нуль - True). Типы UIn - это беззнаковые целые длиной n байтов.

COMMETHOD

Функция COMMETHOD принимает два обязательных параметра: идентификатор COM-объекта и имя метода (типа STRING), а также переменное число параметров арифметического или строкового типа. Функция вызывает у объекта заданного идентификатором метод с именем, заданным вторым параметром. При этом методу передается список параметров, составленный из параметров функции COMMETHOD, начиная с третьего.

Функцию COMMETHOD следует вызывать в том случае, когда соответствующий метод не возвращает результата или возвращает результат, не являющийся ссылкой на COM-объект.

Все это иллюстрируется следующим примером:

(comCreateObject 'xlsApp "Excel.Application")

==> xlsApp

(comPropLet 'xlsApp "visible" -1)

==> T

(comMethod 'xlsApp "FindFile")

==> T

(comMethod 'xlsApp "CentimetersToPoints" 5)

==> 141.732283464567

(comMethod 'xlsApp "MethodaNet" 5)

Ошибка COM 438
Object doesn't support this property or method
==> ERRSTATE

(comMethod 'xlsApp "quit")

==> NIL

(comDestroyObject 'xlsApp)

==> T

На врезке видно, как создается приложение Microsoft Excel, делается видимым, и вызывается метод "FindFile" (этот метод не возвращает значения). Возврат T означает, что метод "FindFile" вызван успешно. Далее вызывается метод приложения, который преобразует сантиметры в пойнты. Этот метод возвращает значение. Попытка вызвать заведомо несуществующий метод вызывает ошибку. И, наконец, вызывается метод "quit" - приложение завершается.

COMMETHODO

Функция COMMETHODO предназначена для вызова метода COM-объекта, возвращающего ссылку на другой COM-объект. Функция COMMETHODO принимает три обязательных параметра: идентификатор исходного COM-объекта, имя метода (типа STRING) и чистый атом, который станет идентификатором объекта, возвращаемого методом. Аргументы метода задаются четвертым и последующими параметрами.

Вот пример вызова функции COMMETHODO:

(comCreateObject 'wdApp "Word.Application")

==> wdApp

(comPropLet 'wdApp "Visible" -1)

==> T

(comPropGetO 'wdApp "Documents" 'docs)

==> docs

(comMethodO 'docs "ADD" 'doc)

==> doc

(proplist 'wdApp)

==> (COM Lib "Word.Application" COMHANDLE 1)

(proplist 'docs)

==> (COM Lib "Word.Application" COMHANDLE 2)

(proplist 'doc)

==> (COM Lib "Word.Application" COMHANDLE 3)

Здесь создается приложение Microsoft Word, делается видимым, а затем создается объект Documents. После чего вызывается метод Add для объекта Documents, а результат (объект Document) присваивается атому doc.

COMPROPGET

Функция COMPROPGET принимает два параметра: идентификатор COM-объекта и имя свойства (арифметического, логического или строкового типа). При успешном завершении функция возвращает значение свойства. Если запрошенного свойства у объекта нет, то возбужается состояние ошибки.

Все это иллюстрируется следующим примером:

(comCreateObject 'wdApp "Word.Application")

==> wdApp

(comPropGet 'wdApp "Visible")

==> NIL

(comPropLet 'wdApp "Visible" -1)

==> T

(comPropGet 'wdApp "Visible")

==> T

(comPropGet 'wdApp "Name")

==> "Microsoft Word"

Здесь свойство Visible отображается в духе Лиспа: True - T и False - Nil.

COMPROPGETO

Назначение функции COMPROPGETO заключается в получении объектного свойства какого-либо COM-объекта. Например, у COM-объекта "Word.Application" есть свойство-объект Documents. Функция COMPROPGETO принимает три обязательных параметра: идентификатор COM-объекта, имя свойства и чистый атом, который и станет возвращенным объектом. При успешном завершении функция возвращает значение свойства. Если запрошенного свойства у объекта нет, то возбужается состояние ошибки.

Все это иллюстрируется следующим примером:

(comCreateObject 'wdApp "Word.Application")

==> wdApp

(comPropLet 'wdApp "Visible" -1)

==> T

(comPropGetO 'wdApp "Documents" 'docs)

==> docs

(comPropGet 'docs "count")

==> 0

Атом docs становится идентификатором коллекции документов приложения Word.Application. Для только что созданного приложения коллекция документов пуста (свойство Count у объекта Documents равно нулю).

COMPROPLET

Функция COMPROPLET обеспечивает присваивание свойству COM-объекта значение (арифметического, логического или строкового типа). Функция COMPROPLET принимает три обязательных параметра: идентификатор COM-объекта, имя свойства и значение, которое будет присвоено свойству, заданному вторым параметром. При успешном завершении функция возвращает T. Если запрошенного свойства у объекта нет, то возбужается состояние ошибки.

Вот пример:

(comCreateObject 'wdApp "Word.Application")

==> wdApp

(comPropLet 'wdApp "Visible" -1)

==> T

Создается приложение Microsoft Word и делается видимым, посредством присваиванию свойству Visible логического значения True (-1).


Конфигурация HomeLisp

Все настроечные параметры компонентов содержатся в конфигурационных файлах. Имена конфигурационных файлов для HomeLispIde.exe и HomeLispWeb.exe одинаковы - HomeLisp.ini. Конфигурационный файл компоненты HomeLispLib.Exe имеет имя HomeLispD.ini (что позволяет располагать библиотеку HomeLispLib.Exe в той же директории, что и среду разработки. Структура файлов сходна, но имеются особенности, которые и описываются в настоящем разделе.

Конфигурацию всех компонентов можно осуществлять как непосредственным редактированием файлов HomeLisp.ini/HomeLispD.ini, так и с использованием встроенных режимов, описанных ниже. Автор настоятельно НЕ РЕКОМЕНДУЕТ корректировать файлы HomeLisp.ini "ручками". Если пользователь испортит файл HomeLisp.ini, то он будет сам виноват в этом! Никаких претензий в этом случае автор не примет. Если же проблема возникает при работе штатного режима конфигурации, пользователь с полным правом может требовать от автора устранения ошибок.

Файлы HomeLisp.ini/HomeLispD.ini имеют простую текстовую структуру - параметры записываются в ключевой форме Имя = Значение. Каждый параметр занимает одну строку - перенос недопустим. Часть строки, стоящая правее пары символов "//" является комментарием и не учитывается.

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

При использовании библиотеки HomeLispLib.Exe, конфигурационный файл должен находиться в той директории, в которой зарегистрирована библиотека HomeLispLib.Exe.

При использовании Web-компоненты, конфигурационный файл должен находиться в той же директории, что и модуль HomeLispWeb.exe.

EXE-файлы, которые могут быть построены в среде разработки, не используют конфигурационных файлов, - все необходимые конфигурационные параметры сохраняются в теле exe-файла.

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

Имя параметра | Значение по умолчанию | Компонента | Назначение параметра
szLex | 10000 | IDE, DLL, WEB | Максимальное количество лексем S-выражения.
szOst | 2000 | IDE, DLL, WEB | Максимальный размер стека объектов.
szAst | 2000 | IDE, DLL, WEB | Максимальный размер рабочего стека.
szOut | 10 | IDE | Размер буфера консоли в Кб.
szSav | 1 | IDE | Признак сохранения размеров и положения главного окна среды разработки при завершении работы в файле HomeLisp.pos. Значение 1 означает сохранение, значение 0 запрещает сохранение размеров и положения окна. Если в текущей директории отсутствует файл HomeLisp.pos, то при запуске окно выводится в центе экрана.
Lib | .\Lib\lib-a.lsp | IDE, DLL, WEB | Путь а автозагружаемой библиотеке (файл с раширением lsp), содержащей определения всех функций и констант. Если путь к библиотеке не задан, при запуске программы выдается сообщение, а пользователю будет представлена среда разработки, содержащая только встроенные функции и константы.
tGc | 20 | IDE, DLL, WEB | Граничный процент приращения занятых информационных и списочных ячеек, для автоматического вызова сборщика мусора после завершения вычисления выражения верхнего уровня. Если значение параметра mGC не равно нулю, то сборщик мусора автоматически не вызывается и значение параметра tGC игнорируется.
mGc | 0 | IDE, DLL, WEB | Если значение параметра отлично от нуля, то сборщик мусора не вызывается автоматически.
log | 1 | IDE | Если значение параметра отлично от нуля, то при запуске автоматичски включается журналирование вводимых команд и выводимых результатов.
nest | 1 | IDE, DLL, WEB | Стиль отображения стека в дампе. Если значение параметра равно 1, стек отображается в виде [nnnnnn], где nnnnnn - текущий размер стека. Если значение параметра равно 2, стек отображается в виде последовательности точек. Если значение параметра равно 0, стек не отображается.
nWin | 100 | IDE, DLL | Маскимально число одновременно существующих графических окон.
nFil | 40 | IDE, DLL | Максимально число одновременно открытых файлов.
nBlo | 50 | IDE, DLL | Максимальное количество BLOB-ов.
nCom | 100 | IDE, DLL | Максимальное количество COM-объектов.
nDlg | 50 | IDE, DLL | Максимальное количество диалогов.
nSta | 1000 | IDE, DLL, WEB | Размер таблицы статистики (максимальное к-во функций, вызов которых будет подсчитываться при включении соответствующей возможности).
tInt | 0 | IDE, DLL, WEB | Максимальная длительность времени вычислений (в сек), по истечении которой произойдет принудительное прерывание. Если параметр равен 0, то вычисления будут продолжаться "до победного конца".
extEditor |  | IDE | Путь к внешнему редактору. Можно использовать блокнот или какую-либо его улучшенную версию.
cmdLine |  | IDE | Командная строка, которая будет подставляться при вызове из среды разработки функций SYSARGC и SYSARGV.
iBackColor | 11992307 | IDE | Цвет фона области ввода.
oBackColor | 8404992 | IDE | Цвет фона области вывода
iForeColor | 8404992 | IDE | Цвет текста области ввода
oForeColor | 16777215 | IDE | Цвет текста области вывода
iFontName | Courier New | IDE | Гарнитура шрифта области ввода
iFontSize | 9.75 | IDE | Размер шрифта области ввода
iFontBold | 0 | IDE | Признак жирности начертания символов области ввода
iFontItalic | 0 | IDE | Признак курсива области ввода
oFontName | Courier New | IDE | Гарнитура шрифта области вывода
oFontSize | 12 | IDE | Размер шрифта области вывода
oFontBold | 1 | IDE | Признак жирности шрифта области вывода
oFontItalic | 0 | IDE | Признак курсива области вывода
Inst | 2 | WEB | Максимальное количество запускаемых процессов HomeLispWeb.exe. Это число следует сделать на 1-2 больше, чем предполагаемое количество рабочих мест.
Конфигурирование среды разработки

Вызов режима конфигурирования среды разработки осуществляется выбором пункта меню "Настроить" или нажатием на 19-ю кнопку (с изображением инструментов). При этом будет вызвана форма настройки, показанная ниже:

Интерфейс настройки инуитивно понятен (по крайней мере, так полагает разработчик...). Пользователь должен задать нужные параметры и нажать кнопку "Принять" (на ней изображена галочка). После чего форма закроется, а пользователю будет выдано сообщение:

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

Если же пользователь решил не менять параметры, то следует нажать кнопку "Закрыть" (с изображением крестика). Форма конфигурации закроется, а изменения приняты не будут.

Конфигурирование библиотеки HomeLispLib.Exe

Для вызова формы настройки библиотеки HomeLispLib.Exe необходимо создать объект clsLisp, и вызвать метод Config. Для этого служит скрипт DllConfig.vbs, входящий в стандартную поставку. Текст скрипта (на языке VBScript) приводится ниже:

    Set Lisp=CreateObject("HomeLisp.clsLisp")

    If Lisp.Config Then

       MsgBox "Конфигурация успешно исправлена"

    Else

       MsgBox "Конфигурация НЕ изменилась"

    End if

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

Если пользователь изменил какие-либо параметры и нажал кнопку "Принять", то выдается сообщение "Конфигурация успешно исправлена", а значения всех параметров будут занесены в файл HomeLispD.ini. Cтарый файл сохранится под именем HomeLispD.bak. Если же пользователь нажмет кнопку "Закрыть", то конфигурация не изменится и будет выдано сообщение "Конфигурация НЕ изменилась".


Описание отладчика

Описание отладчика HomeLisp

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

Реализуюя свой отладчик, разработчик HomeLisp ставил перед собой достаточные скромные задачи: нужно, чтобы можно было "отловить" вызов функции и изменение значения символа. "Отловить" - значит остановить вычисления и увидеть состояние текущих контекстов. Кроме этого желательна возможность выполнить "локальные вычисления" в точке останова. Необходима также возможность продолжения останвленных вычислений. Все побочные эффекты отлаживаемого кода (включая вывод функциями PRINT и родственными ей) не должны нарушаться.

Насколько все это удалось реализовать судить читателю и пользователю HomeLisp.

О г л а в л е н и е
Запуск и остановка отладчика.
Задание точки останова при входе в функцию
Вычисления в текущем контексте
Задание точки останова при изменении значения символа

Запуск и остановка отладчика

Отладчик HomeLisp доступен только при работе в IDE. Активизировать отладчик можно двумя способами: вызовом функции DEBUG или посредством нажатия кнопки на панели инструментов (выбора соответствующего пункта главного меню). Кнопка вызова отладчика на панели инструментов IDE показана ниже:

Главное окно отладчика имеет вид:

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

Тулбар содержит семь кнопок. Четвертая, пятая и шестая кнопки при начале работы с отладчиком изначально неактивны. Ниже тулбара размещается многострочная область ввода. Если ввести в эту область ввода S-выражение, то четвертая и пятая кнопки тулбрара станут активными:

Шестая кнопка тулбара бывает активной только в процессе вычисления. Ниже назначение всех кнопок тулбара будет описано более подробно.

Кнопка | Выполняемое действие
 | Задать точку останова при входе в заданную функцию.
 | Задать точку останова при изменении значения заданного символа.
 | Вычислить произвольное вводимое S-выражение в текущем контексте.
 | Выполнить один шаг (одно вычисление EVAL) и остановиться.
 | Вычислять в автоматическом режиме. Если предварительно не было задано точек останова, то вычисления остановятся либо при возникновении ошибки, либо когда исходное S-выражение будет вычислено.
 | Остановить вычисления. В режиме ожидания кнопка неактивна.
 | Закрыть отладчик и вернуться в среду разработки.

Для завершения работы с отладчиком следует нажать кнопку с изображением черной стрелки; окно отладки закроется и отобразится главное окно среды разработки. Если запущен процесс выполнения в автоматическом режиме, то его можно прервать, нажав кнопку прерывания (на ней изображены две вертикальные стрелки).

Задание точки останова при входе в функцию

Для задания точек останова при входе в нужные функции следует щелкнуть по первой слева кнопке тулбара. Будет отображено окно задания точки останова:

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

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

Установим точку останова при входе в функцию DIFFERENCE, выйдем из режима установки точек останова и запустим выполнение в автоматическом режиме (нажав соответствующую кнопку). Через секунду увидим следующую картинку:

Что означает эта картинка? Произошёл останов при входе в DIFFERENCE. Текущий лексический контекст содержит переменную x со значением 3. Если еще раз нажать кнопку автоматического выполнения, то будет произведен останов при выходе из DIFFERENCE, результат вычисления будет равен, естественно, двум:

Таким образом, можно "прощелкать" все вызовы функций.

Вычисления в текущем контексте

Когда вычисления остановлены, пользователь может выполнить любые вычисления в текущих контекстах (лексическом и глобальном). Для этого следует сначала щелкнуть по третьей кнопке тулбара главной формы отладчика. Будет отображено окно "Вычислить в текущем контексте". На этом окне расположен тулбар с тремя кнопками, область ввода выражения и область ответа. Пользователь вводит S-выражение в область ввода и щелкает по первой кнопке тулбара ("Вычислить"). В области ответа появляется результат:

Легко убедиться, что результат верен (вычисления велись в лексическом контексте, в котором x=2. Вторая кнопка тулбара окна "Вычислить в текущем контексте" служит для очистки областей ввода и результата.

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

Выйти из режима вычислений в текущем контексте (нажав последнюю кнопку тулбара с черной стрелкой), то можно убедиться, что вычисления могут быть продолжены. Разумеется, это справедливо только в случае, если вычисляемое в текущем контексте S-выражение не меняет сам контекст. В этом случае последствия ошибки труднопредсказуемы...

Задание точки останова при изменении значения символа

Другая возможность отладчика обеспечивает останов вычислений при изменении значения заданного символа. Чтобы задействовать эту возможность следует щелкнуть по второй кнопке тулбара главной формы отладчика (на кнопке изображен котёнок). Будет отображено окно "Наблюдение":

Чтобы вызвать останов вычислений при изменении значения символа следует задать этот символ в поле ввода "Добавляемый символ" и нажать кнопку с красным плюсом. Символ будет перенесен из области ввода в список символов (расположенный ниже). При попытке добавить символ повторно добавления не происходит, а выводится предупреждение.

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

При останове доступна возможность вычисления в текущем контексте (описанная выше).

Для примера рассмотрим вычисление S-выражения:

(prog (i) (for i 1 5 ((printline i))))

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


Работа в конструкторе диалогов.

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

В этом разделе описывается работа со встренным конструктором диалогов (экранным дизайнером) HomeLisp. Последовательность действий по созданию визуального интерфейса в HomeLisp довольно схожа с работой в таких системах, как Visual Basic, VBA или Deplphi. Разумеется, возможности и событийная модель HomeLisp, описанная здесь, заметно скромнее, чем у перечисленных выше профессиональных систем разработки. Тем не менее, даже эти скромные возможности вполне позволяют создать простой графический интерфейс для программ, написанных на HomeLisp.

Основой графического пользовательского интерфейса применительно к HomeLisp является т.н. диалог - модальная форма, содержащая элементы управления. Элементами управления являются:

Надпись;

Поле ввода;

Простой список;

Выпадающий список;

Командная кнопка;

Радиокнопка;

Галочка.

Событийная модель HomeLisp очень проста: с каждым элементом управления связан один тип событий. С полем ввода (TextBox) связано событие KeyPressed, заключающееся в том, что пользователь ввел в поле ввода символ. С остальными элементами управления связано событие Click (щелчок мыши). Процедура-обработчик события для поля ввода получает, как параметр, ASCII-код клавиши, нажатой пользователем. Обработчики события Click не получают параметров.

В экранном дизайнере диалог может быть "нарисован" мышью (при минимальном участии клавиатуры). Дизайнер также генерирует заглушки процедур-обработчиков событий для каждого из элементов управления. Содержательную часть кода процедур-обработчиков событий должен написать человек.

Сверстанный диалог можно сохранить в текстовом файле с расширением ".dlg" (и впоследствии восстановить с целью продолжения работы). Полностью готовый диалог следует "протранслировать" в Лисп-код, после чего диалог готов для выполнения.

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

или пункт главного меню "Диалоги". Окно экранного дизайнера выглядит так:

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

Опишем органы управления дизайнером чуть подробнее. Командная панель (тулбар) дизайнер имеет вид:

Кнопки командной панели (перечисленные слева направо) предназначены для:

Загрузить диалог сохраненный ранее в файле .dlg;

Сохранить диалог в файле формата .dlg;

Вырезать выделенный текст и поместить его в буфер обмена (кнопка активна только в режиме корректировки программного кода);

Скопировать выделенный текст и поместить его в буфер обмена (кнопка активна только в режиме корректировки программного кода);

Вставить текст из буфера обмена с текущего положения курсора (кнопка активна только в режиме корректировки программного кода и при непустом буфере обмена);

Вставить стандартный шаблон вызова функции с текущего положения курсора (кнопка активна только в режиме корректировки программного кода);

Сгенерировать Лисп-код диалога (включает программное создание диалога, добавление органов управления и назначение обработчиков событий);

Выйти из конструктора диалогов и вернуться в среду разработки.

Панель инструментов конструктора показана на отдельном рисунке ниже:

Два ряда кнопок в верхней части панели служат для выбора "рисуемого" элемента управления. При этом самая левая кнопка в первом ряду (с изображением стрелки) служит для отказа от выбора типа. Активная в данный момент кнопка обводится синей рамкой.

Ниже в отдельной рамке располагается двухкнопочный переключатель свойства/события. Этот переключатель активен только если на форме имеется хотя бы один элемент управления.

Еще ниже располагается выпадающий список объектов диалога. Каждому элементу управления дается уникальное стандартное имя и оно добавляется в выпадающий список объектов. Если пользователь удалит элемент управления, его имя автоматически удалится из списка объектов диалога.

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

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

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

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

После того, как пользователь отпустит левую клавишу мыши, рамка исчезнет, а на месте рамки появится соответствующий элемент управления:

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

Описанным выше образом, размещаем на форме диалога еще одно поле ввода (для результата) и две командные кнопки. Вид полученного диалога приводится ниже:

Пусть читатель обратит внимание, что в выпадающй список, расположенный между палитрой инструментов и окном свойств, добавляются имена выбранных элементов управления. Первое поле ввода будет называться _TXT_1, второе - _TXT_2 и т.д. Диалогу тоже присваивается имя _DLG_. В поля ввода при их создании автоматически заносится текст Поле_ввода_1, Поле_ввода_2 и т.д. Аналогично, на командных кнопках формируются надписи Кнопка-1, Кнопка-2 и т.д.

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

Аналогичным образом очищается второе поле ввода, а на кнопки наносятся надписи "Вычислить" и "Закрыть" соответственно:

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

Щелкаем по этой кнопке, - появится форма выбора иконки:

Выбираем нужную иконку, нажимаем кнопку "Принять", - и диалог принимает окончательный вид:

Теперь необходимо назначить кнопкам процедуры-обработчики событий. Начнем с кнопки "Закрыть". Выделяем эту кнопку щелчком мыши, а затем щелкаем по кнопке с изображением желтой молнии. Открывается окно редактирования кода:

В теле функции-обработчика события пишем вызов DLGHIDE:

Щелкаем по кнопке с изображением руки (переходим от редактирования кода к дизайну), выбираем кнопку "Вычислить" и, щелкнув по желтой молнии, открываем ее обработчик. В этом обработчике пишем следующее:

Тело функции-обработчика представляет собой PROG-конструкцию. Используются две локальные (связанные) переменные x и y. В переменую x заносится текст, введенный пользователем в первое поле ввода. Чтобы предотвратить возможные ошибки, вычисление факториала заключается в TRY-EXCEPT-группу. Поскольку функция fact требует аргумента типа FIXED, то используется функция STR2FIX. Если при вычислении факториала произойдет любая ошибка, переменная y получит значение "Ошибка при вычислении". В любом случае, значение переменной y заносится в текстовое поле _TXT_2.

Диалог готов. Перед тем, как генерировать Лисп-код, диалог рекомендуется сохранить. Для этого следует выбрать пункт главного меню "Сохранить" или нажать кнопку на панели инструментов с изображением дискеты. Откроется окно "Сохранить как":

Выбираем имя test и нажимаем кнопку сохранить. Выдается сообщение "Диалог сохранен". Теперь следует сгенерировать Лисп-код нашего диалога. Для этого нажимаем седьмую кнопку панели инструментов (или выбираем пункт главного меню "Сгенерировать"). Снова появится окно "Сохранить как":

Задаем имя test и нажимаем кнопку "Сохранить". Выдается сообщение "Лисп-код сгенерирован", а в выбранной директории появится файл test.lsp, содержащий полный код диалога. При желании можно посмотреть Лисп-код. Он будет выглядеть так:

//
//  Программа отображения диалога _Dlg_
//

(prog nil

      (dlgCreate '_Dlg_ 274 157 "")

      (dlgAddControl '_Dlg_ '_TXT_1 _TEXT 11 8 157 24 '("Tahoma" 8 1 0 0) "" 0 &H80000008 &H80000005)

      (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 176 3 81 35 '("Tahoma" 8,25 1 0 0) "Вычислить")

      (dlgAddControl '_Dlg_ '_TXT_2 _TEXT 11 46 246 36 '("Tahoma" 8 1 0 0) "" 0 &H80000008 &H80000005)

      (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 51 86 189 42 '("Tahoma" 8,25 1 0 0) "Закрыть")

      (dlgPutPicture '_BUT_2 36)

      //
      // Обработчик события CLICK для кнопки _BUT_2
      //

      (defun _BUT_2_Click  Nil

          (dlgHide '_DLG_)

      )

      //
      //   Назначение процедуры-события _BUT_2_Click  контролу _BUT_2
      //

      (dlgSetEvent '_BUT_2 '_BUT_2_Click )

      //
      // Обработчик события CLICK для кнопки _BUT_1
      //

      (defun _BUT_1_Click  Nil

                  (PROG (x y)

                     (setq x (dlgGetText '_Txt_1))

                     (TRY

                           (setq y (fact (str2fix x)))

                      EXCEPT

                          (setq y "Ошибка при вычислении")

                      )

                     (dlgPutText '_Txt_2 y)

                  )

      )

      //
      //   Назначение процедуры-события _BUT_1_Click  контролу _BUT_1
      //

      (dlgSetEvent '_BUT_1 '_BUT_1_Click )

      //
      //   Отображение диалога _Dlg_
      //

      (dlgShow '_Dlg_)
)

Можно попытаться исполнить сгенерированный код. Для этого выходим из дизайнера (нажав кнопку "Выход"), и пробуем загрузить и выполнить файл test.lsp. На экране появляется спроектированная выше форма. Вводим в первое поле ввода 10 и нажимаем кнопку Вычислить:

Диалог вполне работоспособен. Если ввести в поле ввода отрицательное число, то будет выдано сообщение об ошибке:

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

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


Описание функций организации диалогов

Разработка программ с графическим пользовательски интерфейсом в HomeLisp-е основано на понятии диалога. С точки зрения пользователя, диалог - это окно Windows, в котором размещены элементы управления, которым назначены обработчики событий (типа нажатия кнопки, выбор элемента списка, включения/выключения радиокнопки и т.д.) С точки зрения ядра HomeLisp-a диалог - это специальным образом созданный атом, имеющий в своем списке свойств флаг DIALOG. Аналогично, элемент управления с точки зрения ядра HomeLisp-a - это специальным образом созданный атом, имеющий в своем списке свойств флаг CONTROL. Эти флаги (DIALOG и CONTROL) добавляются в список свойств атома функциями DLGCREATE и DLGADDCONTROL соответственно. Атом с флагом DIALOG в списке свойств будем называть идентификатором диалога. Соответственно, атом с флагом CONTROL в списке свойств будем называть идентификатором контрола.

Атом, не имеющий списка свойств, будем называть чистым атомом.

Для разработки программы с графическим интерфейсом необходимо:

создать диалог (вызвав функцию DLGCREATE)
разместить на диалоговой форме нужные элементы управления (вызвав нужное число раз функцию DLGADDCONTROL)
при необходимости присвоить свойствам элементов управления нужные значения (для этого служат функции DLGADDITEM, DLGPUTLIST, DLGPUTPICTURE, DLGPUTTEXT)
при необходимости назначить элементам управления обработчики событий (вызвав функцию DLGSETEVENT)
отобразить диалог (вызвав функцию DLGSHOW)

Для скрытия диалога (без разрушения его внутренних структур и с возможностью повторного отображения) служит функция DLGHIDE. После того, как диалог станет ненужным, его следует удалить, вызвав функцию DLGDESTROY. Процедуры - обработчики событий должны быть написаны на HomeLisp-е. Для получения измененных пользователями свойств элементов управления в процедурах - обработчиках событий могут использоваться функции DLGCHKOPT, DLGGETLIST и DLGGETTEXT. Разумеется, эти функции могут использоваться и не в процедурах - обработчиках (если это имеет смысл). При необходимости сделать элемент управления невидимым (или проявить невидимый элемент управления) следует вызывать функцию DLGCNTHIDESHOW.

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

В предлагаемой версии HomeLisp поддерживаются следующие типы элементов управления: командные кнопки, текст-боксы, лист-боксы (списки), комбо-боксы, радиокнопки, чек-боксы (галочки). Тип элемента управления (контрола) задается числом типа FIXED. Для всех контролов в стандартной библиотеке имеется набор констант, приведенный в следующей таблице:

Константа | Описание
_LABEL = 1 | Задает метку (надпись)
_TEXT = 2 | Задает поле ввода (текстбокс)
_LIST = 3 | Задает простой список
_COMBO = 4 | Задает выпадающий список (комбо-бокс)
_BUTTON = 5 | Задает командную кнопку
_CHECK = 6 | Задает чек-бокс (галочку)
_OPTION = 7 | Задает радио-кнопку

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

("Имя" размер b i u)

Здесь "Имя" - строка, задающая имя фонта (например, "Tahoma" или "Courier"). Размер задает размер шрифта в пунктах. b i u - признаки стиля Bold, italic и underline соответственно. Значение 1 означает наличие признака 0 - отсутствие.

Событийная модель HomeLisp-a предельно проста: каждый вид контрола позволяет обработать единственный вид события. Это событие для кнопки, чек-бокса, радиокнопки, списка, комбо-бокса вызывается кликом мыши или клавишей Enter, а для текст-бокса нажатием клавиши.

Имена всех диалоговых функций имеют префикс DLG. Диалоговые функции доступны во всех режимах работы, кроме, естественно, WEB-компоненты.

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

Важное предупреждение

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

Список функций управления диалогами
Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
DLGADDCONTROL | 10 | 1-2 - ATOM; 3-7 - FIXED; 8 - LIST; 9-STRING; 10 - FIXED | Добавляет к диалогу, заданному 1-м аргументом, контрол, задаваемый вторым аргументом. Третий аргумент задает тип контрола; 4-5 аргументы задают координаты левого верхнего угла; 6-7 аргументы задают ширину и высоту; 8-й аргумент задает фонт; 9-й - надпись; 10-й - выравнивание.
DLGADDITEM | 2 | 1- ATOM, 2-STRING | Заносит в лист-бокс или комбо-бокс элемент, задаваемый вторым аргументом
DLGCLEARLIST | 1 | ATOM | Чистит лист-бокс или комбо-бокс, задаваемый аргументом
DLGCNTHIDESHOW | 1 | ATOM | Скрывает/отображает контрол, заданный аргументом
DLGCREATE | 4 | 1-ATOM; 2-3-FIXED; 4-STRING | Создает диалог (форму) с шириной и высотой, задаваемой 2 и 3 параметрами и заголовком, задаваемым 4-м параметром. Первый параметр (атом) получает в список свойств флаг DIALOG.
DLGDELITEM | 2 | 1-ATOM; 2-STRING/FIXED | Удаляет из списка или выпадающего списка (заданного первым параметром) элемент по значению или по номеру
DLGDESTROY | 1 | ATOM | Уничтожает диалог, заданный параметром
DLGGETCHKOPT | 1 | ATOM | Получает статус чек-бокса или радиокнопки, заданной единственным параметром.
DLGGETLIST | 2 | 1-ATOM; 2-FIXED | Первый параметр должен задавать контрол (лист-бокс или комбо-бокс). Для лист-бокса при значении второго параметра -1, возвращается список выделенных элементов; при значении 1, возвращаются номера выделенных элементов; при прочих значениях возвращается активный элемент. Для комбо-бокса всегда возвращается активный элемент.
DLGGETTEXT | 1 | ATOM | Для текстбокса, заданного аргументом, возвращается текст из поля ввода.
DLGHIDE | 1 | ATOM | Скрывает диалог, заданный аргументом
DLGPUTLIST | 2 | 1-ATOM; 2-LIST | В комбо-бокс или лист-бокс, заданный первым аргументом, заносится список, заданный вторым аргументом. Старое содержимое контрола стирается.
DLGPUTPICTURE | 2 | 1-ATOM; 2-FIXED | У кнопки, заданной первым аргументом, устанавливается иконка с номером, заданным вторым аргументом.
DLGPUTTEXT | 2 | 1-ATOM; 2-STRING | Для кнопки, метки, текст-бокса, чек-бокса и радиокнопки, заданной первым аргументом, устанавливается текст, заданный вторым аргументом.
DLGSETEVENT | 2 | 1-ATOM; 2-EXPR | У контрола, заданного первым аргументом, устанавливается процедура-событие, задаваемая вторым аргументом.
DLGSHOW | 3 | ATOM, [FIXED,FIXED] | Отображает диалог, заданный аргументом
DLGSHOWEVENT | 1 | ATOM | Возвращает имя функции-обработчика события
DLGADDCONTROL

Функция DLGADDCONTROL добавляет контрол к диалогу. Функция принимает десять параметров.

Первым параметром является идентификатор диалога.

Вторым параметром должен быть чистый атом (он при успешном завершении станет идентификатором контрола).

Третий параметр задает тип контрола - число типа FIXED из диапазона 1-7 (см. таблицу типов контролов).

Четвертый и пятый параметры задают координаты левого верхнего угла контрола (в пикселах) относительно левого верхнего угла формы диалога.

Шестой и седьмой параметры задают ширину и высоту контрола (в пикселах).

Восьмой параметр задает шрифт контрола (см. задание фонта в виде списка).

Девятый параметр задает текст (типа STRING). Этот параметр задается для меток, текст-боксов, кнопок, чек-боксов, радиокнопок. Для списков и комбо-боксов параметр игнорируется (можно поставить Nil). Для меток, кнопок, чек-боксов, радиокнопок параметр задает надпись, а для текст-боксов - изначальный текст в поле ввода.

Деcятый параметр задает выравнивание надписи. (Число типа FIXED из диапазона 0 - 2; можно использовать константы _LEFT=0, _RIGHT=1, _CENTER=2 . Этот параметр имеет смысл задавать для текст-боксов, чек-боксов и радиокнопок. Для остальных параметр игнорируется (можно задать Nil).

При успешном завершении функция возвращает идентификатор добавленного контрола.

Вот пример вызова функции:

(dlgCreate 'd-1 200 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _BUTTON
               10 10 100 40
               '("Tahoma" 8.25 1 0 1)
               "Кнопочка" Nil)

 ==> 'c-1

(proplist 'c-1)

==> (CONTROL OWNER d-1 Type        5
                       Left       10
                       Top        10
                       width     100
                       height     40
                       Font      ("Tahoma" 8.25 1 0 1)
                       ForeColor NIL
                       BackColor -2147483633
                       TEXT      "Кнопочка"
                       ALIGN     0
    )

(dlgShow 'd-1)

Здесь создается диалог и добавляется командная кнопка. Можно увидеть, как свойства кнопки, задаваемые при ее создании, располагаются в списке свойств. Если выполнить приведенные команды в среде разработки, то пользователь увидит:

Поскольку не предусмотренно никаких средств по скрытию диалога, то для закрытия диалога придется нажать кнопку "Прервать".

Если "погасить" диалог и выполнить приведенный ниже код:

(putprop 'c-1 'BackColor _WHITE)

==> c-1

(putprop 'c-1 'Font '("Times" 12 1 1 0))

==> c-1

(dlgShow 'd-1)

То пользователь увидит, что характеристики командной кнопки изменились:

DLGADDITEM

Функция DLGADDITEM позволяет добавить в лист-бокс или комбо-бокс элемент списка. Старое содержимое списка при этом не стирается. Вызов этой функции в цикле позволяет заполнить/дополнить список нужным содержанием. Другой способ формирования списка дает функция DLGPUTLIST.

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

Если контрол, заданный первым параметром, не является списком или комбо-боксом, то возбуждается состояние ошибки.

Вот пример заполнения списка:

(dlgCreate 'd-1 200 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _LIST
               10 10 60 80
               '("Tahoma" 8.25 1 0 0)
               nil _LEFT)

 ==> c-1

(dlgAddItem 'c-1 "Мышка")

 ==> c-1

(dlgAddItem 'c-1 "Кошка")

 ==> c-1

(dlgAddItem 'c-1 "Жучка")

 ==> c-1

(dlgAddItem 'c-1 "Внучка")

 ==> c-1

(dlgAddItem 'c-1 "Бабка")

 ==> c-1

(dlgAddItem 'c-1 "Дедка")

 ==> c-1

(dlgShow 'd-1)

Здесь создается диалог, добавляется список, который заполняется значениями. Если выполнить приведенные команды в среде разработки, то пользователь увидит:

Поскольку не предусмотренно никаких средств по скрытию диалога, то для закрытия диалога придется нажать кнопку "Прервать".

DLGCLEARLIST

Функция DLGCLEARLIST очистить список или комбо-бокс. Функция требует единственный обязательный параметр - идентификатор контрола (CONTROL). Этот контрол должен быть списком или комбо-боксом, в противном случае возбуждается состояние ошибки.

Вот развернутый пример вызова DLGCLEARLIST:

(defun clr nil (dlgClearList 'c-1))

 ==> clr

(dlgCreate 'd-1 200 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _LIST
               10 10 60 80
               '("Tahoma" 8.25 1 0 0)
               nil _LEFT)

 ==> c-1

(dlgAddControl 'd-1 'c-2 _BUTTON
               90 10 100 40
               '("Tahoma" 8.25 1 0 1)
               "Очистить" Nil)

 ==> c-2

(dlgSetEvent 'c-2 'clr)

 ==> clr

(dlgAddItem 'c-1 "Мышка")

 ==> c-1

(dlgAddItem 'c-1 "Кошка")

 ==> c-1

(dlgAddItem 'c-1 "Жучка")

 ==> c-1

(dlgAddItem 'c-1 "Внучка")

 ==> c-1

(dlgAddItem 'c-1 "Бабка")

 ==> c-1

(dlgAddItem 'c-1 "Дедка")

 ==> c-1

(dlgShow 'd-1)

Здесь создается функция clr, которая чистит список контрола c-1. Пусть читатель обратит внимание, что эта функция может быть зоздана до создания самого контрола. Далее создается диалог и к нему добавляется список командная кнопка. Затем кнопке в качестве обработчика события устанавливается функция clr, список заполняется данными и диалог отображается. Пользователь увидит следующее:

После нажатия кнопки список будет очищен:

DLGCNTHIDESHOW

Функция DLGCNTHIDESHOW скрывать и отображать контрол. Функция действует, как двоичный переключатель: первый вызов скрывает контрол, второй - отображает. Функция требует единственный параметр типа CONTROL.

Вот пример использования этой функции:

(defun HideShow nil (dlgCntHideShow 'c-1))

 ==> HideShow

(dlgCreate 'd-1 200 100 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _LABEL 10 10 60 80 '("Tahoma" 10 1 0 0) "Надпись" _LEFT _RED)

 ==> c-1

(dlgAddControl 'd-1 'c-2 _BUTTON 90 10 100 40 '("Tahoma" 9 1 0 1) "Нажми" Nil)

 ==> c-2

(dlgSetEvent 'c-2 'HideShow)

 ==> HideShow

(dlgShow 'd-1)

Создается функция HideShow, которая будет скрывать/проявлять контрол c-1. Затем создается диалог и на нем размещается метка и кнопка. Затем функция HideShow устанавливается, как обработчик события для кнопки. После выполнения приведенного кода пользователь увидит:

После нажатия кнопки надпись исчезнет:

DLGCREATE

Функция DLGCREATE создает диалог. Эта функция требует четыре обязательных параметра. Первый параметр должен быть чистым атомом (после успешного завершения этот атом станет идентификатором диалога). Второй и третий параметры (оба типа FIXED) задают ширину и высоту окна диалога в пикселах. Последний параметр (типа STRING) задает надпись в заголовке формы.

При успешном завершении функция возвращает идентификатор диалога. Следует обратить внимание, что создание диалога не означает его отбражения. Для отображения созданного диалога служит функция DLGSHOW.

(dlgCreate 'd-1 200 200 "Диалог")

==> d-1

(proplist 'd-1)

==> (DIALOG width 200 height 200 title "Диалог" DLGHANDLE 1)

Видно, что после успешного создания диалога, у атома-идентификатора создался список свойств.

Если изменить значения свойств width, height или title, то изменятся значения ширины, высоты и текста заголовка окна диалога. Свойство DLGHANDLE модифицировать нельзя; попытка изменить это свойство вызовет ошибку. Ниже приводится пример диалога с командными кнопками, нажатие на которые изменяет ширину окна диалога:

//
//  Программа отображения диалога _Dlg_
//

(prog nil

      (dlgCreate '_Dlg_ 170 195 "Модификация")

      (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 13 11 133 42 '("Tahoma" 8,25 1 0 0) "Шире...")

      (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 13 58 133 43 '("Tahoma" 8,25 1 0 0) "Уже...")

      (dlgAddControl '_Dlg_ '_BUT_3 _BUTTON 12 111 134 41 '("Tahoma" 8,25 1 0 0) "Закрыть")

      //
      // Обработчик события CLICK для кнопки _BUT_1
      //

      (defun _BUT_1_Click  Nil

           (prog (x)

             (setq x (getprop '_dlg_ 'width))
             (setq x (+ x 50))
             (putprop '_dlg_ 'width x)

           )

      )

      //
      //   Назначение процедуры-события _BUT_1_Click  контролу _BUT_1
      //

      (dlgSetEvent '_BUT_1 '_BUT_1_Click )

      //
      // Обработчик события CLICK для кнопки _BUT_3
      //

      (defun _BUT_3_Click  Nil

          (prog nil
            (dlgHide '_dlg_)
            (dlgDestroy '_dlg_)
            (gc)
          )
      )

      //
      //   Назначение процедуры-события _BUT_3_Click  контролу _BUT_3
      //

      (dlgSetEvent '_BUT_3 '_BUT_3_Click )

      //
      // Обработчик события CLICK для кнопки _BUT_2
      //

      (defun _BUT_2_Click  Nil

           (prog (x)

             (setq x (getprop '_dlg_ 'width))
             (setq x (- x 50))

             (cond ((<= x 160) (return nil)))

             (putprop '_dlg_ 'width x)

           )

      )

      //
      //   Назначение процедуры-события _BUT_2_Click  контролу _BUT_2
      //

      (dlgSetEvent '_BUT_2 '_BUT_2_Click )

      //
      //   Отображение диалога _Dlg_
      //

      (dlgShow '_Dlg_)
)

Если выполнить приведенный выше программный код, то будет отображен следующий следующий диалог:

При нажатии на кнопку "Шире" ширина окна диалога увеличится:

При нажатии на кнопку "Уже" ширина окна диалога уменьшится.

DLGDELITEM

Функция DLGDELITEM требует два параметра. Первый параметр должен задавать контрол типа LISTBOX или COMBOBOX. Второй параметр может быть строкой или целым числом. Если задана строка, то из внутреннего списка LISTBOX/COMBOBOX будет удален первый элемент, совпадающий с заданной строкой. Если такого элемента не существует, список не изменится.

Если второй параметр есть число FIXED, то будет произведена попытка удаления элемента с заданным номером из внутреннего списка контрола. В этом случае значение числа должно лежать в отрезке [0 , (к-во элементов списка-1)]. Если число выходит за пределы этого отрезка, будет возбуждено состояние ошибки.

Все сказанное демонстрируется примером:

//
//  Программа отображения диалога _Dlg_
//

(prog nil

      (dlgCreate '_Dlg_ 254 392 "Работа со списками")

      (dlgAddControl '_Dlg_ '_CMB_1 _COMBO 18 18 217 24 '("Tahoma" 9 1 0 0)
                     &H80000012 &H8000000A)

      (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 16 56 216 51 '("Tahoma" 9 1 0 0)
                     "Показать")

      (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 16 110 216 51 '("Tahoma" 9 1 0 0)
                     "Очистить весь список")

      (dlgAddControl '_Dlg_ '_BUT_3 _BUTTON 16 165 216 51 '("Tahoma" 9 1 0 0)
                     "Удалить элемент")

      (dlgAddControl '_Dlg_ '_BUT_4 _BUTTON 16 221 216 51 '("Tahoma" 9 1 0 0)
                     "Закрыть")

      (dlgAddControl '_Dlg_ '_OPT_1 _OPTION 18 287 214 22 '("Tahoma" 9 1 0 0)
                     "Показать все" 0 &H80000012 &H8000000F -1)

      (dlgAddControl '_Dlg_ '_OPT_2 _OPTION 18 320 214 21 '("Tahoma" 9 1 0 0)
                     "Показать выбранное" 0 &H80000012 &H8000000F)

      (defun clr nil (dlgClearList '_CMB_1))

      (defun exi nil (prog nil (dlgHide '_Dlg_) (dlgDestroy '_Dlg_)))

      (defun sho nil
             (say (output
                    (prog nil
                       (cond ((= (dlgGetChkOpt '_OPT_1) 1)
                              (return (dlgGetList '_CMB_1 2)))
                             (t  (return (dlgGetList '_CMB_1 0)))
                    )
                  )
             )
       )
      )

      //(defun del nil (dlgDelItem '_CMB_1 (car (dlgGetList '_CMB_1 -1))))

      (defun del nil (dlgDelItem '_CMB_1 0))

      //
      // Назначение обработчиков событий
      //

      (dlgSetEvent '_BUT_1 'sho)

      (dlgSetEvent '_BUT_2 'clr)

      (dlgSetEvent '_BUT_3 'del)

      (dlgSetEvent '_BUT_4 'exi)

      (dlgPutList '_CMB_1 '("Мышка" "Кошка" "Жучка" "Внучка" "Бабка" "Дедка"))

      //
      //   Отображение диалога _Dlg_
      //

      (dlgShow '_Dlg_)

)

Если выполнить приведенный код, то будет выведен диалог:

то, нажимая кнопки "Удалить" и "Показать", можно убедиться, что комбо-бокс "теряет" элемент за элементом. Попытка удалить элемент из пустого комбо-бокса вызовет ошибку.

DLGDESTROY

Функция DLGDESTROY уничтожает диалог (освобождает память, занятую внутренними структурами и очищает список свойств идентификатора диалога, так что атом-идентификатор вновь становится чистым). Функция принимает единственный параметр - идентификатор диалога (тип CONTROL). При успешном завершении функция возвращает T.

Все сказанное демонстрируется примером:

(dlgCreate 'd-1 200 200 "Диалог")

==> d-1

(proplist 'd-1)

==> (DIALOG width 200 height 200 title "Диалог" DLGHANDLE 1)

(dlgDestroy 'd-1)

==> T

(proplist 'd-1)

==> NIL

Видно, что после создания диалога атом-идентификатор получает список свойств, а после уничтожения диалога список свойств очищается.

DLGCHKOPT

Функция DLGCHKOPT опрашивает статус чек-бокса или радиокнопки. Функция принимает параметр типа CONTROL и возвращает единицу если чек-бокс или радиокнопка активизированы и нуль в противном случае.

Вот пример использования Функции DLGCHKOPT:

(dlgCreate 'd-1 200 200 "Диалог")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _CHECK 8 8 175 50
               '("FixedSys" 8 0 0 0) "Что-либо...")

 ==> c-1

(dlgAddControl 'd-1 'c-2 _CHECK 8 58 175 50
               '("FixedSys" 8 0 0 0) "Еще что-то...")

 ==> c-2

(dlgAddControl 'd-1 'b-2 _BUTTON 8 130 175 40
               '("Tahoma" 9 1 1 0) "Опросить")

 ==> b-2

(defun d-1_Click Nil (say (strcat "Chk1="
                                  (fix2str (dlgGetChkOpt 'c-1))
                                  " "
                                  "Chk2="
                                  (fix2str (dlgGetChkOpt 'c-2))
                           )
                      )
)

 ==> d-1_Click

(dlgSetEvent 'b-2 'd-1_Click)

 ==> d-1_Click

(dlgShow 'd-1)

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

а затем нажать кнопку "Опросить", то пользователь увидит:

DLGGETLIST

Функция DLGGETLIST предназначена для получения списка выбранных пользователем элементов для списка (лист-бокса) или комбо-бокса. Функция принимает два параметра: идентификатор контрола (CONTROL) и индикатор (типа FIXED). Функция работает так:

Для списка при значении индикатора, равному 1 возвращается список выбранных (отмеченных) пользователем элементов. При значении индикатора, равному -1 возвращается список номеров выбранных элементов. При прочих значениях возвращается активный элемент списка.

Для комбо-бокса значение индикатора игнорируется и всегда возвращается активный элемент.

Вот пример, иллюстрирующий сказанное:

(dlgCreate 'd-1 120 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _LIST 10 10 95 80 '("Tahoma" 9 1 0 0) nil _LEFT)

 ==> c-1

(dlgAddControl 'd-1 'b-2 _BUTTON 8 87 97 40 '("Tahoma" 9 1 0 0) "Опросить")

 ==> b-2

(dlgAddControl 'd-1 'b-3 _BUTTON 8 130 97 40 '("Tahoma" 9 1 0 0) "Закрыть")

 ==> b-3

(dlgAddItem 'c-1 "Мышка")

 ==> c-1

(dlgAddItem 'c-1 "Кошка")

 ==> c-1

(dlgAddItem 'c-1 "Жучка")

 ==> c-1

(dlgAddItem 'c-1 "Внучка")

 ==> c-1

(dlgAddItem 'c-1 "Бабка")

 ==> c-1

(dlgAddItem 'c-1 "Дедка")

 ==> c-1

(defun b-2_Click Nil (say (output (dlggetlist 'c-1 -1))))

 ==> b-2_Click

(defun b-3_Click Nil (dlgHide 'd-1))

 ==> b-3_Click

(dlgSetEvent 'b-2 'b-2_click)

 ==> b-2_Click

(dlgSetEvent 'b-3 'b-3_click)

 ==> b-3_Click

(dlgShow 'd-1)

В примере создается диалог и к нему добавляются список и две командные кнопки. Затем список заполняется элементами. Одной из кнопок (с надписью "Опросить") назначается процедура-обработчик, включающая вызов функции DLGGETLIST в режиме отображения выбранных элементов списка. Второй кнопке назначается процедура-обработчик, скрывающая диалог. При отображении диалога пользователь увидит картинку:

Теперь, если мышью или клавиатурой выбрать какие-либо элементы:

а затем нажать кнопку "Опросить", то пользователь увидит выбранные элементы:

DLGGETTEXT

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

Рассмотрим следующий пример:

(dlgCreate 'd-1 120 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _TEXT 10 10 95 12 '("Tahoma" 9 1 0 0) "" _LEFT)

 ==> c-1

(dlgAddControl 'd-1 'b-2 _BUTTON 8 87 97 40 '("Tahoma" 9 1 0 0) "Опросить")

 ==> b-2

(dlgAddControl 'd-1 'b-3 _BUTTON 8 130 97 40 '("Tahoma" 9 1 0 0) "Закрыть")

 ==> b-3

(defun b-2_Click Nil (say (dlggettext 'c-1 -1)))

 ==> b-2_Click

(defun b-3_Click Nil (dlgHide 'd-1))

 ==> b-3_Click

(dlgSetEvent 'b-2 'b-2_click)

 ==> b-2_Click

(dlgSetEvent 'b-3 'b-3_click)

 ==> b-3_Click

(dlgShow 'd-1)

 ==> d-1

Здесь создается диалог и к нему добавляется текст-бокс и две кнопки. Одной кнопке назначается обработчик, отображающий в диалоговом окне поле ввода. Вторая кнопка служит для скрытия диалога. После отображения диалога пользователь увидит:

Если теперь в поле ввода ввести текст:

и нажать кнопку "Опросить", то получим:

DLGHIDE

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

Приведенный ниже пример подтверждает это:

(dlgCreate 'd-1 120 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _TEXT 10 10 95 12 '("Tahoma" 9 1 0 0) "" _LEFT)

 ==> c-1

(dlgAddControl 'd-1 'b-2 _BUTTON 8 87 97 40 '("Tahoma" 9 1 0 0) "Опросить")

 ==> b-2

(dlgAddControl 'd-1 'b-3 _BUTTON 8 130 97 40 '("Tahoma" 9 1 0 0) "Закрыть")

 ==> b-3

(defun b-2_Click Nil (say (dlggettext 'c-1 -1)))

 ==> b-2_Click

(defun b-3_Click Nil (dlgHide 'd-1))

 ==> b-3_Click

(dlgSetEvent 'b-2 'b-2_click)

 ==> b-2_Click

(dlgSetEvent 'b-3 'b-3_click)

 ==> b-3_Click

(dlgShow 'd-1)

 ==> d-1

Здесь, как и в разделе, описывающем функцию DLGGETTEXT создается диалог с текст-боксом и двумя кнопками. Одной из кнопок назначается обработчик, который опрашивает текст-бокс. Вторая кнопка скрывает диалог. Если ввести в текст-бокс что либо, нажать кнопку "Закрыть", а затем вызвать процедуру b-2_Click явно, то пользователь увидит строку, введенную в текст-бокс перед закрытием диалога.

DLGPUTLIST

Функция DLGPUTLIST позволяет занести список в лист-бокс или в комбо-бокс. Функция принимает два обязательных параметра: идентификатор контрола и одноуровневый список, состоящий из строк. При успешном завершении функция возвращает T.

Ниже приводится два примера вызова функции DLGPUTLIST:

(dlgCreate 'd-1 120 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _LIST
               10 10 95 80
               '("Tahoma" 9 1 0 0)
               nil _LEFT)

 ==> c-1

(dlgAddControl 'd-1 'b-2 _BUTTON
               8 87 97 40
               '("Tahoma" 9 1 0 0)
               "Занести")

 ==> b-2

(dlgAddControl 'd-1 'b-3 _BUTTON
               8 130 97 40
               '("Tahoma" 9 1 0 0)
               "Закрыть")

 ==> b-3

(defun b-2_Click Nil (dlgPutList 'c-1
                      '("Мышка"
                        "Кошка"
                        "Жучка"
                        "Внучка"
                        "Бабка"
                        "Дедка")))

 ==> b-2_Click

(defun b-3_Click Nil (dlgHide 'd-1))

 ==> b-3_Click

(dlgSetEvent 'b-2 'b-2_click)

 ==> b-2_Click

(dlgSetEvent 'b-3 'b-3_click)

 ==> b-3_Click

(dlgShow 'd-1)

Здесь создается простой список (лист-бокс) и две кнопки. Обработчик нажатия одной из кнопок заносит содержимое в изначально пустой список. Если выполнить приведенные выше команды, то пользователь увидит:

После нажатия на кнопку "Занести" пользователь увидит заполненный список:

Вот аналогичный пример, но с заполнением комбо-бокса:

(dlgCreate 'd-1 120 200 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _COMBO
               10 10 95 12
               '("Tahoma" 9 1 0 0)
               nil _LEFT)

 ==> c-1

(dlgAddControl 'd-1 'b-2 _BUTTON
               8 87 97 40
               '("Tahoma" 9 1 0 0)
               "Занести")

 ==> b-2

(dlgAddControl 'd-1 'b-3 _BUTTON
               8 130 97 40
               '("Tahoma" 9 1 0 0)
               "Закрыть")

 ==> b-3

(defun b-2_Click Nil (dlgPutList 'c-1
                      '("Мышка"
                        "Кошка"
                        "Жучка"
                        "Внучка"
                        "Бабка"
                        "Дедка")))

 ==> b-2_Click

(defun b-3_Click Nil (dlgHide 'd-1))

 ==> b-3_Click

(dlgSetEvent 'b-2 'b-2_click)

 ==> b-2_Click

(dlgSetEvent 'b-3 'b-3_click)

 ==> b-3_Click

(dlgShow 'd-1)

Здесь создается выпадающий список (комбо-бокс) и две кнопки. Обработчик нажатия одной из кнопок заносит содержимое в изначально пустой комбо-бокс. Если выполнить приведенные выше команды, то пользователь увидит:

После нажатия на кнопку "Занести" пользователь увидит заполненный список:

DLGPUTPICTURE

Функция DLGPUTPICTURE позволяет поместить иконку на командную кнопку. К сожалению, иконку приходится выбрать из встроенного автором в программу списка иконок. Возможно, в последующих версиях HomeLisp можно будет загружать иконки из внешних файлов. Функция DLGPUTPICTURE принимает два обязательных параметра: идентификатор командной кнопки и номер иконки (число типа FIXED в интервале 1 - 42). Следует отметить, что высота кнопки должна быть достаточна для размещения выбранной иконки.

Вот пример вызова Функции DLGPUTPICTURE:

(dlgCreate 'd-1 180 100 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _BUTTON 10 10 150 60 '("Tahoma" 9 1 0 0) "Нажми!" Nil)

 ==> c-1

(defun setpic nil (prog nil
                        (dlgPutPicture 'c-1
                                       (str2fix (ask "Задайте номер иконки")))))

 ==> setpic

(dlgSetEvent 'c-1 'setpic)

 ==> setpic

(dlgShow 'd-1)

Создается диалог и к нему добавляется командная кнопка. Кнопке назначается функция-обработчик нажатия, в которой запрашивается номер иконки (вызов ASK), этот номер из строковой формы преобразуется в числовую (вызов STR2FIX) и подается на вход DLGPUTPICTURE.

Если пользователь выполнит приведенный выше код, нажмет кнопку (которая изначально не содержит иконки), и введет 4 , то на кнопку будет помещена следующая иконка:

DLGPUTTEXT

Функция DLGPUTTEXT позволяет для кнопки, чек-бокса, радиокнопи установить текстовую надпись. Для текст-бокса эта функция позволяет занести текст в область ввода. Функция требует два обязательных параметра: идентификатор контрола и текст/надпись (типа STRING).

Пользоваться этой функцией несложно:

(dlgCreate 'd-1 180 100 "Проба")

 ==> d-1

(dlgAddControl 'd-1 'c-1 _BUTTON
               10 10 150 60
               '("Tahoma" 9 1 0 0) "Нажми!" Nil)

 ==> c-1

(defun setpic nil (prog nil (dlgPutText 'c-1 "Еще раз!")))

 ==> setpic

(dlgSetEvent 'c-1 'setpic)

 ==> setpic

(dlgShow 'd-1)

После отображения диалога пользователь увидит:

А после нажатия на кнопку:

DLGSETEVENT

Функция DLGSETEVENT назначает контролу обработчик события. Функция требует два параметра: идентификатор контрола и имя функции-обработчика. При успешном завершении функция возвращает имя функции-обработчика.

Как уже было отмечено во введении, событийная модель в HomeLisp-e очень проста. С каждым типом контролов связано одно событие, которое можно обработать программно (с помощью функции-обработчика). Эти события представлены в нижеследующей таблице:

Контрол | Событие
Метка (_LABEL) | Щелчок мышью.
Текст-бокс (_TEXT) | Нажатие клавиши. Процедуре-обработчику передается ASCII-код нажатой клавиши.
Лист-бокс (_LIST) | Выбор элемента списка мышью (одиночный клик) или клавиатурой.
Комбо-бокс (_COMBO) | Выбор элемента списка мышью (одиночный клик) или клавиатурой.
Кнопка (_BUTTON) | Одиночный клик или Enter.
Чек-бокс (_CHECK) | Одиночный клик или Enter.
Радиокнопка (_OPTION) | Одиночный клик или Enter.

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

(dlgCreate 'd-1 200 170 "Проба")

 ==> d-1

(dlgAddControl 'd-1 't-1
               _TEXT 10 10 170 12
               '("Tahoma" 9 1 0 0) "" _LEFT)

 ==> t-1

(dlgAddControl 'd-1 'b-1
                _BUTTON 10 40 170 40
                '("Tahoma" 9 1 0 0)
                "Показать" _CENTER)

 ==> b-1

(dlgAddControl 'd-1 'b-2
               _BUTTON 10 90 170 40
               '("Tahoma" 9 1 0 0)
               "Закрыть" _CENTER)

 ==> b-2

(defun add-key (keyascii)
               (push keyascii key-log)
)

 ==> add-key

(defun show-log nil (say (output key-log)))

 ==> show-log

(defun quit nil (dlgHide 'd-1))

 ==> quit

(dlgSetEvent 't-1 'add-key)

 ==> add-key

(dlgSetEvent 'b-1 'show-log)

 ==> show-log

(dlgSetEvent 'b-2 'quit)

 ==> quit

(setq key-log nil)

 ==> NIL

(dlgShow 'd-1)

В примере создается диалог с текст-боксом и двумя кнопками. Текст-боксу назначается обработчик события, который добавляет код очередной нажатой клавиши к списку, являющемуся значением переменной key-log. Одной из кнопок назначается обработчик, который отображает текущее состояние списка key-log. Другая кнопка служит для скрытия диалога. Важно, что перед отображением диалога, переменной key-log присваивается значение Nil.

Если ввести текст в текст-бокс:

и нажать кнопку "Показать", то можно увидеть коды всех нажатых клавиш:

Следует отметить только, что коды клавиш запоминаются в списке в обратном порядке. Это связано с особенностью работы функции PUSH.

DLGSHOW

Функция DLGSHOW отображает подготовленный диалог. Функция принимает один обязательный параметр - идентификатор диалога и два необязательных - координаты левого верхнего угла окна диалога. Первый параметр должен иметь тип DIALOG, координаты левого верхнего угла - тип FIXED. Координаты должны задаваться в пикселах. Если координаты опущены, диалог отображается в центре экрана. При успешном завершении функция возвращает идентификатор диалога.


Алфавитный указатель функций

Ниже приводится полный алфавитный список всех функций HomeLisp, включая функции дополнительной библиотеки. Для того, чтобы выйти на начало группы функций, имена которых начинаются на нужную букву, необходимо щелкнуть мышью по изображению этой буквы на горизонтальном буквенном навигаторе, расположенном ниже. В описываемой версии HomeLisp отсутствуют функции, имена которых начинаются с букв: J, K, X и Y. Тем не менее, буквы оставлены в навигаторе в расчете на последующие версии...

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A |
 | ABS
 | ACS
 | ADD1
 | ADDIFNONE
 | AND
 | APPEND
 | APPLY
 | ASSERT
 | ASK
 | ASN
 | ASSOC
 | ASSOLIST
 | ATN
 | ATOM
 | ATOMLIST
 | ATTACH
B |
 | BACKQUOTE
 | BIT2BLO
 | BIT2FIX
 | BITSP
 | BLO2BIT
 | BLO2FIXED
 | BLO2FLO
 | BLO2STR
 | BLOCOPY
 | BLOCREATE
 | BLODESTROY
 | BLODUMP*
 | BLOEQ
 | BLOFILL
 | BLOFSTR
 | BLOREPLACE
C |
 | CADR
 | CAR
 | CART
 | CDR
 | CH
 | CLS
 | COLLECT
 | COMCREATEOBJECT
 | COMDESTROYOBJECT
 | COMINTERFACE
 | COMMETHOD
 | COMMETHODO
 | COMPROPGET
 | COMPROPGETO
 | COMPROPLET
 | COND
 | CONS
 | CONTEXT
 | COPY
 | COS
 | CSETQ
 | CYCLEP
D |
 | DEBUG
 | DEFDYN
 | DEFGLOB
 | DEFINE
 | DEFLIST
 | DEFMACRO
 | DEFUN
 | DEFUNF
 | DEFVAR
 | DIFFERENCE
 | DIFLIST
 | DIVIDE
 | DLGADDCONTROL
 | DLGADDITEM
 | DLGCHKOPT
 | DLGCLEARLIST
 | DLGCNTHIDESHOW
 | DLGCREATE
 | DLGDESTROY
 | DLGGETLIST
 | DLGGETTEXT
 | DLGHIDE
 | DLGPUTLIST
 | DLGPUTPICTURE
 | DLGPUTTEXT
 | DLGSETEVENT
 | DLGSHOW
 | DO/DO*
 | DOLIST
 | DOTIMES
 | DOTPRINT
 | DPAIR
 | DREMOVE
 | DREVERSE
 | DUMP
E |
 | ECHO
 | EFFACE
 | EQ
 | EQUAL
 | EQUALSET
 | EVAL
 | ERRORMESSAGE
 | EXIT
 | EXP
 | EXPLODE
 | EXPT
F |
 | FILCLOSE
 | FILEOF
 | FILEXISTP
 | FILGETBLO
 | FILGETLINE
 | FILGETSTRCOUNT
 | FILLOC
 | FILLOF
 | FILOPEN
 | FILPUTBLO
 | FILPUTLINE
 | FILSEEK
 | FIRST
 | FIX
 | FIX2BIT
 | FIX2BLO
 | FIX2FLO
 | FIXEDP
 | FLAG
 | FLAGP
 | FLATTEN
 | FLO2BLO
 | FLO2FIX
 | FLO2STR
 | FLOATP
 | FOR
 | FORALL
 | FORMAT
 | FORODD
 | FORSOME
 | FUNCALL
 | FUNCTION
G |
 | GC
 | GENSYM
 | GETD
 | GETEL
 | GETPROP
 | GEVAL
 | GO
 | GREATERP
 | GREQP
 | GRWARC
 | GRWARCE
 | GRWBEZIER
 | GRWCIRCLE
 | GRWCLS
 | GRWCREATE
 | GRWDESTROY
 | GRWDESTROYALL
 | GRWDRAW
 | GRWELLIPSE
 | GRWFILL
 | GRWFONT
 | GRWFRECT
 | GRWHIDE
 | GRWHIDEALL
 | GRWINKEY
 | GRWLINE
 | GRWLOADPIC
 | GRWPOINT
 | GRWPRINT
 | GRWPSET
 | GRWRECT
 | GRWREFRESH
 | GRWSAVEPIC
 | GRWSCALE
 | GRWSETPARM
 | GRWSHOW
 | GRWSHOWALL
 | GRWTEXTH
 | GRWTEXTW
 | GRWTRANSPARENT
 | GSET
H |
 | HASHCLEAR
 | HASHCOUNT
 | HASHCREATE
 | HASHDESTROY
 | HASHERASE
 | HASHGET
 | HASHKEYEXIST
 | HASHMAP
 | HASHPUT
I |
 | IF
 | IMPLODE
 | INPUT
 | INTERSECTION
L |
 | LABELS
 | LAMBDA
 | LAST
 | LCYCLEP
 | LEFTSHIFT
 | LENGTH
 | LET/LET*
 | LESSP
 | LEXORDER
 | LEXORDER1
 | LIST
 | LISTP
 | LOG
 | LOGAND
 | LOGOR
 | LOGXOR
 | LOOP
 | LUNION
M |
 | MACROEXPAND
 | MAKESET
 | MAP
 | MAPCAR
 | MAPLIST
 | MAX
 | MEMB
 | MEMBER
 | MIN
 | MINUS
 | MINUSP
N |
 | NCONC
 | NLAMBDA
 | NOT/NULL
 | NUMBERP
O |
 | OBJLIST
 | ONEP
 | OR
 | ORDER
 | ORDER1
 | OUTPUT
P |
 | PAIR
 | PAIRLIS
 | PARSE
 | HIDE
 | PLUS
 | POP
 | POPUP
 | POSSESSING
 | PRINT/PRINTLINE
 | PRINTS/PRINTSLINE
 | PROG
 | PROG1/PROG2/PROG2
 | PROP
 | PROPLIST
 | PSHOW
 | PUSH
 | PUTD
 | PUTEL
 | PUTFLAG
 | PUTPROP
Q |
 | QUOTE
 | QUOTIENT
R |
 | RANK
 | RDS
 | READ
 | READIMAGE
 | REGCLOSEKEY
 | REGCREATEKEY
 | REGDELETEKEY
 | REGDELETEVALUE
 | REGENUMKEY
 | REGENUMVALUE
 | REGOPENKEY
 | REGQUERYVALUE
 | REGSETVALUE
 | REMAINDER
 | REMFLAG
 | REMOVE
 | REMOVEF
 | REMPROP
 | RESTART
 | RETURN
 | REVERSE
 | RIGHTSHIFT
 | RND
 | ROUND
 | RPLACA
 | RPLACD
S |
 | SASSOC
 | SAY
 | SELECT
 | SET
 | SETOF
 | SETQ
 | SEXPR
 | SFEXPR
 | SH
 | SIGN
 | SIN
 | SMACRO
 | SPECIALP
 | SPROPL
 | SQR
 | STAT
 | STR2BLO
 | STR2FIX
 | STR2FLO
 | STRASC
 | STRATRIM
 | STRCAT
 | STRCHR
 | STRDEL
 | STRIND
 | STRINGP
 | STRINJ
 | STRLCASE
 | STRLEN
 | STRLTRIM
 | STRMID
 | STRPRESS
 | STRREP
 | STRRTRIM
 | STRSPACE
 | STRSTRING
 | STRUCASE
 | STRUPD
 | SUB1
 | SUBLIS
 | SUBSET
 | SUBST
 | SUCHTHAT
 | SUCHTHAT1
 | SUCHTHAT2
 | SWAPEL
 | SYSARGC
 | SYSARGV
 | SYSCOMPNAME
 | SYSCOPY
 | SYSDATE
 | SYSDIR
 | SYSDISKSPACE
 | SYSDRIVETYPE
 | SYSENV
 | SYSERASE
 | SYSEXEC
 | SYSGETATTR
 | SYSGETCOLOR
 | SYSGETFONT
 | SYSGETOPENNAME
 | SYSGETSAVENAME
 | SYSHOME
 | SYSLOGICALDRIVES
 | SYSMEMSTATUS
 | SYSMETRICS
 | SYSMKDIR
 | SYSMODE
 | SYSOSVER
 | SYSRENAME
 | SYSRMDIR
 | SYSSETATTR
 | SYSSHELL
 | SYSSYSDIR
 | SYSTIME
 | SYSTMPDIR
 | SYSTMPFILE
 | SYSUSERNAME
 | SYSWINDIR
T |
 | TCONC
 | TERPRI
 | TIMACTIVEP
 | TIMES
 | TIMSTART
 | TIMSTOP
 | TRACE
 | TRY
U |
 | UNDUMP*
 | UNION
 | UNLESS
 | UNSTAT
 | UNTRACE
V |
 | VARLIST
W |
 | WHEN
 | WRITEIMAGE
 | WRS
Z |
 | ZEROP


Работа с библиотекой HomeLispLib.Exe

Работа в интегрированной среде HomeLispIde не позволяет использовать возможности языка Лисп при программировании на других языках. Представлется вполне разумным, дать возможность использовать язык Лисп и тем, кому по каким-либо причинам не требуются (или не нравятся!) возможности интегрированной среды, но желательно использовать преимущества Лиспа для решения своих задач. Вот почему разработчик решил предоставить пользователям библиотеку HomeLispLib.Exe. Разработчик посчитал своим долгом вложить в эту библитеку возможно большую функциональность. Насколько это удалось - судить читателю.

Кроме подробного описания билиотеки HomeLispLib.Exe, эта глава содержит описание двух скриптовых реализаций HomeLisp. Их использование позволяет писать скрипты на языке HomeLisp в любом текстовом редакторе.

О г л а в л е н и е
Ручная инсталляция библиотеки HomeLispLib.Exe.
Класс clsLisp и его методы.
Использование класса clsLisp в программах.
Использование класса clsLisp в VBA/VB.
Использование класса clsLisp в VBScript.
Скриптовые реализации HomeLisp.
Скриптовый движок ConLisp.Exe.
Скриптовый движок WLisp.Exe.
Ручная инсталляция библиотеки HomeLispLib.Exe

Прежде, чем описывать работу с библиотекой HomeLispLib.Exe, разработчик должен отметить, что эта библиотека явлется объектной (по терминологии Майкрософт - ActiveX-Exe). Последнее означает, что прежде, чем пользователь сможет использовать библиотеку, она должна быть зарегистрирована на компьютере пользователя. Если пакет HomeLisp успешно проинсталлирован на компьютере в полном объеме, то библиотека HomeLispLib.Exe будет зарегистрована автоматически. Если же возникает необходимость установить библиотеку вручную, то разработчик советует поступить следующим образом:

Создать на своем компьютере директорию (например, D:\HLisp);

Скопировать в нее следующие файлы: HomeLispLib.Exe, HomeLispD.ini, Lib-a.lsp, Reglib.cmd, Conlisp.exe, Wlisp.exe и Starter.exe. Первые четыре файла являются обязательными, а три последних нужны только тем, кто собирается писать скрипты на языке HomeLisp.

Зарегистрировать библиотеку HomeLispLib.Exe, для чего войти в директорию D:\HLisp и выполнить командный файл Reglib.cmd, содержащий следующую команду:

HomeLispLib.Exe /Regserver

Успешная регистрация завершается "молча". При возникновении ошибок при регистрации будет выдано системное сообщение. Наиболее возможные причины ошибок:

Отсутствие у пользователя, выполняющего регистрацию, необходимых прав. С этими проблемами - к системному администратору;

Отсутствие на компьютере библиотеки MSVBVM60.DLL (виртуальная машина Visual Basic 6.0). Чрезвычайно маловерояно, но возможно. Необходимо установить и зарегистрировать на компьютере эту библиотеку.

Если пользователь хочет писать скрипты на языке HomeLisp, следует связать расширение .lsp или какое-либо другое (.li, .l) с программой Wlisp.exe.

Как уже говорилось, все описанные действия автоматически выполняются на этапе инсталляци HomeLisp. Приведенные выше сведения предназначены тем, кто захочет использовать только библиотеку HomeLispLib.Exe и желает перенести ее на другой компьютер.

Класс clsLisp и его методы.

Библиотека HomeLispLib.Exe содержит единственный класс clsLisp. Этот класс имеет шестнадцать методов и не имеет свойств. Методы класса clsLisp приведены в нижеследующей таблице:

№ | Имя | Параметры | Результат
1 | About | - | Отображает модальную форму "О программе...". Результата не возвращает.
2 | Config | - | Отображает модальную форму "Конфигурация", позволяющую настроить ряд параметров, хранящихся в конфигурационном файле HomeLispD.ini. Если конфигурация успешно изменена, функция возвращает логическое значение True, в противном случае - False.
3 | Get_ErrorMessage | [cP As integer] | Возвращает последнее сообщение об ошибке. Если ошибка не возникала, возвращает пустую строку. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодировке.
4 | Get_flgSucc | - | Возвращает флаг успешности завершения метода SEval. Если значение равно False, значит последний вызов SEval завершился с грубой ошибкой. Продолжение работы обычно не имеет смысла, и программа должна быть завершена.
5 | Get_flgTerm | - | Возвращает флаг терминирования. Если значение равно True, значит была вызвана функция EXIT.
6 | Get_LastExpr | [cP As integer] | Возвращает последнее вычисленное S-выражение в виде строки символов. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодироке.
7 | Get_Output | [cP As integer] | Возвращает буфер вывода (вывод функций PRINT). Если функции PRINT/PRINTLINE при вычислении последнего S-выражения не вызывались, то возвращается пустая строка. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодироке.
8 | Get_Resp | [cP As integer] | Возвращает результат вычисления очередного S-выражения в виде строки. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодироке.
9 | Get_Stat | [cP As integer] | Возвращает строку статистики после вычисления последнего S-выражения, переданного методу SEval. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодироке. Строка статистики включает в себя время вычисления (в сек.); количество списочных ячеек, количество атомов (и через слэш - количество переменных) в области данных HomeLisp.
10 | Get_SysMessage | [cP As integer] | Возвращает последнее системное сообщение. Если при вычислении системное сообщение не генерировалось, возвращается пустая строка. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодироке.
11 | Get_Ver | [cP As integer] | Возвращает версию ядра HomeLisp в виде строки символов. Если значение необязательного параметра не равно нулю, строка возвращается в OEM-кодировке, в противном случае - в ANSI-кодироке.
12 | Set_Argc | n As Integer | Устанавливает значение количества аргументов командной строки, доступной функции sysArgc. Результата не возвращает.
13 | Set_Argv | i As Integer V As String | Устанавливает значение i-го параметра командной строки, доступной функции sysArgv равным значению V. Результата не возвращает.
14 | Set_HomeDir | D As String | Устанавливает путь к директории, которая является текущей для вызывающего приложения (клиента), равным значению параметра D. Результата не возвращает.
15 | Set_Time | ti As Long | Устанавливает интервал времени работы ядра Лиспа при вычислении S-выражения до принудительного завершения (в миллисекундах) разным значению параметра ti. Результата не возвращает. Если значение параметра равно нулю - принудительного прерывания не происходит.
16 | SEval | inpExpr As String | Вычисляет значение S-выражения, заданного обязательным параметром inpExpr, после чего можно получить значение результата (в виде символьной строки) методом Get_Resp; значения системного сообщения методом Get_SystemMessage; содержимое буфера вывода методом Get_Output; сообщение об ошибке методом Get_ErrorMessage, а время вычислений - методом Get_Time. Сам метод SEval результата не возвращает.
Использование класса clsLisp в программах.

Общая схема использования класса clsLisp такова:

1) Создать объект из класса clsLisp и сохранить ссылку на него;

2) При необходимости вызвать методы Set_Argc и Set_ArgV для задания значений, доступных функциям sysArgC и sysArgV. Если этого не сделать, то функция sysArgC будет возвращать нуль, а функция sysArgV - Nil;

3) При необходимости вызвать метод Set_HomeDir для задания значения имени домашней директории, которое будет возвращать функция sysHome. Если этого не сделать, то функция sysHome будет возвращать имя директории, в которой зарегистрирована библиотека HomeLispLib.Exe;

4) При необходимости вызвать метод Set_Time для задания максимального времени вычисления в миллисекундах. Если этого не сделать, то временной интервал до принудительного прерывания будет определяться параметром конфигурационного файла HomeLispD.ini;

5) Подготовить S-выражение в виде текстовой строки и вызвать метод SEval;

6) Принять и, при необходимости, отобразить результат вычисления, вызвав методы Get_Resp, Get_Output, Get_SysMessage;

7) Если результат, возвращенный методом Get_Resp, равен ERRSTATE, получить методом Get_ErrorMessage сообщение об ошибке и, при необходимости, отобразить его. Можно поступить и по-другому: всегда получать сообщение об ошибке и, если оно пусто, то вычисление завершено без ошибок;

8) При необходимости принять и отобразить строку статистики, вызвав метод Get_Stat;

9) Для продолжения работы перейти к пункту 4);

7) Для завершения работы уничтожить объект, созданный в пункте 1).

Конкретная реализация этой схемы зависит от языка программирования, из которого предполагается вызывать HomeLisp. Далее будут приведены примеры для языков VB/VBA и VBScript.

Использование класса clsLisp в VBA/VB.

Здесь будет подробно рассмотрено использование класса clsLisp в программах VB/VBA. Эта методика одинаково хорошо работает в любом офисном компоненте (Word, Excel, PowerPoint и др.), поддерживающем язык VBA. Отличия заключаются только в методах отображения результатов. В качестве примера здесь рассматривается использование класса clsLisp в среде Microsoft-Excel. Предполагается, что библитека HomeLispLib.Exe зарегистрирована и сконфигурирована, как было описано выше.

Как известно, существует два основных способа использования объектных библиотек: раннее связывание (с установкой объектной ссылки) и позднее связывание. Ниже описывается методика раннего связывания. Позднее связывание будет описано в разделе, посвященному использованию библиотеки в сценариях на языке VBScript.

Сейчас читателю предлагается последовательно проделать следующее:

1) Открыть пустую книгу Excel и перейти в режим редактирования программного кода (нажав Alt-F11);

2) Выбрать последовательно пункты главного меню Tools -> References;

3) В окне References найти библиотеку HomeLisp и поставить галочку слева, как показано на приводимом ниже рисунке, после чего нажать кнопку "OK":

Если в окне References отсутствует библиотека HomeLisp, то это означает, что регистрация библиотеки завершилась неудачно. Необходимо зарегистрировать библиотеку. Если после нажатия кнопки "OK" не появилось сообщений об ошибках, значит ссылка успешно установлена. Это означает, что теперь методы класса clsLisp будут появляться в списке ассистента VBA, а весь интерфейс класса можно увидеть в браузере объектов. Чтобы убедиться в этом, следует нажать кнопку с изображением:

Если ссылка на библиотеку HomeLispLib.Exe успешно установлена, то при нажатии на эту кнопку и выборе в выпадающем списке библиотек библиотеки HomeLispLib.Exe пользователь увидит:

4) Если ссылка успешно установлена, следует дважды щелкнуть мышью по иконке первого листа рабочей книги. При этом откроется окно ввода программного кода. В этом окне следует ввести текст:

Sub TestLisp()

Dim myLisp As HomeLisp.clsLisp

    '::: Создание объекта

    Set myLisp = New HomeLisp.clsLisp

    With myLisp

         '::: Считаем факториалы целых

         For i% = 1 To 30

             '::: Формируем S-выражение (fact i)

             SExpr$ = "(fact " + CStr(i%) + ")"

             '::: Вычисляем...

             .SEval SExpr$

             '::: Получаем сообщение об ошибке

             ErrTxt$ = .Get_errormessage()

             '::: Ошибка была?

             If ErrTxt$ <> "" Then

                '::: Да - сообщим об этом

                MsgBox "Ошибка: " + ErrTxt$

                Exit For

             Else

                '::: Нет - занесем результат
                '::: в очередную ячейку

                Cells(i%, 1).Value = CStr(i%) + "!=" + .Get_Resp()

             End If

         Next i%

    End With

    '::: Уничтожим объект (хотя он и сам уничтожится!)

    Set myLisp = Nothing

End Sub

5) Если запустить эту программу на выполнение (нажатием клавиши F5), то на первом листе пользователь увидит:

Желающий может проверить правильность вычислений на калькуляторе (шутка!)... Совершенно аналогично можно установить ссылку на библиотеку в проекте на языке VB и пользоваться всеми возможностями языка VB.

Использование класса clsLisp в VBScript.

В языке VBScript пользователю не удастся установить ссылку на библиотеку (ее просто некуда устанавливать!); здесь придется использовать позднее связывание. Снова предполагается, что библиотека HomeLispLib.Exe зарегистрирована на компьютере пользователя.

Методика использования библиотеки HimeLispLib.Exe при позднем связывании аналогична описанной выше. Единственное существенное отличие заключается в том, что вместо оператора New придется использовать функцию CreateObject. В качестве примера рассмотрим скрипт, который считает факториалы целых числел от 1 до 50 записывает результат в текстовый файл factorials.txt, располагающийся в текущей директории. Этот скрипт может иметь, например, такой вид:

    '::: Получим имя текущей директории

    Tmp=Cstr(WScript.ScriptFullName)

    l=Len(Tmp)

    For i=l to 1 Step -1

        If mid(Tmp,i,1)="\" then

           HomeDir=Left(Tmp,(i-1))

           Exit For

        End if

    Next

    '::: Создаем файл и открываем его для вывода

    Set myFSO=CreateObject("Scripting.FileSystemObject")

    Set OutFile=myFSO.OpenTextFile(HomeDir & "\factorials.txt",2,True)

    '::: Создание объекта "Лисп"

    Set myLisp = CreateObject("HomeLisp.clsLisp")

    With myLisp

         '::: Считаем факториалы целых

         For i = 1 To 50

             '::: Формируем S-выражение (fact i)

             SExpr = "(fact " + CStr(i) + ")"

             '::: Вычисляем...

             .SEval Cstr(SExpr)

             '::: Получаем сообщение об ошибке

             ErrTxt = .Get_errormessage()

             '::: Ошибка была?

             If ErrTxt <> "" Then

                '::: Да - сообщим об этом

                MsgBox "Ошибка: " + ErrTxt

                Exit For

             Else

                '::: Нет - занесем результат
                '::: в файл

                OutFile.WriteLine CStr(i) + "!=" + .Get_Resp()

             End If

         Next

    End With

    '::: Закрываем файл

    OutFile.Close

    '::: Уничтожим объекты (хотя они и сами уничтожатся!)

    Set OutFile=Nothing

    Set myFSO=Nothing

    Set myLisp = Nothing

    MsgBox "Готово!"

После завершения этого скрипта содержимое файла factorials.txt будет таким:

1!=1
2!=2
3!=6
4!=24
5!=120
6!=720
7!=5040
8!=40320
9!=362880
10!=3628800
11!=39916800
12!=479001600
13!=6227020800
14!=87178291200
15!=1307674368000
16!=20922789888000
17!=355687428096000
18!=6402373705728000
19!=121645100408832000
20!=2432902008176640000
21!=51090942171709440000
22!=1124000727777607680000
23!=25852016738884976640000
24!=620448401733239439360000
25!=15511210043330985984000000
26!=403291461126605635584000000
27!=10888869450418352160768000000
28!=304888344611713860501504000000
29!=8841761993739701954543616000000
30!=265252859812191058636308480000000
31!=8222838654177922817725562880000000
32!=263130836933693530167218012160000000
33!=8683317618811886495518194401280000000
34!=295232799039604140847618609643520000000
35!=10333147966386144929666651337523200000000
36!=371993326789901217467999448150835200000000
37!=13763753091226345046315979581580902400000000
38!=523022617466601111760007224100074291200000000
39!=20397882081197443358640281739902897356800000000
40!=815915283247897734345611269596115894272000000000
41!=33452526613163807108170062053440751665152000000000
42!=1405006117752879898543142606244511569936384000000000
43!=60415263063373835637355132068513997507264512000000000
44!=2658271574788448768043625811014615890319638528000000000
45!=119622220865480194561963161495657715064383733760000000000
46!=5502622159812088949850305428800254892961651752960000000000
47!=258623241511168180642964355153611979969197632389120000000000
48!=12413915592536072670862289047373375038521486354677760000000000
49!=608281864034267560872252163321295376887552831379210240000000000
50!=30414093201713378043612608166064768844377641568960512000000000000
Скриптовые реализации HomeLisp

Но к чему посредники (даже такие хорошие, как VB/VBA/VBscript!) если мы любим Лисп?! Почему бы не писать на Лиспе скрипты без каких бы то ни было посредников? Такая возможность имеется. В поставку HomeLisp входят целых два скриптовых "движка": Conlisp.exe и Wlisp.exe. Первый из них (Conlisp.exe) является 32-х битным консольным приложением (команды можно вводить в консольном окне, а можно - из переопределенного стандартного ввода). Второй (Wlisp.exe) исполняет программу, которую берет из файла с подходящим расширением (*.ls *.li *.l или что-либо в этом роде) без отображения консольного окна.

Оба движка используют библиотеку HomeLispLib.Exe, поэтому и описываются в этом разделе документации. Входные языки скриптовых движков Conlisp.exe и Wlisp.exe не различаются между собой и практически не отличаются от языка HomeLisp, используемого интегрированной средой HomeLispIDE.

Единственным серьезным отличием скриптовой реализации является то, что в конкретный момент времени на экране может быть отображено только одно графическое окно. При этом нет ограничений на создание невидимых графических окон; их можно создавать в любых разумных количествах и выполнять с ними любые операции. Функция GRWSHOWALL в скриптовой реализации отсутствует, а вызов GRWSHOW для отображения окна w2 при уже отображенном окне w1 вызывает сокрытие окна w1 с последующим отображением окна w2.

Скриптовый движок Conlisp.exe

Консольный движок может использоваться нонконформистами, которые предпочитают интерфейс командной строки нормальному графическому интерфейсу. Исполняемый модуль движка имеет имя Conlisp.exe. Этот модуль при запуске может принимать следующие параметры командной строки (ключи):

-s - этот ключ вызывает выдачу после вычисления введенного S-выражения строки статистики (включающей время выполнения, количество занятых списочных ячеек, количество атомов и количество переменных);

-o Имя_файла - этот ключ обеспечивает сохранения протокола работы в файле, имя которого задано после ключа -o. Если же строка -o является последней в командной строке, то выдается предупреждение и ключ -o игнорируется (протокол не ведется). Следует отметить, что вызов движка с ключом -o отличается от переопределения стандартного вывода: в протокол попадает вся служебная информация, сопутствующая выполнению, а в переопределенный выходной файл - только выходной поток функций print/prits/printline/printsline;

-t nnnnn - этот ключ обеспечивает принудительное прерывание вычислений после истечения nnnnn миллисекунд. Если же строка -t является последней в командной строке, то выдается предупреждение и ключ -t игнорируется, а интервал времени до принудительного прерывания берется из конфигурационного файла HomeLispD.ini;

-iANSI - наличие этого ключа означает, что входные команды будут поступать в ANSI-кодировке. Ключ имеет смысл использовать, если переопределен стандартный ввод и входные команды поступают из файла в кодировке ANSI. Если ключ опущен, то входные команды рассматриваются как команды в OEM-кодировке. Естественно, ключ влияет только на изображение русских букв;

-oANSI - наличие этого ключа означает, что весь вывод будет представляться в кодировке ANSI. Ключ имеет смысл использовать, если переопределен стандартный вывод и результат выполнения направляется в файл. Ключ влияет только на изображение русских букв. Если ключ не задан, вывод формируется в OEM-кодировке.

При запуске движка без переопределения входного и выходного потоков на экране возникает консольное окно, в котором пользователь может вводить команды. В это же окно осуществляется и вывод ответов. Для завершения работы следует с клавиатуры ввести CTRL-Z и нажать Enter (стандартное сочетание, означающее конец файла). Можно также ввести команду (exit).

Если стандартный ввод переопределен, то выполнение скрипта закончится автоматически, когда будет исчерпан входной файл, поданный на вход.

Ниже приводятся примеры вызова Conlisp.exe.

В первых строках консольного окна выводится стандартный заголовок. Он включает сведения о версии ядра и командную строку запуска (если она была задана). После строки из символов "-" начинается область диалога. При этом:

Команды, введенные пользователем, отображаются ярко-белым;

Результат вычисления S-выражения - ярко-зеленым;

Системные сообщения - ярко-желтым;

Сообщения об ошибках - ярко-красным;

Выходной поток функций print/prits/printline/printsline - светло-голубым;

Статистика выполнения команд (при заданном ключе -s) - серым;

Ниже показан еще один пример работы с Conlisp.exe:

Командная строка здесь не задана. При вызове несуществующей функции FCAT результат вычисления есть атом ERRSTATE. Красным цветом выведено сообщение об ошибке. Далее включен режим статистики. Вычисление факториала сопровождается выводом данных о количестве вызовов функций системы. Поскольку ключ -s не задан, то не выводятся и сведения о времени выполнения и памяти ядра HomeLisp. Последней строкой желтым цветом выведен результат сборки мусора (функция GC была вызвана автоматически).

Следующий пример показывает работу с функцией printline:

Пусть читатель обратит внимание на число 10, выведенное зеленым цветом. Это - результат вычисления функции FOR. Далее идут факториалы чисел от 1 до 10; ниже располагается сообщение сборщика мусора, и последняя строка содержит данные о времени выполнения и памяти (поскольку задан ключ -s).

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

Здесь при запуске движка задан ключ -t 200, который задает максимально допустимый временной интервал вычислений 200 миллисекунд (0.2 сек). Вычисление 100! на компьютере разработчика требует 0.06 сек. Расчет 100!, таким образом, укладывается в заданный интервал. А вот вычисление 200! уже не укладывается - происходит принудительное прерывание, результат равен атому BRKSTATE. Ниже располагается системное предупреждение (о том, что допустимый интервал времени истек).

Если создать файл pdir.lsp, содержащий следующую программу:

(prog (lst item l)

      (setq lst (sysDir "*.li" &HFF))  // Получаем список файлов

      (printsline "+-------------------+")

@loop (setq item (car lst))            // Очередной элемент списка

      (setq lst (cdr lst))             // Остаток списка

      (setq l (strlen item))           // Длина имени

      (cond ((< l 17)                  // Если короче - добавим пробелов

             (setq item (strcat item (strspace (- 17 l)))))
      )

      (prints "| ")                    // Печатаем элемент
      (prints item)
      (printsline " |")

      (cond ((null lst) (go @exit)))   // Список исчерпан - на выход

      (Go @loop)                       // Следующий элемент

@exit

      (printsline "+-------------------+")

)

а затем выполнить в командной строке команду: conlisp < pdir.lsp > ls.txt, то в файле ls.txt окажется, например следующее:

+-------------------+
| 1.li              |
| b.li              |
| grafik-7p.li      |
| h.li              |
| main_test.li      |
| netEnum.li        |
| pdir.li           |
| regEnum.li        |
| u.li              |
+-------------------+

Этот же скрипт можно выполнить и по-другому: запустив conlisp и введя команду (rds "pdir.li"). Результат показан ниже:

В заключение хотелось бы отметить, что консольный характер движка conlisp нисколько не мешает вызывать скрипты с графическим интерфейсом (диалоги, графические окна).

Скриптовый движок Wlisp.exe

Движок Wlisp.exe не использует стандартные потоки ввода/вывода. Он берет имя исполняемого скрипта из командной строки, а для общения с пользователем используется графический интерфейс.

Движок использует три ключа командной строки (два из которых полностью аналогичных соответствующим ключам движка conlisp):

-o Имя_файла - этот ключ обеспечивает сохранения протокола работы в файле, имя которого задано после ключа -o. Если же строка -o является последней в командной строке, то выдается предупреждение и ключ -o игнорируется (протокол не ведется).

-t nnnnn - этот ключ обеспечивает принудительное прерывание вычислений после истечения nnnnn миллисекунд. Если же строка -t является последней в командной строке, то выдается предупреждение и ключ -t игнорируется, а интервал времени до принудительного прерывания берется из конфигурационного файла HomeLispD.ini;

-s - этот ключ обеспечивает подавление выдачи сообщения о завершении скрипта. По умолчанию, если результат выполнения скрипта не является одним из атомов ERRSTATE, BRKSTATE или STOPSTATE, то после его завершения выдается сообщение "Скрипт nnnnnnn.nnn завершен".

Разработчик рекомендует связать какое-либо расширение (например, *.li) с движком wlisp.exe. В этом случае выполнять скрипты можно будет выполнять "двойным щелчком".


Работа с дампами

Как уже было сказано выше, пользователь может запросить дамп вычисления. Дампы - это обычные текстовые файлы, имеющие расширение ".dmp". Чтобы запросить формирование дампа, есть два основных способа:

вызов встроенной функции DUMP*;

нажатие десятой кнопки тулбара.

Следует отметить, что использование функции DUMP* является более гибким, поскольку позволяет задать два уровня дампирования: с включением в дамп снимков ассоциативного списка и без включения. Включение дампирования нажатием кнопки (или выбором пункта меню) позволяет задать только дампирование без включения снимков ассоциативного списка.

При нажатии кнопки (или вызове функции DUMP* без параметров) появляется диалоговое окно выбора файла. Пользователь должен задать имя нового или существующего файла. Новый файл будет создан, а если задано имя существующего файла, то новая информация будет дописываться в конец файла. Имя заданного файла будет отображаться в статус-баре.

Чтобы задать включение снимков ассоциативного списка в дамп, следует вызвать функцию DUMP* со вторым параметром T.

Проиллюстрируем все сказанное примером. В стандартную поставку входит рекурсивная функция fact, которая вычисляет факториал целого числа. Если запустить интерпретатор, включить режим дампирования, задать имя файла 444.dmp и вычислить S-выражение (fact 5), то в dump-файл попадет вся служебная информация о работе функции.

Обратите внимание, что на вычисление ушло 0.17 сек, тогда как без дампирования это же S-выражение вычисляется менее чем за 0,01 сек (т.е. более чем в 17 раз быстрее). Теперь можно посмотреть, какая информация попала дамп. Для просмотра дампа выполнения следует нажать 14-ю кнопку тулбара. Пользователю будет предложено выбрать дамп для просмотра. Если выбрать файл 444.dmp, то пользователь увидит:

По дампу легко прослеживаются все этапы вычисления. Точками показывается уровень рекурсии. При дампировнии алгоритмов с глубокой рекурсией более удобным может оказаться режим отображения глубины вложенности при помощи числа в квадратных скобках. Режимом отображения вложенности рекурсии управляет конфигурационный параметр NEST. Этот параметр может принимать значения 0, 1 или 2. Если значение NEST равно 0, то вложенность не отображается. Значение 1 вызывает отображение вложенности числом в квадратных скобках, а значение 2 вызывает отображение вложенности точками. Это выглядит так:

Если вызвать функцию DUMP* явно, задав вторым параметром атом T, и вычислить 5!, как показано ниже:

а затем просмотреть dum-файл, то можно увидеть, как меняется ассоциативный список:

Для возврата к вводу S-выражений следует повторно нажать 14-ю кнопку тулбара. Для выключения режима дампирования следует повторно нажать 10-ю кнопку тулбара (или явно вызвать встроенную функцию UNDUMP*).

Режим дампирования доступен и в скриптовых движках CONLISP и WLISP, но, естественно, включать дампирование здесь придется вызовом функции DUMP*, а просматривать дампы - с помощью каких-либо подручных средств.


Общие сведения о среде HomeLisp

Для начала работы запустите модуль HomelispIde.exe. После запуска произойдет автоматическая загрузка библиотеки дополнительных функций из модуля, заданного в файле конфигурации после чего пользователь увидит главное окно программы:

Интерфейс программы достаточно прозрачен. Имеется стандартное меню и тулбар. Можно пользоваться тем и другим по желанию пользователя - все функции тулбара продублированы в главном меню. Часть функций дополнительно реализована в контекстном меню (о чем будет сказано ниже).

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

Основную часть главной формы занимают две области: область ввода внизу и область ответа вверху. Между областями ввода и ответа располагается разделительная линия. Эту линию можно перемещать по вертикали мышью (при нажатой левой клавише). Это дает возможность менять по желанию относительные размеры областей ввода и ответа.

В нижней части формы располагается статус-бар с шестью панелями. В первой панели отобразается текущее количество занятых ячеек списочной памяти. Во второй - текущее количество атомов (длина списка объектов). В третьей панели отображается текущее число переменных (атомов, которым присвоены значения конструкцией SET/SETQ). Четвертая панель показывает суммарное время выполнения последней команды (в сек.). Во время выполнения команд загрузки из файлов в этой панели располагается прогресс-бар. В пятой панели размещается индикатор контроля скобочной структуры вводимых S-выражений. В последней, шестой панели отображаются индикаторы управления статистикой, эхо и дампом.

Назначение командных кнопок тулбара:

1. | Открыть и загрузить файл.При нажатии на эту кнопку появляется стандартный диалог выбора файла с расширением ".lsp". Если пользователь нажимает кнопку "принять" - файл загружается (т.е. последовательно исполняются все S-выражения, содержащиеся в файле). При этом, если в файле содержится определяющее выражение уже заданной ранее функции, то оно заменяет старое определение. То же относится и к значениям переменных: если атом A имел значение 12, а в файле содержится команда (SETQ A 20), то после загрузки файла атом A будет иметь значение 20. Если загрузка завершена без ошибок, то в область ввода выдается итоговое сообщение об успешной загрузке. Эту команду можно выполнить, вызвав встроенную функцию HLisp-а: (RDS ["имя-файла"]). Если имя файла опущено - будет вызвано окно диалога.
2. | Сохранить состояние в файле. При нажатии на эту кнопку появится окно стандартного диалога "сохранить как". Пользователь задает имя файла, в который будут сохранены определяющие выражения всех функций, а также значения всех переменных (из ассоциативного списка).Эту команду можно выполнить, вызвав встроенную функцию HLisp-а: (WRS ["имя-файла"]). Если имя файла опущено - будет вызвано окно диалога.
3. | Вызвать автономный редактор. Позволяет готовить исходные тексты в текстовом редакторе. HomeLisp имеет в своем составе встроенный редактор, а также позволяет пользователю вызвать любой внешний редактор (путь ко внешнему редактору можно задать в режиме конфигурации).
4. | Вызвать редактор диалогов. Режим позволяет вызвать встроенный дизайнер экранных форм.
5. | Очистить область ввода. Позволяет очистить область ввода (ниже разделительной линии). Следует отметить, что при вводе S-выражения область ввода очищается автоматически.
6. | Очистить область вывода. Позволяет очистить область вывода (выше разделительной линии). В процессе работы с системой область вывода скроллируется. Размер сохраняемой части (в Кб) задается в ini-файле. Значение по умолчанию - 10Кб. Для очистки области вывода можно использовать контекстное меню (см. ниже), а также встроенную функцию (CLS).
7. | Включить/выключить ведение журнала. При включенном режиме все введенные команды выводятся в журнал. Журнал представляет собой текстовый файл в текущей директории, имеющий имя HomeLisp.log. Кроме самой команды в журнале фиксируется дата и время ввода. Журнал удобно использовать для разбора аварийных или спорных ситуаций. По умолчанию ведение журнала при запуске включается автоматически. Кнопка позволяет отключить ведение журнала. Программа может быть настроена таким образом, чтобы при запуске ведение журнала было выключено (см. режим "Конфигурация")
8. | Включить/выключить режим эха. При включенном режиме эха вводимые S-выражения дублируются в области вывода, после чего вычисляются. Результат вычислений выводится после строки ===>. По умолчанию режим эха включен. При нажатии этой кнопки режим эха выключается. Последующее нажатие вновь включает эхо. Статус эха отображается в шестой панели статус-бара (Эхо=Вкл/Выкл). Включение и выключение режима может осуществляется встроенной функцией (ECHO [выражение]). Если результат вычисления выражения равен Nil, режим эха выключается. Если же результат вычисления выражения не равен Nil, режим эха включается.
9. | Включить/выключить режим статистики. При включенном режиме статистики после вычисления S-выражения в область вывода выводится статистика вызовов функций Лиспа. По умолчанию режим статистики включен. При нажатии этой кнопки режим статистики выключается. Последующее нажатие вновь включает статистику. Статус режима отображается в шестой панели статус-бара (Стат=Вкл/Выкл). Включить статистику можно, введя S-выражение (STAT); выключается статистика вводом S-выражения (UNSTAT).
10. | Включить/выключить формирование дампа. При включенном режиме дампа во внешний файл выводится подробная информация, характеризующая процесс вычисления S-выражения "изнутри". Этот режим может помочь при отладке, но сильно замедляет процесс вычисления. По умолчанию режим выключен. При нажатии кнопки выводится диалог выбора файла для дампа (расширение ".dmp"). Если пользователь отказывается от выбора - режим не включается. Если же файл выбран, то информация о вычислении каждого S-выражения будет дописываться в конец файла. Статус режима отображается в шестой панели статус-бара (Дамп=Вкл/Выкл). Если дамп включен, то в статус-баре отображается имя файла. Как и для режима статистики, существует пара встроенных функций, позволяющая управлять режимом дампирования из программного кода: (DUMP ["имя_файла"]) - включить и (UNDUMP) - выключить дампирование соответственно.
11. | Вставка стандартного шаблона. Для облегчения работы начинающих изучать Lisp, поддерживается возможность вставки в текущее положение области ввода шаблонов стандартных конструкций. В поставку включены следующие шаблоны: COND, LAMBDA, DEFUN, SEXPR, SFEXPR, SMACRO, PROG. Шаблоны как текстовые файлы с расширением .patt хранятся в поддиректории \Patt. При необходимости пользователь может поместить в эту директорию и другие шаблоны. При нажатии на кнопку появится окно выбора шаблона. Пользователь должен выбрать нужный шаблон и нажать кнопку "Принять". Этот режим ниже будет описан более подробно.
12. | Показать/скрыть список объектов. Однократное нажатие на эту кнопку покажет пользователю список всех атомов (список объектов). У каждого атома кроме наименования показываются стандартные индикаторы (APVAL, FIXED, FLOAT, BITS, EXPR, FEXPR, SUBR, FSUBR, MACRO), а также весь список дополнительных свойств. Для атомов типа EXPR, FEXPR, MACRO можно увидеть определяющее выражение. Все, что показывается пользователю, может быть также получено с помощью встроенных и дополнительных функций. Обращаем внимание, что значения атомов в списке объектов не содержатся. Для их просмотра служит режим "показать/скрыть ассоциативный список". Повторное нажатие на кнопку убирает список объектов с главной формы.
13. | Показать/скрыть ассоциативный список. Однократное нажатие на эту кнопку покажет пользователю список всех атомов, имеющих значения. Значение атома можно увидеть, просто введя S-выражение, состоящее из имени атома. Повторное нажатие на кнопку убирает ассоциативный список с главной формы.
14. | Просмотр дампов. Этот режим позволяет просматривать дампы без выхода из среды разработки. Будет отображено диалоговое окно, и пользователь должен будет выбрать имя файла для просмотра.
15. | Менеджер графических окон. Нажатие этой кнопки вызывает менеджер графических окон. Кнопка активна только в случае, если в сеансе создано хотя бы одно графическое окно. Менеджер окон позволяет скрывать, отображать и уничтожать графические окна, созданные вызовом функции grwCreate.
16. | Уборка мусора. Нажатие этой кнопки запускает подпрограмму сборки мусора. В данной версии убирается мусор в списке объектов и в области списочных ячеек. Особенность реализации интерпретатора такова, что в области списочных ячеек мусор образуется чрезвычайно редко (разве что при работе с циклическими структурами; для линейных структур ненужные далее списочные ячейки утилизируются автоматически). Нажатие кнопки "Убрать мусор" полностью эквивалентно вызову встроенной функции (GC). В область ответа выводится итоговое сообщение "Утилизировано столько-то списочных ячеек и столько-то атомов".
17. | Прервать выполнение. Эта кнопка бывает активной только в процессе выполнения введенной команды. Нажатие этой кнопки вызывает прерывание выполнения введенной команды и немедленное завершение. В качестве результата возвращается предопределенный атом brkState.
18. | Построить exe-файл. Система позволяет строить т.н. псевдо-exe-файлы: к объектному коду ядра Лиспа добавляется исходный код всех необходимых функций (а также константы). Эта информация сохраняется на диске с заданным пользователем именем и расширением "exe". При запуске этого файла на выполнение, сначала разворачивается ядро Лиспа, а затем загружается вся сохраненная информация и вызывается стартовое S-выражение (оно задается пользователем при создании exe-файла). После чего программа на Лиспе выполняется обычным образом. Описываемая кнопка будет активной только в случае, когда в текущей директории HomeLisp находится файл-заготовка HomeLisp.stb.
19. | Конфигурация. Нажатие этой кнопки вызывает форму конфигурации. Пользователь может задать некоторые числовые параметры распределения памяти, а также указать путь к файлу автозагружаемой библиотеки стандартных функций. Вся информация, введенная пользователем, сохраняется в ini-файле Homelisp.ini. Старое содержимое файла сохраняется в файле Homelisp.bak.
20. | Помощь. Это то, что вы сейчас читаете.
21. | О программе и авторе. Будет показана форма, на которой представлен номер версии программы (впрочем, он продублирован в заголовке главной формы), а также даны ссылки на сайт разработчика и его почтовый адрес.
22. | Выход. Нажатие на эту кнопку завершает работу. Завершиться можно также, введя команду (EXIT ["Прощальное сообщение"]). Если прощальное сообщение задано, оно будет выведено перед окончательным завершением.

Контекстное меню

Для управления некоторыми параметрами областей ввода и вывода служит контекстное меню, вызываемое по правому щелчку мыши, выполняемому в одной из областей. Контекстное меню имеет вид:

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

В первую группу входят команды, выполняемые с содержимым буфера обмена. Эти команды активны, если буфер обмена содержит данные (Вставить) или в области ввода или вывода имеется выделенный текст. Следует отметить, что даже если буфер обмена содержит текст, команда Вставить никогда не будет активной для области вывода - в области вывода вставка невозможна.

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

Третья группа команд позволяет задать цвет фона, цвет текста и шрифт для той области, на которой контекстное меню активизировано. Рекомендуется использовать моноширинные шрифты (Courier, FixedSys).

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


Создание EXE-файлов.

HomeLisp (по крайней мере в предлагаемой версии) не способен компилировать Лисп-код в команды микропроцессора. Поэтому для создания автономных исполняемых файлов применяется технология, которую можно условно назвать псевдо-EXE.

Суть этой технологии состоит в следующем.

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

Предположим, что разработчик написал и загрузил ряд функций, которые в совокупности составляют приложение. При создании EXE-файла, все выбранные разработчиком функции приписываются в "хвост" целевого файла. Кроме этих функций, в "хвост" целевого файла записывается т.н. стартовое S-выражение, а также задаваемые пользователем настройки среды HomeLisp (размеры внутренних стеков, количество динамических объектов и т.д.)

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

Рассмотрим процесс создания EXE-файла на простом практическом примере: создадим диалоговую программу, которая будет вычислять наибольший общий делитель двух целых чисел с помощью алгоритма Евклида. Пример этот не так уж бесполезен: поскольку HomeLisp работает с целыми неограниченной разрядности, написание аналогичного примера в любой расхожей среде программирования (VB,C++,Delphi) потребовала бы заметных усилий...

Функция *GCD имеет следующий вид:

(defun *gcd (x y) (cond ((eq x y)                x)
                        ((greaterp y x) (*gcd y x))
                        ((eq (remainder x y) 0)  y)
                        (T              (*gcd y (remainder x y)))))

==> *gcd

(*gcd 655 72)

==> 1

(*gcd 655 75)

==> 5

(*gcd 65536 4096)

==> 4096

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

Запрос (посредством функции ASK) первого операнда;

Запрос второго операнда;

Вычисление результата;

Отображение результата с помощью функции SAY;

Напишем определяющее выражение для функции GCD-1, в котором будет реализован приведенный выше алгоритм:

(defun GCD-1 nil

    (prog (a1 a2)

          (setq a1 (str2fix (Ask "Введите первый операнд")))

          (setq a2 (str2fix (Ask "Введите второй операнд")))

          (TRY

              (say (fix2str (*gcd a1 a2)))

           EXCEPT

              (say "Ошибка!")

          )

     )

)

Следует обратить внимание на то, что результат вызова ASK имеет тип STRING, а функция *GCD требует операндов типа FIXED. Поэтому, прежде, чем присваивать введенные пользователем значения локальным переменным, a1 и a2, эти значения следует преобразовать в тип FIXED (для чего служат вызовы функции STR2FIX).

Далее, поскольку готовится диалоговая программа, предназначенная для конечного пользователя, следует предусмотреть возможные ошибки. Именно поэтому вычисление наибольшего общего делителя сделано критическим участком кода с помощью вызова функции TRY Если в процессе вычисления произойдет сбой, программа не "упадет", а будет выдано сообщение "Ошибка!".

Если выполнить функцию GCD-1 из среды разработки, то сначала будет выведено окно запроса первого операнда:

Пользователь вводит первый операнд и нажимает кнопку OK; появляется окно запроса второго операнда:

Пользователь вводит второй операнд, нажимает кнопку OK и получает результат:

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

В обоих случаях будет открыто окно построения EXE-файла:

Чтобы создать EXE-файл, необходимо выполнить следующие действия:

Задать имя EXE-файла. Имя (и путь) будущего EXE-файла задаются в комбинированном поле ввода в левом верхнем углу формы создания EXE-файла. Имя и путь можно задать вручную, а можно нажать кнопку "..." и выбрать директорию и имя с помощью стандартного диалога Сохранить как. По умолчанию создается файл noname.exe в текущей директории.

В области ввода "Стартовое S-выражение" необходимо задать код, с которого начнется выполнение будущего приложения. Стартовое S-выражение может быть вызовом функции. Если требуется задать стартовое S-выражение, состоящее из нескольких вызовов, то эти вызовы следует "заключить" в PROG-конструкцию.

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

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

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

Сбросить флажок "Отображать диспетчерскую форму при старте". Если этот флажок оставить поднятым, то при запуске приложения будет отображаться специальная диспетчерская форма. Этой форме можно задать заголовок (в соответствующем поле ввода). Использование диспетчерской формы удобно при отладке.

Установить или сбросить флажок "Сообщать о завершении". Если этот флажок установлен, то перед завершением приложения будет выдано сообщение.

Окно создания EXE-файла с минимально-необходимым набором параметров может иметь вид:

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

Можно убедиться, что в текущей директории пявился EXE-файл gcd-1.exe, при запуске которого происходит описанный выше диалог с пользователем.

Если же при создании EXE-файла поднять флажок "Отображать диспетчерскую форму при старте" и задать заголовок формы:

то при исполнении полученного EXE-файла, в правом верхнем углу экрана отображается диспетчерская форма следующего вида:

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

Если в процессе выполнения приложения с диспетчерской формой возникает ошибка, то информация об ошибке выводится в область вывода, которая в нормальном состоянии скрыта. Например, если сделать стартовым S-выражением вызов несуществующей функции (unknown) , то при выполнении возникнет картинка, приведенная ниже:

Использование диспетчерской формы позволяет принудительно остановить выполнение приложения. Для этого служит пункт меню "Стоп" и кнопка, расположенная ниже (и пункт меню, и кнопка активны только в процессе выполнения приложения). Если в качестве стартового выражения задать, например, вычисление факториала достаточно большого числа, то можно успеть увидеть пункт меню и кнопку останова:

Если в процессе выполнения нажать "Стоп", то выполнение программы прерывается:

Пункт главного меню "Окна" диспетчерской формы (и соответствующая командная кнопка) будут активны в случае, когда в процессе выполнения приложения создано хотя бы одно графическое окно. При выборе пункта "Окна" (или нажатии на кнопку, расположенную ниже), вызывается менеджер графических окон.

Режим построения EXE-файлов с диспетчерской формой предназначен для облегчения отладки приложений. Если не использовать диспетчерскую форму, то управлять графическими окнами должен будет сам разработчик, а в случае "зависания" приложения для его снятия придется использовать диспетчер задач ОС.

При необходимости изменить значение какого-либо настроечного параметра (размер стека, число лексем и т.д.) необходимо щелкнуть мышью по нужно строке списка "Параметры". Числовое значение соответствующего параметра будет перенесено в область ввода, расположенную под списком:

Далее следует исправить значение параметра и нажать кнопку с изображением стрелки, направленной вверх. Значение параметра будет исправлено:

В качестве второго примера создания EXE-файла, рассмотрим прежнюю задачу (вычисление НОД), но с оконным графическим интерфесом. Для этого создадим диалог (как описано здесь), на форме которого разместим три метки, три поля ввода и две кнопки. Назначим одной из кнопок процедуру-обработчик, в котором вычислим НОД чисел, введенных в первое и второе поля ввода и занесение вычисленного НОД в поле результата. Обработчик нажатия второй кнопки будет закрывать и уничтожать диалог. Форма диалога с элементами управления может иметь вид, приведенный ниже:

Программный код, сгенерированный дизайнером, может иметь такой вид:

   (prog nil

      (dlgCreate '_Dlg_ 419 233 "Наибольший общий делитель:")

      (dlgAddControl '_Dlg_ '_LBL_1 _LABEL 14 15 100 21
                     '("Tahoma" 14,25 1 0 0) "Первое:" 0 &H80000012 &H8000000F)

      (dlgAddControl '_Dlg_ '_TXT_1 _TEXT 155 13 248 31
                     '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005)

      (dlgAddControl '_Dlg_ '_LBL_2 _LABEL 12 59 133 26
                     '("Tahoma" 14,25 1 0 0) "Второе:" 0 &H80000012 &H8000000F)

      (dlgAddControl '_Dlg_ '_TXT_2 _TEXT 156 56 247 31
                     '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005)

      (dlgAddControl '_Dlg_ '_LBL_3 _LABEL 13 101 127 27
                     '("Tahoma" 14,25 1 0 0) "Н.О.Д." 0 &HFF &H8000000F)

      (dlgAddControl '_Dlg_ '_TXT_3 _TEXT 157 98 247 31
                     '("Tahoma" 14 1 0 0) "" 0 &HFF &H80000005)

      (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 24 148 180 50
                     '("Tahoma" 8,25 1 0 0) "Вычислить")
      (dlgPutPicture '_BUT_1 7)

      (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 213 149 180 50
                     '("Tahoma" 8,25 1 0 0) "Закрыть")
      (dlgPutPicture '_BUT_2 36)

      //
      // Обработчик события CLICK для кнопки _BUT_2
      //

      (defun _BUT_2_Click  Nil

          (prog Nil

               (dlgHide '_DLG_)

               (dlgDestroy '_DLG_)

               (gc)

          )

      )

      //
      //   Назначение процедуры-события _BUT_2_Click  контролу _BUT_2
      //

      (dlgSetEvent '_BUT_2 '_BUT_2_Click )
      //
      // Обработчик события CLICK для кнопки _BUT_1
      //

      (defun _BUT_1_Click  Nil

            (prog (a1 a2)

               (setq a1 (str2fix  (dlgGetText '_TXT_1)))

               (setq a2 (str2fix  (dlgGetText '_TXT_2)))

               (TRY

                    (dlgPutText '_TXT_3 (fix2str (*gcd a1 a2)))

                EXCEPT

                    (dlgPutText '_TXT_3 "Ошибка!!!")

               )
            )

      )

      //
      //   Назначение процедуры-события _BUT_1_Click  контролу _BUT_1
      //

      (dlgSetEvent '_BUT_1 '_BUT_1_Click )

      //
      //   Отображение диалога _Dlg_
      //

      (dlgShow '_Dlg_)
   )

Для того, чтобы построить EXE-файл, нужно загрузть в среду Лиспа функцию *GCD и написать стартовое выражение для отображения диалога. Проще всего "погрузить" приведенную выше PROG-конструкцию в функцию MAIN без параметров. Тогда стартовым S-выражением для запуска EXE-файла будет вызов (main). Итак, если загрузить в среду Лиспа следующие функции:

//
//  Поиск наибольшего общего делителя
//

(defun *gcd (x y) (cond ((eq x y)                x)
                        ((greaterp y x) (*gcd y x))
                        ((eq (remainder x y) 0)  y)
                        (T              (*gcd y (remainder x y)))))

//
//  Программа отображения диалога _Dlg_
//

(defun main nil

   (prog nil

      (dlgCreate '_Dlg_ 419 233 "Наибольший общий делитель:")

      (dlgAddControl '_Dlg_ '_LBL_1 _LABEL 14 15 100 21
                     '("Tahoma" 14,25 1 0 0) "Первое:" 0 &H80000012 &H8000000F)

      (dlgAddControl '_Dlg_ '_TXT_1 _TEXT 155 13 248 31
                     '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005)

      (dlgAddControl '_Dlg_ '_LBL_2 _LABEL 12 59 133 26
                     '("Tahoma" 14,25 1 0 0) "Второе:" 0 &H80000012 &H8000000F)

      (dlgAddControl '_Dlg_ '_TXT_2 _TEXT 156 56 247 31
                     '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005)

      (dlgAddControl '_Dlg_ '_LBL_3 _LABEL 13 101 127 27
                     '("Tahoma" 14,25 1 0 0) "Н.О.Д." 0 &HFF &H8000000F)

      (dlgAddControl '_Dlg_ '_TXT_3 _TEXT 157 98 247 31
                     '("Tahoma" 14 1 0 0) "" 0 &HFF &H80000005)

      (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 24 148 180 50
                     '("Tahoma" 8,25 1 0 0) "Вычислить")
      (dlgPutPicture '_BUT_1 7)

      (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 213 149 180 50
                     '("Tahoma" 8,25 1 0 0) "Закрыть")
      (dlgPutPicture '_BUT_2 36)

      //
      // Обработчик события CLICK для кнопки _BUT_2
      //

      (defun _BUT_2_Click  Nil

          (prog Nil

               (dlgHide '_DLG_)

               (dlgDestroy '_DLG_)

               (gc)

          )

      )

      //
      //   Назначение процедуры-события _BUT_2_Click  контролу _BUT_2
      //

      (dlgSetEvent '_BUT_2 '_BUT_2_Click )
      //
      // Обработчик события CLICK для кнопки _BUT_1
      //

      (defun _BUT_1_Click  Nil

            (prog (a1 a2)

               (setq a1 (str2fix  (dlgGetText '_TXT_1)))

               (setq a2 (str2fix  (dlgGetText '_TXT_2)))

               (TRY

                    (dlgPutText '_TXT_3 (fix2str (*gcd a1 a2)))

                EXCEPT

                    (dlgPutText '_TXT_3 "Ошибка!!!")

               )
            )

      )

      //
      //   Назначение процедуры-события _BUT_1_Click  контролу _BUT_1
      //

      (dlgSetEvent '_BUT_1 '_BUT_1_Click )

      //
      //   Отображение диалога _Dlg_
      //

      (dlgShow '_Dlg_)
   )

)

а затем построить EXE-файл gcd-2.exe, задав следующие параметры:

то получится вполне работоспособный EXE-файл. Его можно запустить, и убедиться, что НОД считается верно:


Особенности 13-й редакции ядра HomeLisp

В этом разделе приводятся ссылки на разделы документации, описывающие особенности 13-й редакции ядра HomeLisp.

1) Тело функции может состоять из нескольких форм.

2) Локальные переменные функции хранятся в лексическом контексте (а не в ассоциативном списке, который "упразднен").

3) Необязательные, ключевые вспомогательные параметры функций. Функции с неопределенным числом параметров.

4) Константа не может формальным параметром функции.

5) Особенности функции RETURN.

6) Конструкция NLAMBDA (безымянная функция, не вычисляющая значения параметров).

7) Особенности вычисления EVAL.

8) Конструкция FUNCTION.

9) Лексические, динамические и глобальные переменные.

10) Лексические замыкания-1.

11) Лексические замыкания-2.

12) Двоичная запись и восстановление состояния Лисп-машины


Описание функций работы с файлами

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

как совокупность данных на диске, имеющая имя;

как объект программы, позволяющий обмениваться данными с внешним носителем.

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

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

Cреди разнообразных типов организации существует один традиционный - текстовые наборы данных. Текстовый набор данных представляет собой совокупность строк, содержащих информацию, представляемую кодами ASCII, большими 31. Эти строки отделяются друг от друга парой символов с кодами ASCII 13 и 10. (Только в DOS/Windows; а в UNIX/LINUX разделителем является один символ с кодом 10). Для работы с такими наборами данных следует создавать файлы с методами доступа _INPUT, _OUTPUT или _APPEND. Перечисленные методы доступа позволяют читать/писать информацию из/в НД только целыми строками. Эти операции выполняются вызовом функций filGetLine и filPutLine соответственно. Текстовые методы доступа являются последовательными. Это означает, что прочитать n-ю строку можно, лишь прочитав все предыдущие (n-1) строк.

Двоичные методы доступа к НД требуют создания в программе файлов с режимами BINARY_READ, BINARY_WRITE или BINARY_READ_WRITE. При использовании этих методов доступа, НД рассматривается как цепочка байтов, пронумерованных от первого до последнего. Двоичные методы доступа являются прямыми. Это означает, что байты можно читать/писать в любом порядке. Для этих методов доступа вводится понятие файлового указателя - номера байта, который будет участвовать в следующей операции чтения/записи. Файловый указатель можно менять, осуществляя, таким образом, позиционирование.

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

Кроме того, HomeLisp поддерживает еще один своеобразный метод доступа _TEXT_ARRAY (текстовый массив). Этот метод доступа можно применять только для текстовых НД. В рамках этого метода доступа НД рассматривается, как массив строк. Строки можно читать по номеру в произвольном порядке. Запись в файл при методе доступа _TEXT_ARRAY невозможна.

Для работы с файлом в программе, его нужно сначала открыть. Открытие файла выполняет функция FILOPEN. Первым параметром этой функции должен быть "чистый" атом (не имеющий индикаторов в списке свойств). Будем далее называть этот атом идентификатором файла. Кроме того, функции FILOPEN передается имя файла и FIXED-значение, задающее режим обработки файла. При успешном открытии в список свойств атома-идентификатора заносится флаг FILE. Попытка модификации списка свойств файла вызывает ошибку (список свойств полностью защищен от модификации).

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

После завершения работы файлы следует закрывать. Для этого, разумеется, есть специальная функция. Количество одновременно открытых файлов можно менять, задавая соответствующий конфигурационный параметр nFil в файле homelisp.ini (по умолчанию можно держать открытыми до сорока файлов). Вне зависимости от значения параметра nFil, нельзя открыть больше файлов, чем позволяют текущие настройки операционной системы. Имена всех файловых функций начинаются префиксом FIL

Ниже описаны все файловые функции HomeLisp-а в алфавитном порядке:

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
FILCLOSE | 1 | 1-FILE | Закрывает файл, заданный значением аргумента. У атома-значения аргумента удаляются все файловые флаги.
FILEOF | 1 | 1-FILE | Возвращает признак конца файла (T) для файла, открытого для чтения
FILEXISTP | 1 | 1-STRING | Проверяет наличие файла с именем, заданным первым параметром. Если файл существует (даже с атрибутами HIDDEN, SYSTEM или READ-ONLY в любой комбинации), функция возвращает T. В противном случае возвращается Nil.
FILGETBLO | 2 | 1-FILE; 2-BLOB | Читает из файла, открытого в режиме _BINARY_READ или _BINARY_READ_WRITE, информацию в BLOB, заданный вторым аргументом
FILGETLINE | 1 | 1-FILE | Читает из файла открытого в режиме INPUT очередную строку. Эта строка возвращается в качестве значения.
FILGETSTRCOUNT | 1 | 1-FILE | Возвращает количество строк в файле, открытого в режиме TEXT_ARRAY.
FILLOC | 1 | 1-FILE | Возвращает файловый указатель файла, заданного значением аргумента. Файл должен быть открыт в режимах _BINARY_WRITE или _BINARY_READ_WRITE
FILLOF | 1 | 1-FILE | Возвращает длину файла
FILOPEN | 3 | 1-Атом; 2-STRING; 3-FIXED | Создает и открывает файл с именем, заданным значением ВТОРОГО аргумента. Режим открытия и доступ определяется ТРЕТЬИМ аргументом. Ссылка на файл присваивается атому-ПЕРВОМУ аргументу. В его список свойств добавляется стандрартный индикатор FILE.
FILPUTBLO | 2 | 1-FILE; 2-BLOB | Записывает в файл, заданный значением ПЕРВОГО аргумента, большой двоичный объект (BLOB), заданный значением ВТОРОГО аргумента. Файл должен быть открыт в режимах _BINARY_WRITE или _BINARY_READ_WRITE
FILPUTLINE | 2 | 1-FILE; 2-STRING | Записывает в файл, заданный значением ПЕРВОГО аргумента, строку, заданную значением ВТОРОГО аргумента. Файл должен быть открыт в режимах _OUTPUT или _APPEND
FILSEEK | 2 | 1-FILE; 2-FIXED | Устанавливает файловый указатель файла, заданного значением ПЕРВОГО аргумента, равным значению ВТОРОГО аргумента. Файл должен быть открыт в режимах _BINARY_WRITE или _BINARY_READ_WRITE
FILCLOSE

Функция FILCLOSE принимает единственный аргумент типа FILE и выполняет закрытие файла. При этом у атома-аргумента удаляется список свойств и разрывается связь файла с набором данных. Если пользователь завершит программу, не закрыв открытые файлы, они будут закрыты автоматически. Тем не менее, рекомендуется все файлы закрывать явно. При успешном закрытии файла, функция возвращает T.

Вот пример работы с функцией filClose:

(filClose 'fi)

filClose: аргумент функции - не файл
==> ERRSTATE

(filOpen 'fi "c:\config.sys" _INPUT)

==> fi

(proplist 'fi)

==> (FILE FName "c:\config.sys" FHandle 2 FMode 0)

(filClose 'fi)

==> T

(proplist 'fi)

==> NIL

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

FILEOF

Функция FILEOF принимает единственный аргумент типа FILE и проверяет, не достигнут ли конец набора данных на внешнем носителе. В этом случае функция возвращает T, в противном случае возвращается Nil.

Вот комплексный пример работы с функцией filEOF:

(prog nil (filOpen 'fi "c:\config.sys" _INPUT)

            @Beg  (cond ((filEOF 'fi) (go @Exit)))

                  (prints (filGetLine 'fi))

                  (go @Beg)

            @Exit (filClose 'fi)

                  (return 'ok)

)

device=C:\WINDOWS\COMMAND\display.sys con=(ega,,1)Country=007,866,
C:\WINDOWS\COMMAND\country.sysShell=c:\command.com /P /E:4096

==> ok

В приведенном примере все просто: открывается текстовый файл и читается строка за строкой, пока не будет достигнут его конец.

Если же файл открыт в режиме двоичного доступа _BINARY_READ, то функция filEOF будет возвращать Nil до тех пор, пока очередной вызов filGetBlo будет в состоянии прочитать прочитать количество байтов, равное длине BLOBа. Вот пример на эту тему:

(filOpen 'fi "c:\config.sys" _BINARY_READ)

==> fi

(filLOF 'fi)

==> 133

(bloCreate 'buf 100)

==> buf

(filGetBlo 'fi 'buf)

==> T

(filEOF 'fi)

==> NIL

(filGetBlo 'fi 'buf)

==> T

(filEOF 'fi)

==> T

(bloDump* 'buf)

00000001 | 53 68 65 6C 6C 3D 63 3A | 5C 63 6F 6D 6D 61 6E 64 | Shell=c:\command |
00000017 | 2E 63 6F 6D 20 2F 50 20 | 2F 45 3A 34 30 39 36 0D | .com /P /E:4096. |
00000033 | 0A 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000065 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000081 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000097 | 00 00 00                |                         | ...              |

==> T

Файл Config.sys открывается в режмие двоичного чтения. После успешного открытия определяется его длина (133 байта). Затем создается 100-байтовый буфер и в него читается первые 100 байтов файла, после чего следует вызов filEOF. Возвращается значение Nil, поскольку конец файла еще не достигнут. Следующая попытка прочитать 100 байтов уже не может быть выполнена. Поэтому, после возврата из функции filGetBlo, вызов filEOF возвращает T. Если продампировать буфер после завершения операции чтения, то можно убедиться, что реально прочитано 33 байта. Остальное пространство буфера занято двоичными нулями. При работе с двоичным методом доступа для анализа достижения конца файла предпочтительнее использовать функции filLOF и filLOС.

В заключение, следует отметить, что для выходных файлов (_OUTPUT, _APPEND) функция filEOF всегда возвращает T.

FILEXISTP

Функция FILEXISTP принимает единственный аргумент типа STRING и проверяет наличие файла с именем, заданным значением аргумента. При этом атрибуты файла могут быть любые: HIDDEN, SYSTEM, READ-ONLY. Если файл существует, функция возвращает T, в противном случае возвращается Nil.

Вот пример работы с функцией filExistp:

(filExistp "c:\aaa.aaa")

==> NIL

(filExistp "c:\IO.SYS")

==> T

Файла aaa.aaa нет на диске C:, а файл IO.SYS - есть. Функцию filExistp удобно использовать для проверки существования файла перед его открытием. Это позволяет предотвратить возникновение ошибок.

FILGETBLO

Функция FILGETBLO принимает два аргумента - идентификатор файла и идентификатор BLOBa. Выполнение функции заключается в чтении из файла, заданного первым аргументом, блока байтов и занесение его в BLOB, заданный вторым аргументом. Если чтение выполнено успешно, функция возвращает T, в противном случае функция возвращает Nil.

Файл, заданный первым аргументом, должен быть открыт в одном из двоичных режимов: _BINARY_READ или _BINARY_READ_WRITE. Если режим открытия отличается от приведенных - возникает ошибка.

Вот пример работы с функцией FILGETBLO:

(bloCreate 'buf 250)

==> buf

(filOpen 'fi "homelispide.exe" _BINARY_READ)

==> fi

(filGetBlo 'fi 'buf)

==> T

(bloDump* 'buf)

00000001 | 4D 5A 90 00 03 00 00 00 | 04 00 00 00 FF FF 00 00 | MZђ.........яя.. |
00000017 | B8 00 00 00 00 00 00 00 | 40 00 00 00 00 00 00 00 | ё.......@....... |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00 C8 00 00 00 | ............И... |
00000065 | 0E 1F BA 0E 00 B4 09 CD | 21 B8 01 4C CD 21 54 68 | .є..ґ.Н!ё.LН!Th |
00000081 | 69 73 20 70 72 6F 67 72 | 61 6D 20 63 61 6E 6E 6F | is program canno |
00000097 | 74 20 62 65 20 72 75 6E | 20 69 6E 20 44 4F 53 20 | t be run in DOS  |
00000113 | 6D 6F 64 65 2E 0D 0D 0A | 24 00 00 00 00 00 00 00 | mode....$....... |
00000129 | 4B 9C C7 DA 0F FD A9 89 | 0F FD A9 89 0F FD A9 89 | KњЗЪ.э©‰.э©‰.э©‰ |
00000145 | 8C E1 A7 89 0E FD A9 89 | 40 DF A0 89 2A FD A9 89 | Њб§‰.э©‰@Я ‰*э©‰ |
00000161 | 0B DE A4 89 0E FD A9 89 | 52 69 63 68 0F FD A9 89 | .Ю¤‰.э©‰Rich.э©‰ |
00000177 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000193 | 00 00 00 00 00 00 00 00 | 50 45 00 00 4C 01 03 00 | ........PE..L... |
00000209 | 73 DD 3C 49 00 00 00 00 | 00 00 00 00 E0 00 0F 01 | sЭ T

В примере создается BLOB размером 250 байтов, открывается для двоичного чтения файл "homelispide.exe" из текущей директории, а затем из файла читается в BLOB первые 250 байтов. После этого BLOB дампируется. Заголовок EXE-файла отчетливо виден.

FILGETLINE

Функция FILGETLINE принимает первый аргумент - идентификатор файла, открытого в режиме _INPUT или _TEXT_ARRAY Функция читает из файла очередную строку и возвращает ее в качестве значения. Если возникает состояние "конец файла" - функция возвращает Nil.

Если файл открыт в режиме _TEXT_ARRAY, то можно задать второй необязательный аргумент - номер строки. Если этот номер выводит за пределы набора данных - функция вернет Nil. В случае, когда второй аргумент не задан или равен нулю, функция вернет очередную строку набора данных. Таким образом, обращение к функции FILGETLINE с файлом, открытым в режиме _TEXT_ARRAY, со вторым аргументом, равным нулю или без второго аргумента, позволяет читать строки из текстового массива одну за другой, не задавая номера.

Вот пример работы с функцией FILGETLINE для последовательного доступа:

(filOpen 'fi "c:\config.sys" _INPUT)

==> fi

(filGetLine 'fi)

==>  "device=C:\WINDOWS\COMMAND\display.sys con=(ega,,1)"

(filGetLine 'fi)

==>  "Country=007,866,C:\WINDOWS\COMMAND\country.sys"

(filGetLine 'fi)

==>  "Shell=c:\command.com /P /E:4096"

(filGetLine 'fi)

==>  "Files=120"

(filGetLine 'fi)

==>  "Buffers=120"

(filGetLine 'fi)

==>  NIL

(filGetLine 'fi)

==>  NIL

(filEOF 'fi)

==>  T

В примере открывается файл, связанный с набором данных "c:\config.sys" в режиме _INPUT. Затем читается строка за строкой. Когда набор данных оказывается исчерпанным, функция начинает возвращать Nil. Контрльный вызов функции filEOF показывает, что набор данных действительно исчерпан.

А вот пример работы с тем же файлом, но открытым в режиме _TEXT_ARRAY:

(filOpen 'fi "c:\config.sys" _TEXT_ARRAY)

==>  fi

(filGetLine 'fi 3)

==>  "Shell=c:\command.com /P /E:4096"

(filGetLine 'fi)

==>  "Files=120"

(filGetLine 'fi)

==>  "Buffers=12"

(filGetLine 'fi)

==>  NIL

(filGetLine 'fi 1)

==>  "device=C:\WINDOWS\COMMAND\display.sys con=(ega,,1)"

Здесь тот же файл открывается для произвольного чтения. Читается сразу третья строка. Потом три следующие (без явного задания номера). Третья операция возвращает Nil, что означает конец файла. После чего читается первая строк.

FILGETSTRCOUNT

Функция FILGETSTRCOUNT принимает единственный аргумент - идентификатор файла, открытого в режиме _TEXT_ARRAY. Функция возвращает количество строк в файле (включая пустые строки). Вот пример работы FILGETSTRCOUNT:

(filOpen 'fi "c:\config.sys" _TEXT_ARRAY)

==>  fi

(filGetStrCount 'fi 3)

==>  5
FILLOC

Функция FILLOC принимает единственный аргумент - идентификатор файла, открытого в режиме _BINARY_READ или _BINARY_READ_WRITE. Функция возвращает текущую позицию файлового указателя (номер байта, c которого начнется следующая файловая операция).

Вот пример работы FILLOC:

(filOpen 'fi "c:\config.sys" _BINARY_READ)

==>  fi

(filLoc 'fi)

==>  1

(bloCreate 'buf 6)

==>  buf

(filGetBlo 'fi 'buf)

==>  T

(filLoc 'fi)

==>  7

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

FILLOF

Функция FILLOF принимает единственный аргумент - идентификатор файла. Функция возвращает длину файла в байтах.

Вот пример работы FILLOF:

(filOpen 'fi "c:\config.sys" _INPUT)

==>  fi

(filLof 'fi)

==>  155

Файл может быть открыт как в двоичном, так и в текстовом режиме.

Если файл открыт как выходной, то вызов функции filLof будет каждый раз возвращать текущую длину файла.

(filOpen 'fo "o.o" _OUTPUT)

==>  fo

(filLof 'fo)

==>  0

(filPutLine 'fo "QWERTY")

==>  T

(filPutLine 'fo "QWERTY")

==>  T

(filLof 'fo)

==>  16

Здесь в только что созданный файл выводятся две строки по 6 байтов. Каждая из строк автоматически завершается двумя байтами с кодами &H0D, &H0A, что дает общую длину 16 байтов.

FILOPEN

Функция FILOPEN принимает три аргумента: идентификатор файла, его имя и значение типа FIXED - режим открытия файла. Идентификатор файла должен быть "чистым" атомом (т.е. его список свойств должен быть пуст). Имя файла - это атом со значением типа STRING. Выполняются следующие соглашения: если имя не содержит буквы диска и слэшей, то предполагается, что файл размещается в текущей директории. Можно использовать и стандартное обозначение текущей директории .\, а также перехода на уровень вверх ..\.

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

Значение | Встроенная константа | Режим открытия (метод доступа)
0 | _INPUT | Файл открывается, как текстовый для чтения. Если НД с заданным именем не существует, возникает ошибка с кодом 53.
1 | _OUTPUT | Файл открывается, как текстовый для записи. Если НД с заданным именем не существует, то он создается. Если НД уже существовал перед открытием, то его содержимое уничтожается.
2 | _APPEND | Файл открывается, как текстовый для записи. Если НД с заданным именем не существует, то он создается. Если НД уже существовал перед открытием, то его содержимое сохраняется. Последующие операции записи будут добавлять строки в конец НД.
3 | _BINARY_READ | Файл открывается, как двоичный для чтения. Если НД с заданным именем не существует, возникает ошибка открытия.
4 | _BINARY_WRITE | Файл открывается, как двоичный для записи. Если НД с заданным именем не существует, он создается с нулевой длиной.
5 | _BINARY_READ_WRITE | Файл открывается, как двоичный для чтения и записи. Если НД с заданным именем не существует, он создается с нулевой длиной.
6 | _TEXT_ARRAY | Этот метод доступа позволяет читать строки текстового НД в произвольном порядке. Если НД с заданным именем не существует, возникает ошибка.

Вот классический пример работы с текстовым НД с именем autoexec.bat:

(filOpen 'fi "c:\autoexec.bat" _INPUT)

==> fi

(filGetLine 'fi)

==> "set TEMP=C:\TEMP"

(filGetLine 'fi)

==> "set TMP=C:\TEMP"

(proplist 'fi)

==> (FILE FName "c:\autoexec.bat" FHandle 2 FMode 0)

(filClose 'fi)

==> T

(proplist 'fi)

==> NIL

Здесь файл fi связывается с набором данных autoexec.bat. Затем из набора читается две первых строки. Затем запрашивается список свойств атома fi. Отчетливо виден стандартный индикатор FILE, внутренний номер хэндла и метод доступа (FMODE). Далее файл закрывается. После закрытия список свойств атома fi очищается.

А вот пример работы с тем же самым набором, но двоичным методом доступа:

(filOpen 'fi "c:\autoexec.bat" _BINARY_READ)

==> fi

(proplist 'fi)

==> (FILE FName "c:\autoexec.bat" FHandle 2 FMode 3)

(filLOF 'fi)

==> 874

(bloCreate 'buf 874)

==> buf

(filGetBlob 'fi 'buf)

==> T

(bloDump* 'buf)

00000001 | 0D 0A 73 65 74 20 54 45 | 4D 50 3D 43 3A 5C 54 45 | ..set TEMP=C:\TE |
00000017 | 4D 50 0D 0A 0D 0A 73 65 | 74 20 54 4D 50 3D 43 3A | MP....set TMP=C: |
00000033 | 5C 54 45 4D 50 0D 0A 0D | 0A 53 45 54 20 50 41 54 | \TEMP....SET PAT |
00000049 | 48 3D 43 3A 5C 57 49 4E | 44 4F 57 53 3B 43 3A 5C | H=C:\WINDOWS;C:\ |
00000065 | 57 49 4E 44 4F 57 53 5C | 43 4F 4D 4D 41 4E 44 3B | WINDOWS\COMMAND; |
00000081 | 22 44 3A 5C 50 79 74 68 | 6F 6E 32 34 5C 2E 22 3B | "D:\Python24\."; |
00000097 | 45 3A 5C 41 52 43 48 3B | 45 3A 5C 44 4E 3B 44 3A | E:\ARCH;E:\DN;D: |
00000113 | 5C 53 45 52 56 3B 44 3A | 5C 53 59 53 3B 45 3A 5C | \SERV;D:\SYS;E:\ |
00000129 | 54 42 3B 45 3A 5C 42 50 | 5C 42 49 4E 3B 43 3A 5C | TB;E:\BP\BIN;C:\ |
00000145 | 50 52 4F 47 52 41 7E 31 | 5C 42 4F 52 4C 41 4E 44 | PROGRA~1\BORLAND |
00000161 | 5C 44 45 4C 50 48 49 34 | 5C 42 49 4E 3B 43 3A 5C | \DELPHI4\BIN;C:\ |
00000177 | 50 52 4F 47 52 41 7E 31 | 5C 42 4F 52 4C 41 4E 44 | PROGRA~1\BORLAND |
00000193 | 5C 56 42 52 4F 4B 45 52 | 5C 42 49 4E 3B 43 3A 5C | \VBROKER\BIN;C:\ |
00000209 | 50 52 4F 47 52 41 7E 31 | 5C 42 4F 52 4C 41 4E 44 | PROGRA~1\BORLAND |
00000225 | 5C 56 42 52 4F 4B 45 52 | 5C 4A 52 45 5C 42 49 4E | \VBROKER\JRE\BIN |
00000241 | 3B 43 3A 5C 50 42 57 49 | 4E 37 30 5C 42 49 4E 3B | ;C:\PBWIN70\BIN; |
00000257 | 22 43 3A 5C 50 72 6F 67 | 72 61 6D 20 46 69 6C 65 | "C:\Program File |
00000273 | 73 5C 53 74 65 65 6C 20 | 42 61 6E 6B 20 43 6F 6D | s\Steel Bank Com |
00000289 | 6D 6F 6E 20 4C 69 73 70 | 5C 31 2E 30 5C 22 0D 0A | mon Lisp\1.0\".. |
00000305 | 0D 0A 72 65 6D 20 53 45 | 54 20 50 41 54 48 3D 25 | ..rem SET PATH=% |
00000321 | 50 41 54 48 25 3B 22 43 | 3A 5C 50 52 4F 47 52 41 | PATH%;"C:\PROGRA |
00000337 | 4D 46 49 4C 45 53 5C 4D | 54 53 22 3B 43 3A 5C 50 | MFILES\MTS";C:\P |
00000353 | 52 4F 47 52 41 7E 31 5C | 42 4F 52 4C 41 4E 44 5C | ROGRA~1\BORLAND\ |
00000369 | 43 42 55 49 4C 44 7E 31 | 5C 42 49 4E 3B 0D 0A 0D | CBUILD~1\BIN;... |
00000385 | 0A 53 45 54 20 43 4C 41 | 53 53 50 41 54 48 3D 43 | .SET CLASSPATH=C |
00000401 | 3A 5C 50 52 4F 47 52 41 | 7E 31 5C 42 4F 52 4C 41 | :\PROGRA~1\BORLA |
00000417 | 4E 44 5C 56 42 52 4F 4B | 45 52 5C 6C 69 62 5C 76 | ND\VBROKER\lib\v |
00000433 | 62 63 70 70 2E 6A 61 72 | 0D 0A 0D 0A 72 65 6D 20 | bcpp.jar....rem  |
00000449 | 2D 20 42 79 20 57 69 6E | 64 6F 77 73 20 53 65 74 | - By Windows Set |
00000465 | 75 70 20 2D 20 65 72 61 | 73 65 20 63 3A 5C 77 69 | up - erase c:\wi |
00000481 | 6E 64 6F 77 73 5C 2A 2E | 74 6D 70 20 3E 20 6E 75 | ndows\*.tmp > nu |
00000497 | 6C 0D 0A 0D 0A 72 65 6D | 20 2D 20 4C 69 6E 65 73 | l....rem - Lines |
00000513 | 20 61 64 64 65 64 20 62 | 79 20 56 69 73 75 61 6C |  added by Visual |
00000529 | 20 46 6F 72 74 72 61 6E | 20 36 2E 30 2E 30 20 53 |  Fortran 6.0.0 S |
00000545 | 65 74 75 70 20 6F 6E 20 | 35 2D 31 30 2D 32 30 30 | etup on 5-10-200 |
00000561 | 35 0D 0A 43 41 4C 4C 20 | 43 3A 5C 50 52 4F 47 52 | 5..CALL C:\PROGR |
00000577 | 41 7E 31 5C 4D 49 43 52 | 4F 53 7E 31 5C 44 46 39 | A~1\MICROS~1\DF9 |
00000593 | 38 5C 42 49 4E 5C 44 46 | 56 41 52 53 2E 42 41 54 | 8\BIN\DFVARS.BAT |
00000609 | 20 41 55 54 4F 45 58 45 | 43 0D 0A 72 65 6D 20 2D |  AUTOEXEC..rem - |
00000625 | 20 45 6E 64 20 6F 66 20 | 6C 69 6E 65 73 20 61 64 |  End of lines ad |
00000641 | 64 65 64 20 62 79 20 56 | 69 73 75 61 6C 20 46 6F | ded by Visual Fo |
00000657 | 72 74 72 61 6E 20 36 2E | 30 2E 30 20 53 65 74 75 | rtran 6.0.0 Setu |
00000673 | 70 0D 0A 20 0D 0A 72 65 | 6D 20 50 41 54 48 3D 25 | p.. ..rem PATH=% |
00000689 | 50 41 54 48 25 3B 22 43 | 3A 5C 50 72 6F 67 72 61 | PATH%;"C:\Progra |
00000705 | 6D 46 69 6C 65 73 5C 4D | 74 73 22 0D 0A 0D 0A 6D | mFiles\Mts"....m |
00000721 | 6F 64 65 20 63 6F 6E 20 | 63 6F 64 65 70 61 67 65 | ode con codepage |
00000737 | 20 70 72 65 70 61 72 65 | 3D 28 28 38 36 36 29 20 |  prepare=((866)  |
00000753 | 43 3A 5C 57 49 4E 44 4F | 57 53 5C 43 4F 4D 4D 41 | C:\WINDOWS\COMMA |
00000769 | 4E 44 5C 65 67 61 33 2E | 63 70 69 29 0D 0A 0D 0A | ND\ega3.cpi).... |
00000785 | 6D 6F 64 65 20 63 6F 6E | 20 63 6F 64 65 70 61 67 | mode con codepag |
00000801 | 65 20 73 65 6C 65 63 74 | 3D 38 36 36 0D 0A 53 45 | e select=866..SE |
00000817 | 54 20 53 42 43 4C 5F 48 | 4F 4D 45 3D 43 3A 5C 50 | T SBCL_HOME=C:\P |
00000833 | 72 6F 67 72 61 6D 20 46 | 69 6C 65 73 5C 53 74 65 | rogram Files\Ste |
00000849 | 65 6C 20 42 61 6E 6B 20 | 43 6F 6D 6D 6F 6E 20 4C | el Bank Common L |
00000865 | 69 73 70 5C 31 2E 30 5C | 0D 0A                   | isp\1.0\..       |

==> T

(filClose 'fi)

==> T

(bloDestroy 'buf)

==> T

Используется двоичный метод доступа. В списке свойств атома fi метод доступа соответствует встроенной константе _BINARY_READ. После успешного открытия вызов функции filLOF позволяет определить реальную длину НД. Затем создается буфер нужной длины, и все содержимое НД читается в буфер. Далее буфер дампируется. И, наконец, закрыватся файл и уничтожается буфер.

FILPUTBLO

Функция FILPUTBLO принимает два аргумента - идентификатор файла (открытого в режиме _BINARY_WRITE или _BINARY_READ_WRITE и идентификатор BLOBа. Содержимое BLOBа заносится в файл с текущей позиции.

Рассмотрим пример вызова FILPUTBLO:

(filOpen 'fo "o.o" _BINARY_WRITE)

==>  fo

(bloCreate 'buf 256)

==>  buf

(for i 0 255 (
              (fix2blo 'buf    // BLOB
                       i       // Заносимое число
                       (+ i 1) // Начальная позиция
                       1       // Длина
              )
             )
)

==>  255

(filPutBlo 'fo 'buf)

==>  T

(bloDestroy 'buf)

==>  T

(filClose 'fo)

==>  T

(filOpen 'fi "o.o" _BINARY_READ)

==>  fi

(bloCreate 'buf 256)

==>  buf

(filGetBlo 'fi 'buf)

==>  T

(bloDump* 'buf)

00000001 | 00 01 02 03 04 05 06 07 | 08 09 0A 0B 0C 0D 0E 0F | ................ |
00000017 | 10 11 12 13 14 15 16 17 | 18 19 1A 1B 1C 1D 1E 1F | ............... |
00000033 | 20 21 22 23 24 25 26 27 | 28 29 2A 2B 2C 2D 2E 2F |  !"#$%&'()*+,-./ |
00000049 | 30 31 32 33 34 35 36 37 | 38 39 3A 3B 3C 3D 3E 3F | 0123456789:;<=>? |
00000065 | 40 41 42 43 44 45 46 47 | 48 49 4A 4B 4C 4D 4E 4F | @ABCDEFGHIJKLMNO |
00000081 | 50 51 52 53 54 55 56 57 | 58 59 5A 5B 5C 5D 5E 5F | PQRSTUVWXYZ[\]^_ |
00000097 | 60 61 62 63 64 65 66 67 | 68 69 6A 6B 6C 6D 6E 6F | `abcdefghijklmno |
00000113 | 70 71 72 73 74 75 76 77 | 78 79 7A 7B 7C 7D 7E 7F | pqrstuvwxyz{|}~ |
00000129 | 80 81 82 83 84 85 86 87 | 88 89 8A 8B 8C 8D 8E 8F | ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏ |
00000145 | 90 91 92 93 94 95 96 97 | 98 99 9A 9B 9C 9D 9E 9F | ђ‘’“”•–—™љ›њќћџ |
00000161 | A0 A1 A2 A3 A4 A5 A6 A7 | A8 A9 AA AB AC AD AE AF |  ЎўЈ¤Ґ¦§Ё©Є«¬®Ї |
00000177 | B0 B1 B2 B3 B4 B5 B6 B7 | B8 B9 BA BB BC BD BE BF | °±Ііґµ¶·ё№є»јЅѕї |
00000193 | C0 C1 C2 C3 C4 C5 C6 C7 | C8 C9 CA CB CC CD CE CF | АБВГДЕЖЗИЙКЛМНОП |
00000209 | D0 D1 D2 D3 D4 D5 D6 D7 | D8 D9 DA DB DC DD DE DF | РСТУФХЦЧШЩЪЫЬЭЮЯ |
00000225 | E0 E1 E2 E3 E4 E5 E6 E7 | E8 E9 EA EB EC ED EE EF | абвгдежзийклмноп |
00000241 | F0 F1 F2 F3 F4 F5 F6 F7 | F8 F9 FA FB FC FD FE FF | рстуфхцчшщъыьэюя |

==>  T

В приведенном примере сначала открывается файл в режиме двоичного вывода. Затем создается буфер размером 256 байтов и в него заносятся байты с кодами от 00 до 255. Далее этот буфер выводится в файл. После чего буфер уничтожается, а файл закрывается. Чтобы убедиться, что буфер действительно выведен в файл, буфер пересоздается, файл снова открывается в режиме двоичного чтения. Затем буфер считывается из файла и дампируется.

FILPUTLINE

Функция FILPUTLINE принимает первый аргумент - идентификатор файла, открытого в режиме _OUTPUT или _APPEND, и второй аргумент - строку. Функция пишет строку, заданную вторым аргументом в файл.

Вот пример работы с функцией FILPUTLINE:

(filOpen 'fo "o.o" _OUTPUT)

==>  fo

(for i 1 10 ((filPutLine 'fo (strCat "i=" (fix2str i)))))

==>  10

(filClose 'fo)

==>  T

(filOpen 'fi "o.o" _TEXT_ARRAY)

==>  fi

(for i 1 10 ((printsline (filGetLine 'fi))))
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9
i=10

==>  10

(filClose 'fi)

==>  T

(gc)

Утилизировано ячеек: 0; атомов: 40.

В примере создается набор данных "o.o" в текущей директории. Затем в этот набор данных выводится последовательность строк вид "i=...". После чего файл закрывается. Далее, чтобы проверить содержимое набора данных создается входной файл с прямым доступом (в режиме _TEXT_ARRAY) и в цикле распечатывается содержимое набора данных "o.o".

Напоминаю, что при открытии выходного файла в режиме _APPEND, старое содержимое набора данных (если до открытия файла он был непуст) сохраняется.

FILSEEK

Функция FILSEEK принимает первый аргумент - идентификатор файла, открытого в режиме _BINARY_READ или _BINARY_READ_WRITE, и второй аргумент - типа FIXED. Функция устанавливает файловый указатель файла на номер байта, заданного вторым аргументом. Следующая операция ввода/вывода начнется с этого байта.

Вот пример работы с функцией FILSEEK:

(bloCreate 'buf 100)

==>  buf

(filOpen 'fo "ooo.ooo" _BINARY_READ_WRITE)

==>  fo

(str2blo 'buf "QWERTYUIOP{}" 10)

==>  T

(bloDump* 'buf)

00000001 | 00 00 00 00 00 00 00 00 | 00 51 57 45 52 54 59 55 | .........QWERTYU |
00000017 | 49 4F 50 7B 7D 00 00 00 | 00 00 00 00 00 00 00 00 | IOP{}........... |
00000033 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000049 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000065 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000081 | 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 | ................ |
00000097 | 00 00 00 00             |                         | ....             |

==>  T

(filPutBlo 'fo 'buf)

==>  T

(filloc 'fo)

==>  101

(filSeek 'fo 15)

==>  T

(filLoc 'fo)

==>  15

(bloCreate 'buf2 5)

==>  buf2

(filGetBlo 'fo 'buf2)

==>  T

(bloDump* 'buf2)

00000001 | 59 55 49 4F 50          |                         | YUIOP            |

==>  T

(filClose 'fo)

==>  T

(bloDestroy 'buf)

==>  T

(bloDestroy 'buf2)

==>  T

(gc)

Утилизировано ячеек: 0; атомов: 15.

В примере создается BLOB (буфер) размером 100 байтов и в него с десятой позиции заносится строка символов. После чего открывается двоичный файл в режиме _BINARY_READ_WRITE. Далее, в набор данных, связанный с файлом, выводится созданный стобайтовый буфер. Запрос значения файлового указателя дает величину 101. После этого вызовом FILSEEK файл позиционируется на 15-байт от начала. Создается буфер на 5 байтов, и в этот буфер читается пять байтов с 15-й позиции файла. Дампирование пятибайтового буфера показывает его содержимое.


Описание графических функций

Описание графических функций HomeLisp

Графика HomeLisp построена на понятии графического окна. Графическое окно представляет собой форму, имеющую в своем составе область рисования (Picture Box), главное меню с двумя пунктами (позволяющими сохранять построенное изображение в файле типа BMP или GIF или вывести изображение на печать), а также статусбар, в котором отображаются координаты курсора мыши при перемещении его над областью рисования. Эта форма не может быть закрыта "крестом", но может быть свернута и развернута. Форма имеет фиксированные размеры (задаваемые при ее создании) и впоследствии не может быть ресайзирована. Для уничтожения формы нужно вызвать специальную функцию (описываемую ниже). Можно удалить форму и вручную с помощью т.н. менеджера графических окон.

Имена всех графических функций имеют префис GRW (от GRaphic Window).

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

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

Одновременно может быть создано до сотни графических окон. (Честно говоря, автор не пробовал создать 100 окон одновременно, но 5-6 штук - легко!).

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

Встроенная в HomeLisp графика, позволяет задавать т.н. мировые координаты - выполнять отображение координат реального объекта на заданное окно. Если такое отображение установлено, то во всех графических функциях следует использовать мировые координаты. Если же отображение не установлено, то следует задавать оконные координаты - точке (0,0) соответствует левый верхний угол окна, а точке (W,H) соответствует правый нижний угол. Здесь W и H - ширина и высота окна в писелах.

Хотя способ представления цвета на компьютере можно считать общеизвестным, для полноты картины ниже приводится его краткое описание. Любой цвет, передаваемый видео-системой компьютера, получается сложением трех основных цветов: красного (Red) , зеленого (Green) и синего (Blue). Это представление называется RGB-представлением. Интенсивность каждой составляющей может быть числом в диапазоне от 0 до 255. Черному цвету соответствует RGB-комбинация (0,0,0), белому - соответственно (255,255,255). Эта комбинация трех чисел обычно записывается, как одно шестнадцатеричное число. Черному цвету соответствует число &H000000, а белому - &HFFFFFF. В HomeLisp для представления цвета используются данные типа BITS.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
GRWARC 7 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-FLOAT;
6-FLOAT;
7-BITS 		Рисует в окне, заданном ПЕРВЫМ аргументом ДУГУ ОКРУЖНОСТИ радиуса, заданного ЧЕТВЕРТЫМ аргументом с центром в точке, заданной парой координат (ВТОРОЙ и ТРЕТИЙ) аргументы. Начальный и конечный угол (в радианах) задаются значениями ПЯТОГО и ШЕСТОГО аргументов. Цвет задается седьмым аргументом.
GRWARCE 8 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-FLOAT;
6-FLOAT;
7-FLOAT;
8-BITS 		Рисует в окне, заданном ПЕРВЫМ аргументом ДУГУ ЭЛЛИПСА радиуса, заданного ЧЕТВЕРТЫМ аргументом с центром в точке, заданной парой координат (ВТОРОЙ и ТРЕТИЙ) аргументы. Начальный и конечный угол (в радианах) задаются значениями ПЯТОГО и ШЕСТОГО аргументов. Аспект (соотношение полуосей) задается СЕДЬМЫМ аргументом. Цвет задается ВОСЬМЫМ аргументом.
GRWBEZIER 2 1-WINDOW;
2-LIST 		Рисует в окне, заданном ПЕРВЫМ аргументом КРИВУЮ БЕЗЬЕ по точкам, заданным в списке - значении второго аргумента
GRWCIRCLE 5 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-BITS; 		Рисует в окне, заданном ПЕРВЫМ аргументом окружность радиуса, заданного ЧЕТВЕРТЫМ аргументом с центром в точке, заданной парой координат (ВТОРОЙ и ТРЕТИЙ) аргументы. Цвет задается ПЯТЫМ аргументом.
GRWCLS | 1 | 1-WINDOW | Стирает содержимое области рисования окна, заданного аргументом
GRWCREATE 5 1-АТОМ;
2-FIXED;
3-FIXED;
4-STRING;
5-BITS 		Создает окно с шириной, заданой ВТОРЫМ параметром и высотой, заданной ТРЕТЬИМ параметром. Значение ЧЕТВЕРТОГО параметра заностися в заголовок окна. Цвет фона задается ПЯТЫМ параметром. Атом, заданный первым параметром, получает в список свойств индикатор WINDOW. Размеры задаются в пикселах.
GRWDESTROY | 1 | 1-WINDOW | Уничтожает окно, заданное единственным параметром.
GRWDESTROYALL - - Уничтожает все графические окна
GRWDRAW 2 1-WINDOW;
2-LIST 		Выполняет пакет графических команд, заданных вторым параметром.
GRWELLIPSE 6 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-BITS;
6-FLOAT; 		Рисует в окне, заданном ПЕРВЫМ аргументом эллипс радиуса, заданного ЧЕТВЕРТЫМ аргументом с центром в точке, заданной парой координат (ВТОРОЙ и ТРЕТИЙ) аргументы. Аспект (соотношение полуосей) задается ШЕСТЫМ аргументом. Цвет задается ПЯТЫМ аргументом.
GRWFILL 5 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-BITS;
5-BITS; 		Выполняет заливку замкнутого контура. Цвет контура (границы) задается ЧЕТВЕРТЫМ параметром. Цвет заливки - ПЯТЫМ. Заливка начинается в точке с координатами, заданными ПЕРВЫМ и ВТОРЫМ параметрами.
GRWFONT 5 1-WINDOW;
2-STRING;
3-FIXED;
4-BOOL;
5-BOOL 		Устанавливает для графического окна, заданного ПЕРВЫМ параметром, фонт c гарнитурой, заданной ВТОРЫМ параметром и размером, заданным ТРЕТЬИМ параметром. ЧЕТВЕРТЫЙ и ПЯТЫЙ параметры задают соответственно ЖИРНЫЙ и КУРСИВ соответственно.
GRWFRECT 6 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-FLOAT;
6-BITS; 		Рисует в окне, заданным ПЕРВЫМ параметром, заполненный прямоугольник. Координаты левого верхнего угла задаются ВТОРЫМ и ТРЕТЬИМ параметром; координаты правого нижнего угла задаются ЧЕТВЕРТЫМ и ПЯТЫМ параметрами. Цвет задается ШЕСТЫМ параметром.
GRWHIDE | 1 | 1-WINDOW | Скрывает окно, заданное единственным параметром.
GRWHIDEALL - - Скрывает все графические окна.
GRWINKEY | 2 | 1-WINDOW; 2-FIXED | Возвращает код нажатой клавиши из буфера клавиатуры заданного окна или 0, если буфер клавиатуры пуст. Окно при вызове обновляется.
GRWLINE 6 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-FLOAT;
6-BITS; 		Рисует в окне, заданным ПЕРВЫМ параметром, прямую линию. Координаты левого верхнего угла задаются ВТОРЫМ и ТРЕТЬИМ параметром; координаты правого нижнего угла задаются ЧЕТВЕРТЫМ и ПЯТЫМ параметрами. Цвет задается ШЕСТЫМ параметром.
GRWLOADPIC 3 1-WINDOW;
2-STRING;
3-FIXED 		Загружает в окно, заданное ПЕРВЫМ параметром графическое изображение из файла c именем, заданным ВТОРЫМ параметром. ТРЕТИЙ параметр задает режим сжатия/растяжения изображения. Если значение ТРЕТЬЕГО параметра - не нуль, то изображение "подгоняется" под размер окна. В противном случае растяжения/сжатия не происходит.
GRWPOINT 3 1-WINDOW;
2-FLOAT;
3-FLOAT; 		Возвращает цвет точки с координатами, заданными ВТОРЫМ и ТРЕТЬИМ параметрами в окне, заданном ПЕРВЫМ параметром..
GRWPRINT 5 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-STRING;
5-BITS 		Выводит в окне, заданным ПЕРВЫМ параметром, текст, заданный ЧЕТВЕРТЫМ параметром. Вывод начинается с позиции с координатами, заданными ПЕРВЫМ и ВТОРЫМ параметрами. Цвет текста задается ПЯТЫМ параметром. Используется текущий фонт.
GRWPSET 4 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-BITS; 		Ставит в окне, заданном ПЕРВЫМ параметром точку цветом, который задается ЧЕТВЕРТЫМ параметром. Координаты точки задаются ВТОРЫМ и ТРЕТЬИМ параметрами.
GRWRECT 6 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-FLOAT;
6-BITS; 		Рисует в окне, заданным ПЕРВЫМ параметром, НЕзаполненный прямоугольник. Координаты левого верхнего угла задаются ВТОРЫМ и ТРЕТЬИМ параметром; координаты правого нижнего угла задаются ЧЕТВЕРТЫМ и ПЯТЫМ параметрами. Цвет задается ШЕСТЫМ параметром.
GRWREFRESH 2 1-WINDOW;
2-FIXED 		Обновляет окно, заданное ПЕРВЫМ параметром. Второй параметр задает задержку (в миллисекундах) после обновления.
GRWSAVEPIC 3 1-WINDOW;
2-FIXED;
3-STRING 		Сохраняет графическое изображение из окна, заданного ПЕРВЫМ параметром в файле c именем, заданным ТРЕТЬИМ параметром. Тип файла задается значением ВТОРОГО параметра (0-BMP; 1-GIF).
GRWSCALE 5 1-WINDOW;
2-FLOAT;
3-FLOAT;
4-FLOAT;
5-FLOAT 		Для окна, заданного ПЕРВЫМ параметром устанавливает систему мировых координат. ВТОРОЙ и ТРЕТИЙ параметры задают минимум и максимум по оси ОХ. ЧЕТВЕРТЫЙ и ПЯТЫЙ параметры задают минимум и максимум по оси ОY.
GRWSETPARM 5 1-WINDOW;
2-FIXED;
3-FIXED;
4-BITS;
5-BITS 		Для окна, заданного ПЕРВЫМ параметром устанавливает ширину рисуемых линий (ВТОРОЙ параметр); стиль заполнения (ТРЕТИЙ параметр); цвет заполнения (ЧЕТВЕРТЫЙ параметр) и цвет переднего плана (ПЯТЫЙ параметр).
GRWSHOW | 3 | 1-WINDOW, [2,3 - FIXED] | Отображает окно, заданное первым параметром.
GRWSHOWALL | 2 | 1-WINDOW, 2-FIXED | Отображает все графические окна. Стиль отображения задается вторым параметром.
GRWTEXTH 2 1-WINDOW;
2-STRING 		Возвращает ВЫСОТУ прямоугольника, объемлющего текст, заданный ВТОРЫМ параметром в окне, заданным ПЕРВЫМ параметром.
GRWTEXTW 2 1-WINDOW;
2-STRING 		Возвращает ШИРИНУ прямоугольника, объемлющего текст, заданный ВТОРЫМ параметром в окне, заданным ПЕРВЫМ параметром.
GRWTRANSPARENT 2 1-WINDOW;
2-FIXED 		Задает прозрачность окна, заданного ПЕРВЫМ параметром. Прозрачность задается значением ВТОРОГО параметра, которое должно лежать в диапазоне 0 - 255.
GRWARC

Функция GRWARC принимает семь аргументов: идентификатор окна, координаты центра, радиус окружности, угол начала и угол конца дуги, цвет задается последним параметром (битовой шкалой). Вот подробный пример рисования дуги.

(grwCreate 'w 200 200 "Проба" &HFFFFFF)

==> w

(grwShow 'w)

==> w

(grwScale 'w -100 100 -100 100)

==> w

(grwArc 'w 0 0 30 0 (/ _pi 2) &HFF0000)

==> w

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

GRWARCE

Функция GRWARCE принимает восемь аргументов: идентификатор окна, координаты центра, радиус окружности, угол начала и угол конца дуги. Седьмой аргумент задает аспект (соотношение осей). Цвет дуги задается последним параметром (битовой шкалой). Вот подробный пример рисования дуги эллипса.

(grwCreate 'w 200 200 "Проба" &HFFFFFF)

==> w

(grwShow 'w)

==> w

(grwScale 'w -100 100 -100 100)

==> w

(grwArc 'w 0 0 30 0 _pi 2 &HFF)

==> w

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

GRWBEZIER

Рисование кривых Безье (сплайнов) входят в стандартный графический интерфейс Windows (GDI). Добавить соответствующую функцию в число графических функций HomeLisp оказалось несложно. Функция GRWBEZIER принимает два параметра - идентификатор графического окна и список пар xi,yi. Для рисования одной кривой пар должно быть 4 - они задают опорные точки сплайна. Сам сплайн рисуется установленным цветом и жирностью. Если нужно нарисовать серию кривых, то для рисования первой кривой задются 4 опорные точки, а для каждой последующей - по три (начальная точка следующей кривой будет совпадать с концом предыдущей). Вот пример:

(prog (w x0 y0 x1 y1 x2 y2 x3 y3 xx0 xx1 xx2 xx3 yy0 yy1
         yy2 yy3 ff fi sinfi cosfi)

      (grwCreate 'w 400 400 "Набор кривых Безье" _WHITE)

      (grwScale 'w -120 120 -120 120)

      (grwSetParm 'w 2 7 _GREEN _BLUE)

      (grwShow 'w)

      (setq x0 10)
      (setq y0 10)

      (setq x1 -20)
      (setq y1 20)

      (setq x2 70)
      (setq y2 -70)

      (setq x3 80)
      (setq y3 80)

      (dolist (ff '(0 45 90 135 180 225 270 315) T)

                   (setq fi (* _Pi (/ ff 180.)))
                   (setq sinfi (sin fi))
                   (setq cosfi (cos fi))

                   (setq xx0 (+ (* x0 cosfi) (* y0 sinfi)))
                   (setq yy0 (- (* y0 cosfi) (* x0 sinfi)))

                   (setq xx1 (+ (* x1 cosfi) (* y1 sinfi)))
                   (setq yy1 (- (* y1 cosfi) (* x1 sinfi)))

                   (setq xx2 (+ (* x2 cosfi) (* y2 sinfi)))
                   (setq yy2 (- (* y2 cosfi) (* x2 sinfi)))

                   (setq xx3 (+ (* x3 cosfi) (* y3 sinfi)))
                   (setq yy3 (- (* y3 cosfi) (* x3 sinfi)))

                   (grwBezier 'w (list xx0 yy0 xx1 yy1 xx2 yy2 xx3 yy3))

                   (setq xx1 (+ (* (- x1) cosfi) (* (- y1) sinfi)))
                   (setq yy1 (- (* (- y1) cosfi) (* (- x1) sinfi)))

                   (setq xx2 (+ (* (- x2) cosfi) (* (- y2) sinfi)))
                   (setq yy2 (- (* (- y2) cosfi) (* (- x2) sinfi)))

                   (grwBezier 'w (list xx0 yy0 xx1 yy1 xx2 yy2 xx3 yy3))

      )

  @Ink

   (if (= (grwInkey 'w 100) 0) (go @Ink) nil)

   (grwHide 'w)

   (grwDestroy 'w)

   (return 'Ok)

)

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

GRWCIRCLE

Функция GRWCIRCLE принимает пять аргументов: идентификатор окна, координаты центра, радиус окружности, и цвет окружности. Вот подробный пример рисования окружности.

(grwCreate 'w 200 200 "Проба" &HFFFFFF)

==> w

(grwShow 'w)

==> w

(grwScale 'w -100 100 -100 100)

==> w

(grwCircle 'w 0 0 30 &HFF00)

==> w

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

GRWCLS

Функция GRWCLS принимает один аргумент - идентификатор окна. Результатом работы функции является стирание графического окна.

GRWCREATE

Функция GRWCREATE является центральной функцией графической подсистемы HomeLisp. Функция создает графическое окно. Первым параметром функции должен быть "чистый" (не имеющий никаких индикаторов) атом. Этот атом при успешном завершении получит в свой список свойств индикатор WINDOW. Второй и третий параметры функции задают размеры формы (в пискелах). Четвертый параметр задает заголовок окна. Пятый параметр задает цвет фона области рисования.

(grwCreate 'w 300 300 "Окно" &HEEDDBB)

==> w

(grwShow 'w)

==> w

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

Полезно посмотреть список свойств атома - идентификатора окна:

(proplist 'w)

==>  (WINDOW width 300 height 300 title
             "Окно" backcolor &HEEDDBB WINHANDLE 1)

Из списка свойств отчетливо видны все свойства сконструированного окна. Последний параметр (WINHANDLE) - это внутренний номер окна. Если же попытаться создать окно с атомом-идентификатором, который уже имеет какие-либо стадартные флаги FIXED, FLOAT, STRING и др. ("нечистый" атом), то окно не создается и выводится сообщение об ошибке:

(grwCreate 1 200 200 "" &HFFFFFF)

grwCreate: недопустимый флаг у первого аргумента
==>  ERRSTATE

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

(grwCreate 'w 200 200 "" &HFFFFFF)

==>  w

(grwCreate 'w 300 300 "" &H00FF00)

grwCreate: окно w уже существует
==>  ERRSTATE

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

(spropl 'w '(a s d))

==>  (a s d)

(proplist 'w)

==>  (a s d)

(grwCreate 'w 200 300 "Окно" _AQUA)

==>  w

(proplist 'w)

==>  (WINDOW width 200 height 300 title
             "Окно" backcolor &H00FFFF WINHANDLE 1)

Параметры созданного окна можно менять, модифицируя список свойств окна. Для этого можно использовать функцию PUTPROP. Допустимо менять индикаторы width, height, title и backcolor. Параметр winhandle менять нельзя (при попытке будет возбуждено состояние ошибки). Изменение параметров в списке свойств окна вызывает мгновенное изменение соответствующих внешних свойств. При этом проверяется тип устанавливаемого значения (для ширины и высоты - FIXED/FLOAT, для заголовка - STRING, для фона - BITS); при несоответствии типа возбуждается ошибка и модификация не выполняется.

Рассмотрим пример. Создадим и отобразим окно, выполнив следующий код:

(grwCreate 'w1 200 200 "Окно" _WHITE)

==> w1

(grwShow 'w1)

==> w1

Результат показан на приведенном ниже рисунке:

Теперь заменим в списке свойств окна w1 значение индикатора backcolor на константу _LIME, а индикатор width на 300:

(putprop 'w1 'backcolor _LIME)

==> w1

(putprop 'w1 'width 300)

==> w1

Результат представлен на рисунке:

GRWDESTROY

Функция GRWDESTROY уничтожает ранее созданное графическое окно.

(grwCreate 'w 300 300 "Окно" &HEEDDBB)

==> w

(grwShow 'w)

==> w

(proplist 'w)

==> (WINDOW width 300 height 300 title "Окно" backcolor &HEEDDBB WINHANDLE 1)

(grwDestroy 'w)

==> T

(proplist 'w)

==> Nil

Видно, что после уничтожения окна список свойст идентификатора окна очищается.

GRWDESTROYALL

Функция GRWDESTROY уничтожает все ранее созданные графические окна (независимо от того, отображены они или нет). Функция не требует параметров.

(grwCreate 'w1 300 300 "Окно-1" _RED)

==> w1

(grwCreate 'w2 300 300 "Окно-2" _LIME)

==> w2

(proplist 'w1)

==> (WINDOW width 300 height 300 title "Окно-1" backcolor &HFF0000 WINHANDLE 1)

(proplist 'w2)

==> (WINDOW width 300 height 300 title "Окно-2" backcolor &H00FF00 WINHANDLE 2)

(grwDestroyAll)

==> T

(proplist 'w1)

==> NIL

(proplist 'w2)

==> NIL

Видно, что один вызов GRWDESTROYALL уничтожает оба созданных ранее окна.

GRWDRAW

Функция GRWDRAW реализует т.н. "черепашью графику". Будем считать, что с графическим окном связано перо, которое может перемещаться по области рисования (оставляя или не оставляя следа). Управляют пером специальные графические команды, которые задает второй параметр функции GRWDRAW. Этот параметр должен быть списком. Первый параметр функции - как всегда, идентификатор окна.

Список графических команд имеет вид:

(Код_команды Число Код_команды Число Код_команды Число ...)

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

Команда | Описание
При выполнении этих команд перо оставляет след и координаты графического курсора меняются
U nn | Сдвиг вверх на величину nn
D nn | Сдвиг вниз на величину nn
L nn | Сдвиг влево на величину nn
R nn | Сдвиг вправо на величину nn
E nn | Сдвиг по диагонали вправо и вверх на величину nn
F nn | Сдвиг по диагонали вправо и вниз на величину nn
G nn | Сдвиг по диагонали влево и вниз на величину nn
H nn | Сдвиг по диагонали влево и вверх на величину nn
При выполнении этих команд перо не оставляет следа, а координаты графического курсора меняются
BU nn | Сдвиг вверх на величину nn
BD nn | Сдвиг вниз на величину nn
BL nn | Сдвиг влево на величину nn
BR nn | Сдвиг вправо на величину nn
BE nn | Сдвиг по диагонали вправо и вверх на величину nn
BF nn | Сдвиг по диагонали вправо и вниз на величину nn
BG nn | Сдвиг по диагонали влево и вниз на величину nn
BH nn | Сдвиг по диагонали влево и вверх на величину nn
При выполнении этих команд перо оставляет след, но координаты графического курсора не меняются
NU nn | Сдвиг вверх на величину nn
ND nn | Сдвиг вниз на величину nn
NL nn | Сдвиг влево на величину nn
NR nn | Сдвиг вправо на величину nn
NE nn | Сдвиг по диагонали вправо и вверх на величину nn
NF nn | Сдвиг по диагонали вправо и вниз на величину nn
NG nn | Сдвиг по диагонали влево и вниз на величину nn
NH nn | Сдвиг по диагонали влево и вверх на величину nn
Команда абсолютного или относительного сдвига пера в заданную точку
M [+/-] xx [+/-] yy | Перемещение пера в точку с координатами (xx,yy). Если перед величиной xx/yy стоит знак плюс или минус, то новое значение координаты получается из текущего значения координаты пера увеличением (уменьшением) на величину xx(yy) - выполняется относительный сдвиг.
Команда поворота системы координат.
T fi | Выполняет поворот системы координат на угол fi (в градусах). Если значение fi положительно, то поворот выполняется против часовой стрелки. В противном случае поворот осуществляется по часовой стрелке.
Команда установки цвета линии.
С Сolor | Задает цвет линии, которую будет проводить перо. Параметр Color должен иметь тип BITS.
Команда установки цвета заливки.
P Сolor-1 Color-2 | Закрашивает область, ограниченную контуром цвета Color-2 цветом Color-1. Параметры Color-1 и Color-2 должны иметь тип BITS.

Ниже приводится пример использования функции GRWDRAW. В двух окнах рисуются две звезды - с шестью лучами и с восемнадцатью лучами.

(grwCreate 'w1 300 300 "Пробное окно - 1" &HFFFFFF)

==> w1

(grwScale 'w1 -100 100 -100 100)

==> w1

(grwSetParm 'w1 1 0 &HFFFFFF &HFF0000)

==> w1

(grwShow 'w1)

==> w1

(setq fi 60.0)

==> 60.0

(grwDraw 'w1 '(C &H0000FF))

==> w1

(for i 1 6 ( (grwDraw 'w1 '(U 20 R 20))
             (grwDraw 'w1 (list T fi))
             (setq fi (+ fi 60.0))
            )
)

==> 6

(grwDraw 'w1 '(M -7 30 P &HEE88AA &H0000FF))

==> w1

(grwCreate 'w2 300 300 "Пробное окно - 2" &HFFFFFF)

==> w2

(grwScale 'w2 -100 100 -100 100)

==> w2

(grwSetParm 'w2 1 0 &HFFFFFF &H0000FF)

==> w2

(grwShow 'w2)

==> w2

(setq fi 20.0)

==> 20.0

(grwDraw 'w2 '(M 50 -60))

==> w2

(for i 1 18 ( (grwDraw 'w2 '(U 20 R 20))
              (grwDraw 'w2  (list T fi))
              (setq fi (+ fi 20.0))
            )
)

==> 18

(grwDraw 'w2 '(M 0 0 P &H6688AA &H0000FF))

==> w2

А вот результат выполнения этой программы:

GRWELLIPSE

Функция GRWELLIPSE рисует эллипс. Первый параметр, как всегда, задает окно. Второй и третий параметры задают координаты центра эллипса. Четвертый параметр задает радиус, а пятый - цвет эллипса. Отношение полуосей эллипса (аспект) задает последний, шестой параметр. Если выполнить приведенные ниже команды:

(grwCreate 'w 300 300 "Эллипс" _WHITE)

==> w

(grwEllipse 'w 100 100 60 _RED 0.7)

==> w

то можно увидеть такую картинку:

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

(grwCls 'w)

==> w

(grwEllipse 'w 120 120 80 _BLUE 1.4)

==> w

то получим:

GRWFILL

Функция GRWFILL выполняет заливку замкнутого контура заданным цветом заливки. Первым параметром функции должен быть идентификатор окна. Второй и третий параметр задают координаты точки, куда будет "литься" краска. Будем называть эту точку начальной точкой заливки. Четвертый параметр задает цвет заливки, а пятый - цвет ограничивающего контура. Ниже приводится пример заливки внутренности эллипса:

(grwCreate 'w 300 300 "Эллипс" _WHITE)

==> w

(grwEllipse 'w 120 120 80 _BLUE 1.4)

==> w

(grwFill 'w 120 120 _YELLOW _BLUE)

==> w

Результат будет таким:

Функция GRWFILL коварна - если неверно задать координаты точки заливки, то результат может оказаться обескураживающим - вместо заливки внутренности контура может оказаться залитым все окно, а внутренность контура как раз останется незалитой. В этом можно убедиться, выполнив следующие команды:

(grwCls 'w)

==> w

(grwEllipse 'w 120 120 80 _BLUE 1.4)

==> w

(grwFill 'w 246 150 _YELLOW _BLUE)

==> w

Результат будет таким:

Заливка выполняется следующим образом. Из начальной точки заливки цвет распространяется во все стороны, пока не встретит точку с цветом контура (последний параметр). Если начальная точка заливки лежит вне контура, то цветом будет залита вся область окна, за исключением контура. Внутрь контура краска не распространится.

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

(grwCreate 'w 300 300 "Заливка" _WHITE)

==> w

(grwShow 'w)

==> w

(grwArc 'w 100 100 80 0 6 _BLUE)

==> w

(grwFill 'w 100 100 _YELLOW _BLUE)

==> w

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

Осталось добавить, что для заливки простейших объектов (боксов, кругов, эллипсов) нет смысла использовать GRWFILL - для кругов и эллипсов можно установить стиль заполнения, а для рисования заполненных боксов есть специальная функция.

GRWFONT

Функция GRWFONT устанавливает некоторые параметры фонта для вывода текста. Первым параметром вызова должен быть идентификатор окна. Вторым параметром должен быть атом строкового типа, задающий имя гарнитуры фонта. Третий параметр задает размер шрифта в пунктах. Четвертый и пятый параметры должны принимать значения T или Nil. Четвертый параметр задает жирность шрифта (T - жирный, Nil - обычный). Пятый параметр задает курсив (T - курсив, Nil - обычный). По умолчанию у графического окна устанавливается шрифт "Courier New" с размером 8 пунктов.

Вот пример использования разных шрифтов:

(grwCreate 'w 300 300 "Разные шрифты" _WHITE)

==> w

(grwFont 'w "Times New Roman" 14 T Nil)

==> w

(grwPrint 'w 1 10 "Проба пера" _BLUE)

==> w

(grwFont 'w "Tahoma" 9 T T)

==> w

(grwPrint 'w 1 40 "Проба пера" _NAVY)

==> w

Результат будет таким:

GRWFRECT

Функция GRWFREC рисует заполненный прямоугольник. Первый параметр - идентификатор окна. Второй и третий параметры - координаты левого верхнего угла прямоугольника. Четвертый и пятый параметры - координаты правого нижнего угла. Последний параметр битового типа задает цвет прямоугольника. Пример вызова функции GRWFREC приводится ниже:

(grwCreate 'w 300 300 "Заполненные прямоугольники" _WHITE)

==> w

(grwFRect 'w 10 10 100 100 _MAROON)

==> w

(grwFRect 'w 50 50 160 160 _YELLOW)

==> w

(grwFRect 'w 200 60 250 110 _GREEN)

==> w

Результат будет таким:

GRWHIDE

Функция GRWHIDE скрывает (но не уничтожает) окно, идентификатор которого задан ее единственным параметром. Если параметр функции не является идентификатором никакого окна, то возникает ошибка. Если окно с заданным идентификатором уже скрыто - функция не выполняет никаких действий.

GRWHIDEALL

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

GRWINKEY

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

(defun kbd (wi) (prog (x)

          GetKey (setq x (grwInkey wi))

                 (cond ((eq x 0) (go GetKey)))

                 (return x)

                )
)

==> kbd

Если теперь для существующего окна с идентификатором w вызвать функцию kbd так: (kbd 'w), среда HomeLisp не вернет управления до тех пор, пока пользователь не нажмет какую-либо клавишу, предварительно активизировав окно w.

Основное назначение функции GRWINKEY - организация графических меню. Пример такого меню приведен в разделе "Примеры программ".

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

Клавиша | Код
Стрелка вверх | 38
Стрелка вниз | 40
Стрелка вправо | 39
Стрелка влево | 37
Insert | 45
Home | 36
Page Up | 38
Page Down | 34
End | 35
F1 - F12 | 112 - 123

Если одновременно с какой-либо клавишей нажат клавишный переключатель (Ctrl, Shift или Alt), то функция GRWINKEY вернет целое, вычисляемое по следующему правилу:

Код=(Код_ASCII)+256*Ctrl+512*Shift+1024*Alt

где Ctrl, Shift и Alt есть целые числа, равные единице, если нажат соответствующий переключатель и нулю в противном случае. Так, если нажато сочетание Shift-2, то функция GRWINKEY вернет код 562, что, в свою очередь, равно 50+512 (50 - скан-код "2", а 512 - признак Shift).

GRWLINE

Функция GRWLINE рисует отрезок прямой. Первым параметром функции является идентификатор окна. Второй и третий пареметры - это координаты левого начала отрезка. Четвертый и пятый параметры - это координаты конца отрезка. Шестой параметр задает цвет отрезка. Толщина линии определяется текущими установками (см. функцию GRWSETPARM). Если для окна установлена система мировых координат (масштаб), то параметры 2-4 должны быть мировыми координатами, в противном случае используется экранная система координат. Вот примеры рисования отрезков:

(grwcreate 'w 300 300 "Отрезки" _WHITE)

==> w

(grwLine 'w 10 10 90 90 _BLUE)

==> w

(grwsetparm 'w 2 1)

==> w

(grwLine 'w 20 10 90 120 _RED)

==> w

(grwScale 'w -100 100 -100 100)

==> w

(grwLine 'w -100 0 100 0 _BLACK)

==> w

В приводимом выше наборе команд выполняются следующие действия:

- Создается окно 300*300 с белым фоном;

- Проводится голубая линия от точки (10,10) к точке (90,90);

- Для окна устанавливается двойная толщина рисования;

- Проводится красная линия от точки (20,10) к точке (90,120);

- Для окна задается масштаб по обеим осям от -100 до 100;

- Проводится горизонтальная черная линия от точки (-100,0) к точке (100,0).

Все это порождает следующую картинку:

GRWLOADPIC

Функция GRWLOADPIC загружает в окно с идентификатором, заданным первым параметром, графическое изображение из файла, с именем, заданным вторым параметром. Поддерживаются форматы графических файлов BMP, GIF и JPG. Третий параметр может принимать значение 0 или 1. Нуль означает, что изображение не подстраивается под размеры окна (не растягивается и не сжимается). Если значение третьего параметра равно единице, то изображение подстраивается под размеры окна. Все это демонстрируется следующим примером:

(grwCreate 'w1 300 400 "Кошка" &HFFFFFF)

 ==>  w1

(grwShow 'w1)

 ==>  w1

(grwLoadPic 'w1 "..\cat-117.jpg" 0)

 ==>  w1

(grwCreate 'w2 300 400 "Кошка" &HFFFFFF)

 ==>  w2

(grwShow 'w2)

 ==>  w2

(grwLoadPic 'w2 "..\cat-117.jpg" 1)

 ==>  w2

Здесь создаются два окна. В оба загружается одна и та же картинка с растяжением и без. Результат будет таков:

Результ работы (grwLoadPic 'w1 "..\cat-117.jpg" 1) Последний параметр равен 1 выполняется растяжение картинки. | Результ работы (grwLoadPic 'w2 "..\cat-117.jpg" 0) Последний параметр равен 0 растяжение картинки не выполняется.
GRWPOINT

Функция GRWPOINT возвращает цвет точки окна, заданного первым параметром и координатами, заданными вторым и третьим параметрами. Цвет представляется битовой шкалой (тип BITS). Если для окна установлена система мировых координат (масштаб), то параметры 2 и 3 должны быть мировыми координатами, в противном случае используется экранная система координат. Вот пример вызова функции GRWPOINT:

(grwcreate 'w 300 300 "Проба" _WHITE)

==> w

(grwScale 'w -100 100 -100 100)

==> w

(grwFrect 'w -50 -50 50 50 _RED)

==> w

(grwPoint 'w 0 0)

==> &HFF0000

(grwPoint 'w 90 90)

==> &HFFFFFF

Здесь создается окно, устанавливается масштаб (-100,-100) - (100,100) и в центре окна рисуется красный прямоугольник. Затем проверяется цвет точки в центре прямоугольника. Он получается красным. А цвет точки за пределами прямоугольника оказывется, естественно, белым.

GRWPRINT

Функция GRWRINT предназначена для вывода текста в графическом окне. Первый параметр функции, как обычно, задает окно. Второй и третий параметры задают левый верхний угол прямоугольника, в котором будет выводиться текст. Если для окна установлена система мировых координат (масштаб), то эти параметры должны быть мировыми координатами, в противном случае используется экранная система координат. Сам текст задается четвертым параметром. Цвет текста задается пятым параметром. Используется текщий фонт (который можно сменить, вызвав функцию GRWFONT). Вот комплексный пример, демонстрирующий возможности функции GRWPRINT:

(grwCreate 'w 300 300 "Проба печати" &HAADDFF)

==> w

(grwSetParm 'w 1 0 nil &HFF0000)

==> w

(grwShow 'w)

==> w

(setq y 1)

==> 1

(setq z 8)

==> 8

(for i 1 10 (
              (grwFont 'w "Tahoma" z T T)

              (grwPrint 'w 1 y "проба")

              (setq y (+ y 20))

              (setq z (+ z 1))
            )
)

==> 10

Выполнив этот код, пользователь должен увидеть вот такую картинку:

GRWPSET

Функция GRWPSET ставит точку заданного цвета в заданных координатах. Первым параметром функции является идентификатор окна. Второй и третий пареметры - это координаты точки. Третий параметр задает цвет отрезка. "Размер" точки зависит от текущих установок окна (см. функцию GRWSETPARM). Если для окна установлена система мировых координат (масштаб), то параметры 2 и 3 должны быть мировыми координатами, в противном случае используется экранная система координат. Вот примеры рисования точек:

(grwCreate 'w 200 200 "Точки" _WHITE)

==> w

(grwShow 'w)

==> w

(grwPset 'w 2 2 _BLUE)

==> w

(grwSetParm 'w 3)

==> w

(grwPset 'w 10 10 _BLUE)

==> w

(grwPset 'w 20 20 _RED)

==> w

(grwPset 'w 40 40 _LIME)

==> w

(grwPset 'w 50 50 _GREEN)

==>  w

(grwPset 'w 60 60 _TEAL)

==> w

В приводимом выше наборе команд выполняются следующие действия:

- Создается окно 200*200 с белым фоном;

- Ставится голубая точка в координатах (2,2). Поскольку параметр жирности по умолчанию равен 1, точка еле видна ;

- Для окна устанавливается тройная толщина рисования;

- Ставится несколько цветных точек.

Все это порождает следующую картинку:

GRWRECT

Функция GRWRECT рисует незаполненный прямоугольник. Первый параметр - идентификатор окна. Второй и третий параметры - координаты левого верхнего угла прямоугольника. Четвертый и пятый параметры - координаты правого нижнего угла. Последний параметр битового типа задает цвет прямоугольника. Пример вызова функции GRWRECT приводится ниже:

(grwCreate 'w 300 300 "Незаполненные прямоугольники" _WHITE)

==> w

(grwSetParm 2)

==> w

(grwRect 'w 10 10 100 100 _MAROON)

==> w

(grwRect 'w 50 50 160 160 _YELLOW)

==> w

(grwRect 'w 200 60 250 110 _GREEN)

==> w

Результат будет таким:

GRWREFRESH

Функция GRWREFRESH обновляет окно, идентификатор которого задан первым параметром вызова. Обновление заключается в том, что выполняются все графические операции, которые, возможно, еще не были выполнены (находились в очереди). Второй параметр (типа FIXED) необязательный. Если он задан, то после обновления окна выполняется задержка выполнения на количество миллисекунд, равное значению второго параметра. Основное назначение функции GRWREFRESH - обновление окна во время анимации (Да-да! Это тоже возможно!)

GRWSAVEPIC

Функция GRWSAVEPIC сохраняет графическое изображение в файле. Поддерживаются два формата сохранения BMP и GIF. Для сохранения графического изображения в формате GIF использована программа, исходный текст которой любезно опубликовал в 2001 г. Владислав Помелов. Автору настоящего руководства удалось ускорить работу опубликованной программы в 3-4 раза.

Первый параметр функции GRWSAVEPIC - идентификатор окна. Второй параметр (FIXED) - формат сохранения (0-BMP; 1-GIF). Третий параметр (STRING) - имя выходного файла. Ниже приводится пример сохранения изображения:

(grwCreate 'w 300 300 "Пример" _WHITE)

==> w

(grwSetParm 2)

==> w

(grwFRect 'w 10 10 100 100 _MAROON)

==> w

(grwFRect 'w 50 50 160 160 _YELLOW)

==> w

(grwFRect 'w 200 60 250 110 _GREEN)

==> w

(grwSavePic 'w 1 "myPic.gif")

==> w

После выполнения этого кода в текущей директории образуется файл MyPic.gif. Этот файл можно посмотреть:

GRWSCALE

Функция GRWSCALE устанавливает систему мировых координат для заданного окна. Первым параметром задается идентификатор окна. Второй и третий параметры задают минимум и максимум по оси ОX. Четвертый и пятый параметры задают минимум и максимум по оси ОY. Если для заданного окна уже ранее была установлена система координат, то ее установки сбрасываются и заменяются новыми.

Функция GRWSCALE чрезвычайно удобна, поскольку позволяет рисовать реальные объекты без утомительного пересчета мировых координат в экранные. Так для построения графика функции в диапазоне [Xmin, Xmax] достаточно вычислить значения функции на этом отрезке с досточно мелким шагом и определить максимальное и минимальное значения функции (Ymin и Ymax). После этого следует установить масштаб окна, вызвав функцию GRWSCALE так: (GRWSCALE 'w Xmin Xmax Ymin Ymax). После этого можно просто ставить точки, используя координаты (x,y), где (Xmin <= x <= Xmax) и (Ymin <= y <= Ymax).

GRWSETPARM

Функция GRWSETPARM позволяет для окна (заданного первым параметром) установить некоторые графические характеристики.

Второй параметр задает ширину линий при рисовании. По умолчанию значение ширины равно 1 (один пиксел).

Третий параметр задает параметры заполнения (для рисования кругов и эллипсов). Этот параметр должен иметь тип FIXED. Значение параметра лежит в диапазоне [0,7]. Ниже приводится табличка, содержащая смысл каждого из значений:

Значение параметра | Выполняемое действие
0 | Сплошная заливка
1 | Нет заливки
2 | Горизонтальная штриховка
3 | Вертикальная штриховка
4 | Диагональная из левого верхнего угла в правый нижний
5 | Диагональная штриховка из правого верхнего угла в левый нижний
6 | Горизотнальная и вертикальная штриховка (стили 1 и 2 совместно)
7 | Диагональная штриховка (стили 4 и 5 совместно)

Четвертый параметр задает цвет заливки (должен иметь тип BITS).

Вот пример установки параметров окна:

(grwCreate 'w 300 300 "Заливка" _WHITE)

==> w

(grwShow 'w)

==> w

(grwSetParm 'w 3 7 _GREEN)

==> w

(grwCircle 'w 50 50 40 _YELLOW)

==> w

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

GRWSHOW

Функция GRWSHOW отображает окно, идентификатор которого задан ее первым параметром и устанавливает на это окно фокус ввода. Если параметр функции не является идентификатором никакого окна, то возникает ошибка. Если окно с заданным идентификатором уже отображено и имеет фокус ввода - функция не выполняет никаких действий.

Второй и третий параметры являются необязательными. Они задают координаты в пикселах левого верхнего угла графического окна (второй параметр - Х, третий - Y).

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

GRWSHOWALL

Функция GRWSHOWALL отображает все графические окна, существующие в момент вызова. Функция GRWSHOWALL доступна в среде IDE и в составе exe-файла. В скриптах функция недоступна. Функция требует единственный параметр типа FIXED, принимающий одно из значений:

0 - отображать окна без позиционирования;

1 - отображать окна каскадом;

-1 - отображать окна таблицей;

Ниже приводится пример табличного расположения пяти графических окон:

Те же самые окна, отображенные каскадом, выглядят так:

GRWTEXTH

Функция GRWTEXTH возвращает высоту прямоугольника, объемлющего текст, заданный вторым параметром функции (а первый параметр - это, как всегда, идентификатор окна).

Ниже приводится пример использования функции GRWTEXTH (а также функции GRWTEXTW) для риcования объемлющего прямоугольника, заключающего текст.

(grwCreate 'w 200 200 "Текст" _WHITE)

==> w

(grwShow 'w)

==> w

(grwFont 'w "Tahoma" 14 T)

==> w

(grwTexth 'w "Проба")

==> 23

(grwTextw 'w "Проба")

==> 62

(grwPrint 'w 20 20 "Проба" _RED)

==> w

(grwRect 'w 20 20 82 43 _NAVY)

==> w

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

GRWTEXTW

Функция GRWTEXTW возвращает ширину прямоугольника, объемлющего текст, заданный вторым параметром функции (а первый параметр - это, как всегда, идентификатор окна).

Ниже приводится пример использования функции GRWTEXTW (а также функции GRWTEXTH) для риcования объемлющего прямоугольника, заключающего текст.

(grwCreate 'w 200 200 "Текст" _WHITE)

==> w

(grwShow 'w)

==> w

(grwFont 'w "Tahoma" 14 T)

==> w

(grwTexth 'w "Проба")

==> 23

(grwTextw 'w "Проба")

==> 62

(grwPrint 'w 20 20 "Проба" _RED)

==> w

(grwRect 'w 20 20 82 43 _NAVY)

==> w

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

GRWTRANSPARENT

Функция GRWTRANSPARENT задает прозрачность графического окна. Функция принимает два обязательных параметра: идентификатор окна и фактор прозрачности. Последний параметр должен быть целым числом в диапазоне от 0 до 255. Нулю соответствует полная прозрачность, а величине 255 - полная непрозрачность (стандартный режим отображения окна). При успехе функция возвращает идентификатор окна. При неуспехе возникает ошибка.

Ниже приводится пример использования функции GRWTRANSPARENT.

(grwCreate 'w1 200 200 "Обычное окно" _WHITE)

==> w1

(grwCreate 'w2 200 200 "Прозрачное окно" _WHITE)

==> w2

(grwTransparent 'w2 125)

==> w2

(grwShowAll -1)

==> 2

Рисунок все объясняет. Прозрачность окна, естественно, можно менять многократно.


Менеджер графических окон

Графика HomeLisp основана на понятии графического окна. Графическое окно представляет собой форму, имеющую в своем составе область рисования (Picture Box), главное меню с двумя пунктами (позволяющими сохранять построенное изображение в файлах или печатать на доступных принтерах).

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

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

Менеджер графических окон доступен при работе в среде разработки (IDE) и при работе исполняемых файлов (если они были сгенерированы с диспетчерской формой). При использовани библиотеки HomeLispLib.Exe менеджер окон недоступен.

При работе в IDE менеджер окон актиаизируется либо через главное меню (пункт Смотреть -> Окна) или при нажатии на кнопку со следующим изображением:

При работе исполняемого файла, сгенерированного с диспетчерской формой менеждер окон вызывается с диспетческой формы (аналогичной кнопкой или через пункт Окна главного меню).

Если в сеансе работы нет графических окон, то кнопка и пункт меню будут неактивны. Если же в сеансе работы имеются созданные окна (неважно, видимые или нет), то при нажатии на указанную кнопку или выборе соответствующего пункта меню, пользователь увидит форму менеджера. Если запустить HomeLispIde.Exe и создать графическое окно, выполнив следующий код:

(grwCreate 'w 200 200 "Пробное окно" _WHITE)

==> w

то будет создано окно с областью рисования, размером 200*200 пикселов. Однако это окно после создания не отображается сразу. Для отображения окна можно использовать менеджер окон. При активизации менеджера окон, пользователь увидит список всех окон, имеющихся в сеансе:

В списке активных окон в каждой строке содержится информация об одном окне: в квадратных скобках стоит идентификатор окна, далее располагается заголовок.

Форма имеет главное меню из четырех пунктов: Отобразить, Скрыть, Уничтожить и Выход. Пункты меню Скрыть и Уничтожить устроены одинаково и имеют по два подпункта: Выбранное окно и Все окна. Пункт Отобразить также имеет два подпункта (Выбранное окно и Все окна), но подпункт Все окна имеет еще два подпункта: Как есть и Каскадом. Подпункт Как есть отображает все окна в соответствии с их координатами. Подпункт Каскадом перемещает окна к левому верхнему углу формы (экрана) и выстраивает их с постоянным сдвигом левого верхнего угла по обеим координатам.

Подпункт Выбранное окно будет активен только в случае, если пользователь выбрал одно из окон в списке, как это показано на следующем рисунке:

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

Рассмотрим более развернутый пример. Если в среде разработки выполнить приведенный ниже код:

(defun wintest nil

   (prog (hh i k)

   (setq winlist '(w1 w2 w3 w4 w5 w6))

   (setq collist '(_WHITE _BLUE _GREEN _OLIVE _RED _YELLOW))

   (setq ww 100)

   (setq hh 100)

   (for i 1 6 (

        (grwCreate (car winlist) ww hh (fix2str ww) (car collist))

        (setq winlist (cdr winlist))

        (setq collist (cdr collist))

        (setq ww (+ ww 20))

        (setq hh (+ hh 20))

              )

   )

   )

)

==> wintest

(wintest)

==> NIL

то будет создано шесть окон с размерами графической области от 100 до 200 пикселов. Однако все они будут невидимы. Если открыть менеджер окон и выполнить режим Отобразить -> Все окна -> Каскадом, то пользователь увидит следующее:

Если снова открыть менеджер и выполнить скрытие окна w4:

То соответствующее окно будет скрыто, а остальные окна останутся отображенными:

Аналогично работают и режимы отображения и удаления окон.


Описание функций работы с hash-массивами

Hash-массив - это структура, позволяющая хранить произвольные данные с доступом по ключу. Обычный массив можно рассматривать как частный случай hash-массива, ключами которого являются последовательные целые числа. Современные версии Лиспа (в частности, Common Lisp) имеют в своем составе средства работы с hash-массивами. Поэтому разработчик принял решение реализовать hash-массивы и в составе HomeLisp.

Работа с hash-массивами в HomeLisp отличается от схемы работы с hash-массивами в Common Lisp двумя моментами.

В HomeLisp манипулирование данными, хранящимися в hash-массивах, полностью инкапсулировано в соответствующих функциях (в отличие от Common Lisp, где для помещения даных в hash-массив используется функция SETF).

В HomeLisp функции занесения и извлечения элементов по ключу возвращают списки из двух элементов, в которых второй элемент есть T (признак успеха) или Nil (признак неудачи). В Common Lisp эти функции возвращают два значения (возврат множества значений в HomeLisp не поддерживается).

Реализация hash-массивов, описанная ниже, есть просто лисповская обертка для свойств и методов известного объекта DICTIONARY, входящего в состав библиотеки SCRRUN.DLL.

Все описываемые ниже функции реализованы только в 13-й редакции ядра HomeLisp и принадлежат к типу SUBR.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
HASHCLEAR | 1 | HASH | Очищает hash-массив
HASHCOUNT | 1 | HASH | Возвращает число элементов hash-массива
HASHCREATE | 1 | ATOM | Создает hash-массив
HASHDESTROY | 1 | HASH | Уничтожает hash-массив
HASHERASE | 2 | 1 - HASH; 2 - ANY | Удаляет элемент hash-массива c заданным ключом
HASHGET | 2 | 1 - HASH; 2 - ANY | Возвращает элемент hash-массива с заданным ключом
HASHKEYEXIST | 2 | 1 - HASH; 2 - ANY | Проверяет существование в hash-массиве элемента с заданным ключом
HASHMAP | 2 | 1 - HASH; 2 - FUNCT | Применяет функцию, заданную вторым аргументом к каждому элементу hash-массива
HASHPUT | 3 | 1 - HASH; 2,3 - ANY | Заносит элемент с заданным ключом в hash-массив
HASHCLEAR

Функция HASHCLEAR принимает один параметр - идентификатор hash-массива (атом). Hash-массив очищается, но не уничтожается. Вот развернутый пример:

(hashCreate 'h)

==> h

(hashPut 'h 'k-1 'v-1)

==> (v-1 T)

(proplist 'h)

==> (HASH COUNTER 1 HASHHANDLE 1)

(hashClear 'h)

==> h

(proplist 'h)

==> (HASH COUNTER 0 HASHHANDLE 1)

Создается hash-массив. В него заносится атом v-1 с ключом k-1. Список свойств атома h показывает, что hash-массив содержит один элемент. Далее hash-массив очищается. Повторный запрос списка свойств показывает, что хотя счетчик элементов (индикатор COUNTER) обнулился, но сам hash-массив не уничтожился.

HASHCOUNT

Функция HASHCOUNT принимает один параметр - идентификатор hash-массива и возвращает количество элементов, содержащихся в hash-массиве. В принципе, количество элементов hash-массива можно получить из списка свойств идентификатора массива, использование HASHCOUNT несколько проще. Рассмотрим пример:

(hashcount 'h)

Символ h - не HASH-таблица
==> ERRSTATE

(hashCreate 'h)

==> h

(hashcount 'h)

==> 0

(hashput 'h 'k 'v)

==> (v T)

(hashcount 'h)

==> 1

(hashput 'h 'kk 'vv)

==> (vv T)

(hashcount 'h)

==> 2

(proplist 'h)

==> (HASH COUNTER 2 HASHHANDLE 1)

Сначала делается попытка применить функция HASHCOUNT к "чистому" атому (не являющемуся идентификаторм hash-массива). Эта попытка, естественно, завершается ошибкой.

Далее создается hash-массив. Запрос количества элементов показывает, что массив пуст. Далее в массив заносится значение v с ключом k. Теперь вызов HASHCOUNT показывает, что в массиве содержится один элемент. Добавление еще одного элемента vv с ключом kk и последующий вызов HASHCOUNT показывает, что элементов стало два. В этом можно убедиться, запросив список свойств идентификаторов hash-массива.

HASHCREATE

Функция HASHCREATE создает hash-массив. Она ожидает один параметр-атом. Желательно, чтобы этот атом был "чистым" (имел пустой список свойств). Если же атом-параметр содержит в списке свойств какие-либо стандартные индикаторы (APVAL,FIXED, STRING и т.д.), то возбуждается состояние ошибки. В случае, когда атом-параметр содержит список свойств без стандартных индикаторов, то этот список уничтожается (заменяется на другой); при этом выдается соответствующее сообщение. И, наконец, если атом-параметр уже является идентификаторм hash-массива, то возбуждается состояние ошибки.

Если вызов HASHCREATE завершается успешно, то атом-параметр становится идентификатором hash-массива и у атома-параметра создается защищенный список свойств.В этом списке свойств содержится стандартный флаг HASH и два индикатора - COUNTER и HASHHANDLE. Под индикатором COUNTER хранится текущее количество элементов hash-массива, а под индикатором HASHHANDLE - внутренний номер hash-массива.

Попытка прямой модификации списка свойств (посредством PUTPROP или SPROPL) вызовет ошибку защиты.

Все это выглядит следующим образом:

(hashCreate T)

hashCreate: недопустимый флаг у первого аргумента
==> ERRSTATE

(hashCreate "a")

hashCreate: недопустимый флаг у первого аргумента
==> ERRSTATE

(putprop 'h 'ff 'zz)

==> h

(proplist 'h)

==> (ff zz)

(hashCreate 'h)

==> h
hashCreate: список свойств атома h будет очищен

(proplist 'h)

==> (HASH COUNTER 0 HASHHANDLE 1)

(hashCreate 'h)

HASH h уже существует
==> ERRSTATE

(putprop 'h 'counter 8)

SPROPL: Недопустимая модификация списка свойств
==> ERRSTATE

(spropl 'h '(1 2 3))

SPROPL: Недопустимая модификация списка свойств
==> ERRSTATE

Попытка превратить в hash-массив встроенную константу T или строку завершается ошибкой. Далее у атома h создается список свойств (ff zz), а затем делается попытка превратить этот атом в идентификатор hash-массива. Попытка оказывается успешной, но выдается предупреждающее сообщение об очистке списка свойств атома h. А вот повторный вызов HASHCREATE завершается ошибкой.

Попытка любой "ручной" модификации списка свойств идентификатора hash-массива завершается ошибкой.

HASHDESTROY

Функция HASHDESTROY ожидает на входе атом-идентификатор hash-массива. Функция уничтожает заданный hash-массив, при этом список свойств атома-идентификатора очищается. При успешном завершении функция возвращает T.

(hashCreate 'h)

==> h

(HashPut 'h '(1 2 3) (sumlist '(1 2 3)))

==> (6 T)

(HashPut 'h '(4 5 6) (sumlist '(4 5 6)))

==> (15 T)

(hashCount 'h)

==> 2

(hashDestroy 'h)

==> T

(hashCount 'h)

Символ h - не HASH-таблица
==> ERRSTATE

(proplist 'h)

==> NIL

Здесь создается hash-массив h и в него заносятся два значения. Запрос количества элементов показывает наличие двух элементов. Далее массив уничтожается. Попытка получить количество элементов теперь вызывает ошибку. И это неудивительно - после вызова HASDESTROY атом h уже не представляет hash-массив (и его список свойств пуст).

HASHERASE

Функция HASHERASE уничтожает элемент hash-массива. На вход функции требуется подать два параметра: идентификатор has-массива и ключ удаляемого элемента. Если элемент с заданным ключом существует, то он удаляется из hash-массива, а функция HASHERASE возвращает T. Если же элемента с заданным ключом в hash-массиве нет, то функция вернет Nil.

Вот пример работы HASHERASE:

(hashcreate 'h)

==> h

(hashput 'h 'k-1 '(1 2 3 4))

==> ((1 2 3 4) T)

(hashput 'h 'k-2 '(5 6 7 8))

==> ((5 6 7 8) T)

(hasherase 'h 'k-1)

==> T

(hashCount 'h)

==> 1

(hasherase 'h 'k-1)

==> NIL

Создается hash-массив и в него заносятся два элемента с ключами k-1 и k-2. После этого удаляется элемент с ключом k-1. Количество элементов в hash-массиве становится равным 1. Попытка повторного удаления элемента с тем же ключом завершается неудачно (функция возвращает Nil).

HASHGET

Функция HASHGET позволяет получить значение элемента hash-массива по ключу. Функция ожидает два параметра: идентификатор hash-массива и ключ. При успехе функция возвращает список из двух элементов, первым из которых будет значение элемента , а вторым - атом T. Если ключ не найден в hash-массиве, функция вернет список (Nil Nil).

Ниже приведен пример использования функции HASHGET:

(hashCreate 'h)

==> h

(hashPut 'h 'k_1 111)

==> (111 T)

(hashPut 'h 'k_2 222)

==> (111 T)

(hashPut 'h 'k_3 333)

==> (333 T)

(hashGet 'h 'k_2)

==> (222 T)

(hashGet 'h 'k_7)

==> (NIL NIL)

Создается hash-массив h. В него записываются значения 111, 222 и 333 с ключами k_1,k_2 и k_3 соответственно. Затем из hash-массива извлекается значение, ассоциированное с ключом k_2. Результат - список (222 T). Поскольку его второй элемент есть T, то первый элемент результата есть искомое значение. А вот попытка извлечь значение, ассоциированное с ключом h_7 оканчивается неудачно - второй элемент результата есть Nil (элемент с ключом h_7 не существует).

HASHKEYEXIST

Функция HASHKEYEXIST проверяет наличие в hash-массиве значения, ассоциированного с заданным ключом. Функция принимает два параметра - идентификатор hash-массива и ключ (произвольная форма). Если в hash-массиве, заданным первым параметром имеется элемент с ключом, заданным вторым параметром, то функция HASHKEYEXIST вернет T, если же элемента нет - функция вернет Nil.

Вот иллюстрация сказанного выше:

(hashCreate 'h)

==> h

(hashPut 'h 'k-1 '(1 2 3 4))

==> ((1 2 3 4) T)

(hashPut 'h 'k-2 '(1 2 3 4))

==> ((1 2 3 4) T)

(hashKeyExist 'h 'k-1)

==> T

(hashKeyExist 'h 'k-2)

==> T

(hashKeyExist 'h 'k-3)

==> NIL

(hashKeyExist 'g 'k-3)

Символ g - не HASH-таблица
==> ERRSTATE

Здесь создается hash-массив и в него заносятся два значения. Вызовы hashKeyExist показывают, какие ключи существуют, а какие - нет. Попытка вызвать hashKeyExist с первым параметром, не являющимся hash-массивом, вызывает ошибку.

HASHMAP

Функция HASHMAP принимает два параметра - идентификатор hash-массива (атом) и функциональное выражение, задающее функцию с двумя параметрами. HASMAP применяет эту функцию последовательно ко всем элементам hash-массива, заданного первым параметром. Первым параметром вызова подставляется ключ очередного элемента, а вторым - его значение. После "пробега" всего hash-массива функция HASHMAP возвращает число элементов исходного hash-массива.

Вот как это выглядит:

(hashCreate 'h)

==> h

(hashPut 'h 'a1 '(1 2 3))

==> ((1 2 3) T)

(hashPut 'h 'a2 '(4 5 6))

==> ((4 5 6) T)

(hashPut 'h 'a3 '(7 8 9))

==> ((7 8 9) T)

(hashPut 'h 'a4 '(10 11 12))

==> ((10 11 12) T)

(hashPut 'h 'a5 '(13 14 15))

==> ((13 14 15) T)

(hashPut 'h 'a6 '(16 17 18))

==> ((16 17 18) T)

(hashMap 'h '(lambda (k v) (printline (list k v))))
(a1 (1 2 3))
(a2 (4 5 6))
(a3 (7 8 9))
(a4 (10 11 12))
(a5 (13 14 15))
(a6 (16 17 18))

==> 6

(hashMap 'h '(lambda (k v) (printline (list k (sumlist v)))))
(a1 6)
(a2 15)
(a3 24)
(a4 33)
(a5 42)
(a6 51)

==> 6

(hashClear 'h)

==> h

(hashMap 'h '(lambda (k v) (printline (list k v))))

==> 0

Здесь создается hash-массив и в него заносятся трехэлементые числовые списки с ключами a1 - a6. Первый вызов HASHMAP просто распечатывает пары "ключ - значение". Второй вызов распечатывает пары "ключ - сумма элементов значения". Далее hash-массив очищается. Попытка мапировать функцию на пустой hash-массив завершается без ошибки, просто число обработанных элементов оказывается равным нулю.

HASHPUT

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

Вот как выглядит работа HASHPUT:

(hashCreate 'h)

==> h

(hashPut 'h 'a1 'v1)

==> (v1 T)

(hashPut 'h 'a2 'v2)

==> (v2 T)

(hashPut 'h 'a1 'v3)

==> (v3 NIL)

Видно, что попытка повторно занести элемент с ключом v1 завершается неудачно.

Работа с hash-массивами имеет одну тонкость, проявляющуюся в том случае, когда ключ элемента не есть атом. Рассмотрим следущий пример:

(hashCreate 'h)

==> h

(hashPut 'h '(1 2 3) '(x y z))

==> ((x y z) T)

(hashPut 'h '(4 5 6) '(a b c))

==> ((a b c) T)

(hashCount 'h)

==> 2

(hashMap 'h '(lambda (k v) (printline (list k v))))

((1 2 3) (x y z))
((4 5 6) (a b c))

==> 2

(hashGet 'h '(1 2 3))

==> (NIL NIL)

(hashKeyExist 'h '(1 2 3))

==> NIL
Сначала ничего не предвещает неприятностей - создается hash-массив и в него заносятся два значения с ключами, представляющими собой трехэлементные списки. Вызов HASHCOUNT показывает, что в hash-массиве находятся два элемента. Вызов HASHMAP позволяет убедиться, что элементы "живы". А попытка получить элемент по ключу (1 2 3) завершается неудачно. Вызов HASHKEYEXIST тоже показывает, что элемента с ключом (1 2 3) не существует. В чем же дело?

Дело в механизме сравнения ключей при поиске в hash-массиве. Можно считать, что при сравнении ключей используется функция EQL. Когда происходит запрос существования с ключом (1 2 3), список (1 2 3) из вызова HASHGET/HASHKEYEXIST не есть тот же самый список, что и список из вызова HASHPUT.

Если использовать в качестве ключей атомы, то проблема не возникает. И понятно, почему: ведь атомы уникальны. Любая ссылка на атом a ссылвется на один и тот же атом.

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

(hashCreate 'h)

==> h

(setq k-1 '(1 2 3))

==> (1 2 3)
Создана глобальная переменная k-1

(setq k-2 '(4 5 6))

==> (4 5 6)
Создана глобальная переменная k-2

(hashPut 'h k-1 '(x y z))

==> ((x y z) T)

(hashPut 'h k-2 '(a b c))

==> ((a b c) T)

(hashMap 'h '(lambda (k v) (printline (list k v))))

((1 2 3) (x y z))
((4 5 6) (a b c))

==> 2

(hashGet 'h k-1)

==> ((x y z) T)

(hashKeyExist 'h k-1)

==> T

Можно убедиться, что теперь поведение всех функций стало предсказуемым. Важно подчеркнуть, что в вышеприведенном примере ключи есть именно списки, а не атомы k-1 и k-2. Поскольку все функции манипулирования hash-массивами принадлежат классу SUBR, то при их вычислении значения символов-аргументов k-1 и k-2 будут заменены их значениями (списками (1 2 3) и (4 5 6)). Причем, при вызовах всех функций с аргументом k-1 этот символ будет заменен одним и тем же списком.

В Common Lisp дело обстоит сходным образом, но там есть возможность управлять механизмом сравнения ключей, который можно задать при создании hash-массива. В HomeLisp эта возможность пока не реализована...


Описание функций управления средой и диалогом

Описание функций управления средой HomeLisp и диалогом с пользователем

В этом разделе описываются встроенные функции HomeLisp, предназначенные для управления средой разработки и диалогом с пользователем. Все эти функции, за исключением пары TRACE/UNTRACE, отсутствуют в реализации Лаврова и Силагадзе.

Некоторые из функций, рассматриваемых в этом разделе, недоступны при работе WEB-компоненты или при работе с библиотекой HomeLispLib.exe (этот факт отмечается в детальном описании таких функций). Те функции, которые реализованы только в ядре 11 или 13 помечены значками (11) и (13) соответственно.

Запись и сохранение состояния

Одной из новых возможностей 13-го ядра является возможность сохранения/восстановления полного состояния Лисп-машины в двоичном файле.

При сохранении состоянии в файл попадает полное состояние Лисп-машины за исключением открытых файлов (двоичные объекты, диалоги, графические окна с их содержимым и все введенные пользовательские функции). Очень существенным отличием этого способа сохранения состояния является то, что можно сохранить циклические списочные структуры. Традиционный способ сохранения состояния (с помощью функции WRS) такую возможность не обеспечивает.

Сохранение/восстановление состояния обеспечивается функцией WRITEIMAGE и псевдофункцией READIMAGE. Рестарт системы может быть выполнен с помощью псевдофунции RESTART.

Имя функции | Тип функции | К-во аргументов | Тип аргументов | Выполняемое действие
ASK | SUBR | 1 | STRING | Выводит диалоговое окно (InputBox), c текстом приглашения, равным значению атома-аргумента. Возврашает введенный пользователем текст в виде атома типа STRING.
ASSERT (13) | FSUBR | 1 | ANY | Вычисляет условие и, при истинности, останавливает выполнение. Работает только в режиме отладки.
ASSOLIST (11) | SUBR | 1 (необязательный) | STRING | Строит снимок ассоциативного списка.
CLS | SUBR | 0 | - | Программно очищает область вывода
DEBUG (13) | FSUBR | 1 | ANY | Вызывает интерактивный отладчик
DUMP* | FSUBR | 1 (необязательный) | STRING | Включает режим дампирования, направляя его в файл с полным именем, заданным аргументом
ECHO | SUBR | 0 | - | Включает/выключает режим эха
EXIT | SUBR | 1 (необязательный) | STRING | Завершает выполнение с итоговым сообщением
INPUT | SUBR | 1 | STRING | Преобразует значение аргумента в S-выражение. Это S-выражение возвращется в качестве результата.
OBJLIST | SUBR | 1 (необязательный) | STRING | Строит снимок списка объектов.
OUTPUT | SUBR | 1 | ANY | Выводит значение своего аргумента в виде строки.
PHIDE | SUBR | 1 | ANY | Скрывает прогресс-бар.
PSHOW | SUBR | 1 | ANY | Отображает прогресс-бар.
RDS | SUBR | 1 (необязательный) | STRING | Читает все S-выражения из файла с именем, заданным аргументом; Если аргумент опущен - вызывается диалог OPEN
READ | SUBR | - | - | Читает S-выражение, вводимое пользователем во время выполнения программного кода
READIMAGE (13) | Псевдофункция | 1 (необязательный) | STRING | Восстанавливает состояние Лисп-машины из файла-образа, созданного функцией WRITEIMAGE
RESTART (13) | Псевдофункция | - | - | Сброс состояния Лисп-машины
RDS | SUBR | 1 (необязательный) | STRING | Читает все S-выражения из файла с именем, заданным аргументом; Если аргумент опущен - вызывается диалог OPEN
SAY | SUBR | 1 | STRING | Выводит диалоговое окно (MessageBox) с текстом сообщения, равным значению аргумента.
STAT | FSUBR | 0 | - | Включает режим статистики
TRACE | FSUBR | 1 | Атом | Включает режим трассировки для функции, имя которой задано атомом-аргументом
UNDUMP* | FSUBR | 0 | - | Выключает режим дампирования
UNSTAT | FSUBR | 0 | - | Выключает режим статистики
UNTRACE | FSUBR | 1 | Атом | Выключает режим трассировки для функции, имя которой задано атомом-аргументом
VARLIST (13) | SUBR | 1 (необязательный) | STRING | Выводит таблицу активных переменых
WRITEIMAGE (13) | SUBR | 1 (необязательный) | STRING | Записывает состояние Лисп-машины в двоичный файл с именем, заданным аргументом; Если аргумент опущен - вызывается диалог "SAVE AS"
WRS | SUBR | 1 (необязательный) | STRING | Записывает все S-выражения (кроме циклических) в текстовый файл с именем, заданным аргументом; Если аргумент опущен - вызывается диалог "SAVE AS"
ASK

Функция ASK предназначена для организации примитивного диалога с пользователем (во время выполнения программы на Лиспе). Функция ожидает единственный аргумент - атом. Выводится диалоговое окно (INPUT BOX), в область приглашения которого попадет значение атома-аргумента. Пользователь вводит ответ в область ввода и нажимает кнопку "Принять". Ответ пользователя превращается в атом с индикатором STRING и этот атом возвращается в качестве результата функции.

Так, если ввести: (ask "Введите что-нибудь"), то будет выведено диалоговое окно:

Если далее пользователь введет в область ввода текст или S-выражение, то функция вернет ввод пользователя в виде атома с индикатором STRING. Если пользватель нажмет кнопку "Cancel" ("Отказ"), то результатом будет пустая строка.

Разработчик обращает внимание на то, что если требуется ввести S-выражение, то результат вызова ASK следует передать на вход функции INPUT, описываемой ниже. Функция недоступна при работе WEB-компоненты.

ASSERT

Функция ASSERT (реализованная только в 13-й редакции ядра HomeLisp),предназначена для работы в диалоговом отладчике среды разработки. Эта функция недоступна в WEB-компоненте. При работе с библиотекой HomeLispLib.exe или в составе EXE-файла функция игнорируется (сразу же возвращает Nil).

Функция принимает S-выражение. Если при вызове функции активен диалоговый отладчик, то заданное S-выражение вычисляется в текущем контексте. Если результат оказывается отличным от Nil, выполнение приостанавливается, и пользователь получает возможность увидеть значение переменных, задать/сменить точки останова и т.д.

Если же при вызове функции отладчик неактивен, то функция ASSERT сразу же возвращает Nil. Рассмотрим пример:

(defun f (x &aux i n) (for i 20 40 ((setq n (fact i))
                                    (assert (>= n x)))
                      )
)

==> f

(f 100000000000000000000000000)

==> 40

Здесь в теле функции последовательно вычисляется значение факториалы целых от 20 до 40. Если вызвать эту функцию из IDE, то цикл выполнится до конца (как это видно из приведенной врезки). Если же выполнить эту функцию в автоматическом режиме под отладчиком, то пользователь увидит:

Нетрудно убедиться, что выражение, заданное в ASSERT станет истиным, когда i > 25, что и подтверждается информацией, отображенной в окне "Лексический контекст".

ASSOLIST

Функция ASSOLIST строит снимок ассоциативного списка (на момент вызова функции). При вызове может быть указан один необязательный аргумент (типа STRING). В снимок включаются все переменные из ассоциативного списка, в именах которых содержится (в любой позиции) значение аргумента. Если аргумент опущен - строится снимок всего ассоциативного списка.

При работе в Web-компоненте снимок представляет собой отдельный html-файл, сылка на который сохраняется в течение сеанса пользователя. Так, если выполнить в Web-компоненте следующий код:

(setq a 111)

(defun f (x) (prog nil (assolist) (* x x)))

то снимок ассоциативного списка будет иметь вид:

Здесь хорошо видно, что переменная a (созданная на верхнем уровне вызовом SETQ) является глобальной переменной, тогда как переменная x является параметром функции и при выходе из функции будет уничтожена.

В составе библиотеки HomeLispLib.Exe снимок ассоциативного списка в виде таблички направляется в область вывода (аналогично выходным данным функции PRINT). Так, в составе скриптового движка Conlisp, снимок ассоциативного списка отображается так:

Хотя при работе в IDE пользователю доступна кнопка просмотра ассоциативного списка, можно пользоваться и функцией ASSOLIST (это оправдано, поскольку можно вызвать ASSOLIST из функции и увидеть значения параметров. Вот как это выглядит:

(setq a 222)

==> 222

(defun f (x y) (prog nil (assolist) (return (* x y))))

==> f

(f 5 6)

+-----------------+---------+-----------+
| Имя переменной  | Тип     | Значение  |
+-----------------+---------+-----------+
|a                |Глобал.  |222        |
|x                |Параметр |5          |
|y                |Параметр |6          |
+-----------------+---------+-----------+

==> 30
CLS

Эта функция не имеет аргументов. Ее выполнение заключается в очистке области вывода. Этого же эффекта можно добиться, нажав соответствующую кнопку или выполнив пункт контекстного меню "Очистить область вывода". Функция недоступна при работе WEB-компоненты, а также библиотеки HomeLispLib.exe.

DEBUG

Функция DEBUG (реализованная только в 13-й редакции ядра HomeLisp), доступна только при работе в IDE. Функция активизирует интерактивный отладчик (описанию отладчика посвящен отдельный документ).

Функция DEBUG принадлежит к типу FSUBR и принимает один необязательный аргумент - отлаживаемое S-выражение. Если аргумент не задан, то выражение можно задать уже в отладчике (в окне "Стартовое S-выражение").

DUMP*

Функция DUMP* (отсутствующая у Лаврова и Силагадзе) позволяет пользователю проследить весь процесс вычисления S-выражения на весьма детальном уровне. Использование странного, на первый взгляд, окончания имени функции (*) имеет на самом деле вескую причину. В Лиспе принято, что функция, имя которой оканчивается буквой p, является предикатом (функцией, возвращающей логическое значение). Поскольку описываемая функция предикатом не является, использование слова DUMP для ее именования нарушило бы традицию Лиспа.

Функция DUMP* может иметь единственный параметр строкового типа. Он интерпретируется как имя выходного файла (может присутствовать буква диска и слэши). Если параметр не содержит буквы диска и слэшей, то он интерпретируется как имя файла в текущей директории. Если присутствует расширение, то оно заменяется на ".dmp". Если функция DUMP* вызвана без параметров, то отображается окно диалога и пользователь может выбрать выходной файл. После того, как выходной файл выбран, его имя отображается в шестой (самой правой) панели статус-бара главного окна. Теперь при вводе любого S-выражения результаты всех промежуточных вычислений будут выведены в выбранный файл; файл каждый раз переоткрывается в режиме "APPEND".

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

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

Рассмотрим пример. Пусть значение переменной x равно 1, значение переменной y равно 2, и значение переменной z равно 3.Ниже приводится ход вычисления выражения (+ x y z).

.EVAL вход:  (PLUS x y z)
..CAR вход: A= (PLUS x y z)
..CAR выход: Рез= PLUS
..CDR вход: A= (PLUS x y z)
..CDR выход: Рез= (x y z)
..APPLY вход: A1= PLUS A2= (x y z)
...EVFUN вход: A1= PLUS A2= (x y z)
...
... Ассоциативный список:
...
...  3  ->  (z . 3)
...  2  ->  (y . 2)
...  1  ->  (x . 1)
...
....LIST вход: A= (x y z)
.....CAR вход: A= (x y z)
.....CAR выход: Рез= x
.....EVAL вход:  x
......EVATOM вход:  x
......EVATOM выход:  1
.....EVAL выход:  1
.....CDR вход: A= (x y z)
.....CDR выход: Рез= (y z)
.....CAR вход: A= (y z)
.....CAR выход: Рез= y
.....EVAL вход:  y
......EVATOM вход:  y
......EVATOM выход:  2
.....EVAL выход:  2
.....CDR вход: A= (y z)
.....CDR выход: Рез= (z)
.....CAR вход: A= (z)
.....CAR выход: Рез= z
.....EVAL вход:  z
......EVATOM вход:  z
......EVATOM выход:  3
.....EVAL выход:  3
.....CDR вход: A= (z)
.....CDR выход: Рез= NIL
....LIST выход: Рез= (1 2 3)
....PLUS вход: A= (1 2 3)
.....CAR вход: A= (1 2 3)
.....CAR выход: Рез= 1
....PLUS пром. рез=1
.....CDR вход: A= (1 2 3)
.....CDR выход: Рез= (2 3)
.....CAR вход: A= (2 3)
.....CAR выход: Рез= 2
....PLUS пром. рез=3
.....CDR вход: A= (2 3)
.....CDR выход: Рез= (3)
.....CAR вход: A= (3)
.....CAR выход: Рез= 3
....PLUS пром. рез=6
.....CDR вход: A= (3)
.....CDR выход: Рез= NIL
....PLUS выход: Рез= 6
...EVFUN выход:  6
..APPLY выход:  6
.EVAL выход:  6

В дампе отчетливо видны основные стадии вычисления. Сначала выделяется голова списка; этот атом интерпретируется как имя функции (PLUS). Из списка аргументов строится новый список, при этом, каждый атом заменяется своим значением (они берутся из ассоциативного списка). Вновь построенный список дается на вход функции PLUS. Функция PLUS вычисляет сумму элементов списка значений аргументов ("чинит карандаш", пока он не исчерпается). Результат вычисления по цепочке вызовов возвращается пользователю (попадает в область вывода).

ECHO

Функция ECHO управляет режимом дублироания введенного S-выражение в область вывода. Включается режим вызовом функции (ECHO T), а выключается вызовом (ECHO NIL). Когда режим эха включен, введенное пользователем S-выражение дублируется в области вывода; результат вычисления выводится ниже после комбинации "==>". Если режим эха выключен, то в области вывода печатется только результат вычисления. Состояние режима эха (Вкл/Выкл) выводится в шестой (самой правой) панели статус-бара. Режимом эха можно управлять и с помощью меню или тулбара - имеется пункт меню и командная кнопка. Однократное нажатие на кнопку меняет статус эха на противоположный. Функция недоступна при работе WEB-компоненты, а также при работе в составе EXE-файла и при вызове из библиотеки HomeLispLib.Exe.

EXIT

Функция EXIT завершает сеанс работы. Программа HomeLispIde.Exe завершается. Функция может иметь аргумент (обязательно типа STRING). Если аргумент задан, текст строки выдается в диалоговом окне в качестве "прощального сообщения" - (EXIT "Прощай!..") Для завершения сеанса пользователь может также нажать кнопку "Выход" или щелкнуть по соответствующему пункту главного меню. Функция недоступна при работе WEB-компоненты.

INPUT

Функция INPUT ожидает единственный аргумент-атом строкового типа. Функция рассматривает значение атома (строку символов) как S-выражение и переводит его во внутреннюю Лисп-форму. Полученное S-выражение функция INPUT возвращает в качестве результата. Вот как это выглядит:

(input "(+ 3 4)")

==>  (PLUS 3 4)

(eval (input "(+ 3 4)"))

==>  7

(input "(a . b)")

==>  (a . b)

(input "nil")

==>  NIL

(input "(a b c)")

==>  (a b c)

(eval (input "(a b c)"))

Атом "a" не имеет значения (не связан).
==>  ERRSTATE

(input '(a b c))

INPUT: параметр - не атом
==>  ERRSTATE

(input 'a)

INPUT: параметр должен иметь тип STRING
==>  ERRSTATE

(input ")(")

Нарушен баланс скобок или неверна скобочная структура
==>  NIL

Пусть читатель обратит внимание, что описываемая функция не вычисляет выражение, а только переводит его во внутреннюю списковую форму. Если к результату INPUT применить EVAL, то результат будет вычислен (точнее, будет выполнена попытка его вычисления - как это показано на врезке).

OBJLIST

Функция OBJLIST принимает единственный необязательный аргумент (типа STRING) и строит снимок списка объектов (атомов), в именах которых в любой позиции встречается значение аргумента. Если аргумент опущен, то строится снимок всех объектов. Следует отметить, что если очередной объект есть строка, то проверка осуществляется с учетом регистра. Для прочих атомов проверка выполняется без учета регистра.

При работе в Web-компоненте снимок списка объектов формируется в отдельном html-файле (который сохраняется до конца сеанса пользователя). Вид списка объекта приводится ниже:

При работе со скриптовым движком Conlisp.exe список имеет вид:

Здесь создано графическое окно ww, а затем запрошен список объектов, в именах которых содержится комбинация ww. Видно, что таких атомов оказалось всего два: окно и строка "ww" (из вызова (objlist "ww"). Пусть читатель обратит внимание на то, что у строки установлен только стандартный флаг STRING, тогда как у атома ww наряду со стандартным флагом WINDOW, имеется список нестандартных свойств (характеристики окна).

Совершенно аналогично выглядит результат вызова функции OBJLIST в среде IDE:

(grwCreate 'ww 200 200 "Графическое окно" _WHITE)

==> ww

(objlist "ww")
+-----------------------------------------+-----------------+-----------------+
| Имя атома                               | Станд. флаги    | Доп. флаги      |
+-----------------------------------------+-----------------+-----------------+
|"ww"                                     |STRING;          |                 |
|ww                                       |WINDOW;          |(width 200 height|
|                                         |                 | 200 title "Графи|
|                                         |                 |ческое окно" back|
|                                         |                 |color &HFFFFFF WI|
|                                         |                 |NHANDLE 1)       |
+-----------------------------------------+-----------------+-----------------+

==> T

OUTPUT

Функция OUTPUT ожидает единственный аргумент произвольного типа (атом или список) и выводит его значение в виде строки. Функция OUTPUT позволяет сохранить внешнюю (печатную) форму S-выражения в виде строки. Функция OUTPUT предназначена в основном для использования в диалоговых программах. Следует отметить, что в отличие от функций PRINT/PRINTS, Функция OUTPUT возвращает строку и дополнительно ничего не выводит в область вывода (при использовании в IDE). Ниже приводятся примеры вызовов OUTPUT:

(output (cdr '(1 2 3)))

==> "(2 3)"

(input (output (cdr '(1 2 3))))

==> (2 3)

Видно, что функция OUTPUT в некотором смысле является обратной по отношению к INPUT: результат OUTPUT может быть из строковой формы преобразован в форму S-выражения посредством INPUT.

PHIDE

Функция PHIDE не имеет аргументов. Вызов этой функции скрывает прогресс-бар, отображенный ранее функцией PSHOW.

PSHOW

Функция PSHOW имеет два аргумента. Первый аргумент должен быть числом в интервале [0 - 100]. Второй аргумент (необязательный) должен быть строкой разумной длины. Вызов функции PSHOW вызывает отображение на экране прогресс-бара. При этом первый параметр задает процент выполнения процесса, ход которого показывает прогресс-бар. Второй параметр задает текст, который отображается над прогресс-баром. Если второй параметр опущен, то в области текста отображается значение первого параметра. Прогресс-бар, отображенный вызовом (pshow 43 "Немного подождите...") выглядит так:

Скрыть прогресс-бар можно вызовом функции PHIDE.

RDS

Функция RDS предназначена для чтения S-выражений из файла. Вызвать функцию RDS можно двумя способами: без аргументов и с единственным аргументом (значение которого должно быть строкового типа).

В первом случае будет вызвано стандартное окно диалога выбора файла (с расширением .lsp). Если пользователь выбирает файл и нажимает кнопку "Принять", то из файла считываются и загружаются все S-выражения. S-выражения в файле могут быть записаны в свободном формате. При этом содержимое строки после пары символов "//" или ";;" считается комментарием и игнорируется.

Во втором случае проверяется, существует ли файл с именем, заданным значения аргумента. Имя может содержать букву диска. Если имя не содержит обратных слешей и буквы диска, файл будет искаться в текущей директории. При этом, если расширение файла задано отличным от .lsp, оно принудительно Заменяется на .lsp.

В случае успешной загрузки всех S-выражений из файла, в область вывода выводится сообщение об успешной загрузке файла, в противном случае выводится сообщение об ошибке. Функция RDS неявно вызывается при запуске HomeLisp с параметром имя автозагружаемой библиотеки. При работе WEB-компоненты функция позволяет загрузить данные только из рабочей директории авторизованного пользователя. Аргумент в этом случае является обязательным и не может содержать путь (диск и директорию).

READ

Функция READ введена в HomeLisp для совместимости с существующими версиями Лиспа. Функция не требует параметров и обеспечивает чтение S-выражения, вводимого пользователем во время выполнения программы. Результатом функции является S-выражение, введенное пользователем. Функция READ имеет два отличия от описанной выше функции ASK:

Функция READ возвращает не строку, а S-выражение (т.е. не требует последующего вызова функции INPUT);
Функция READ не выводит диалогового окна, а ожидает ввода пользователя в области ввода (зависящей от режима выполнения).

При выполнении программы в IDE вызов функции READ выполнение программы прерывается, цвет области ввода становится белым, а вся область ввода окружается красной рамкой. Пользователь должен ввести S-выражение в область ввода и нажать ENTER. Функция не вернет управление до тех пор, пока пользователь не введет корректное S-выражение (со сбалансированными скобками). После возврата из функции READ цвет области ввода восстановится, а красная рамка исчезнет.

При выполнении программы в среде Conlisp.exe вызов READ устанавливает курсор в начало строки, следующей за текущей. Пользователь должен ввести S-выражение и нажать ENTER.

Если вызов функции READ встретился в коде, выполняющемся в среде Wlisp.exe, то функция возвращает Nil. Функция READ недоступна в WEB-компоненте и в составе EXE-файлов.

READIMAGE

Псевдофункция READIMAGE (реализованная только в 13-й редакции ядра HomeLisp) обеспечивает восстановление состояния Лисп-машины из образа, созданного вызовом функции WRITEIMAGE. Слово "псевдо" в названии фигурирует не случайно: READIMAGE не является частью ядра HomeLisp. Если при работе IDE ввести в область ввода команду (READIMAGE), то откроется окно диалога Открыть и пользователь может выбрать файл-образ (*.hli). Если же ввести (READIMAGE "имя.hli"), то будет выполнена попытка восстановления состояния из заданного файла.

В обоих случаях состояние Лисп машины после восстановления не имеет ничего общего с состоянием машины до выполнения (READIMAGE), поэтому совершенно бессмысленно использовать вызов (READIMAGE) в программном коде (такая попытка вызовет ошибку Отсутствует функция READIMAGE.

Вместо того, чтобы вводить команду (READIMAGE), можно просто нажать кнопку "Загрузить" и в выпадающем списке типов файлов выбрать "Файлы образов *.hli".

Пользователю будет предложено подтвердить восстановление состояния. При подтверждении состояние восстанавливается. При этом все объекты Лиспа, созданные до восстановления, замещаются номыми.

RESTART

Псевдофункция RESTART (реализованная только в 13-й редакции ядра HomeLisp) переводит Лисп-машину в состояние, которое она имела при старте сразу после загрузки функций стандартной библиотеки. Команду (RESTART) можно ввести только при работе с IDE. Честно говоря, разработчик сейчас сомневается, стоило ли открывать доступ к служебной функции RESTART, ведь достаточно просто достичь эффекта рестарта можно просто перезапустив IDE...

SAY

Функция SAY предназначена выдачи сообщения пользователю во время выполнения программы на Лиспе. Функция ожидает единственный параметр-атом. Этот атом может иметь тип FIXED, FLOAT, STRING или BITS. Выводится стандартный message-box в область текста которого попадает значение аргумента. Так, если введено выражение (SAY &H112233), то пользователь увидит следующее:

Функция SAY всегда возвращает T. Функция недоступна при работе в составе WEB-компоненты.

STAT

Функция STAT не требует аргументов. Эта функция включает режим статистики. При включенном режиме статистики после завершения вычисления входного выражения выдается количество обращений ко всем функциям системы. Включение режима статистики позволяет оценивать и анализировать сложность вычисления вводимых выражений. Статус режима статистики выводится в шестой (самой правой) панели статус-бара главного окна программы (Стат=Вкл/Выкл; по умолчанию режим выключен). Для включения режима статистики можно также использовать соответствующую кнопку панели инструментов или пункт главного меню. При успешном завершении функции STAT, она возвращает результат T.

Вот как выглядит результат применения режима статистики. Рассчитаем значение числа 20! двумя способами - с использованием рекурсивной процедуры fact_rec и с использованием итерационной процедуры fact_iter:

(defun fact_rec (x) (cond ((eq x 1) 1) (t (* x (fact_rec (- x 1))))))

==> fact_rec

(STAT)

==> T

(fact_rec 20)

==>  2432902008176640000

*** Статистика обращений ***
DIFFERENCE ...............  19
TIMES ....................  19
EQ .......................  20
FACT_REC .................  20
COND .....................  20

(defun fact_iter (x) (prog (r)
                       (setq r 1)
                   L   (cond ((eq x 1) (return r)))
                       (setq r (* r x))
                       (setq x (- x 1))
                       (go l)
                     )
)

==> fact_iter

*** Статистика обращений ***
DEFUN ....................  1

(fact_iter 20)

==> 2432902008176640000

(fact_iter 20)

==> 2432902008176640000

*** Статистика обращений ***
FACT_ITER ................  1
PROG .....................  1
RETURN ...................  1
DIFFERENCE ...............  19
GO .......................  19
TIMES ....................  19
COND .....................  20
EQ .......................  20
SETQ .....................  39

Следует обратить внимание на то, что, будучи включенным, режим статистики выводит сведения о каждом вычисленном S-выражении на верхнем уровне (т.е. о каждом S-выражении, введенном пользователем). Так, в приведенном выше примере, задание новой функции fact_iter осуществляется вызовом функции DEFUN. Этот вызов тоже попадает в статистику.

Для выключении режима статистики служит "парная" функция UNSTAT, описываемая ниже.

TRACE

Функция TRACE принимает один аргумент - имя функции типа EXPR, FEXPR или MACRO. Если функция, заданная аргументом TRACE, существует, то в качестве результата вычисления возвращается значение аргумента. В противном случае возвращается NIL. Вызов функции TRACE устанавливает у функции, заданной значением аргумента, флаг трассировки. Если теперь вызвать эту функцию, то перед печатью результата вычисления будет выведен протокол трассировки, включающий входную информацию каждого вызова трассируемой функции и результат вычислений. Рассмотрим, например, протокол трассировки рекурсивной функции вычисления факториала:

(defun fact_rec (x) (cond ((eq x 1) 1) (t (* x (fact_rec (- x 1))))))

==> fact_rec

(trace fact_rec)

==> fact_rec

(fact_rec 7)

Вход в функцию fact_rec Аргументы:  (7)
  Вход в функцию fact_rec Аргументы:  (6)
    Вход в функцию fact_rec Аргументы:  (5)
      Вход в функцию fact_rec Аргументы:  (4)
        Вход в функцию fact_rec Аргументы:  (3)
          Вход в функцию fact_rec Аргументы:  (2)
            Вход в функцию fact_rec Аргументы:  (1)
            Возврат из функции fact_rec Результат:  1
          Возврат из функции fact_rec Результат:  2
        Возврат из функции fact_rec Результат:  6
      Возврат из функции fact_rec Результат:  24
    Возврат из функции fact_rec Результат:  120
  Возврат из функции fact_rec Результат:  720
Возврат из функции fact_rec Результат:  5040

==> 5040

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

UNDUMP*

Функция UNDUMP* (звездочка в конце имени введена из-за нежелания нарушать традицию Лиспа - все, что оканчивается на p - предикат) выключает режим дампирования. Вместо явного вызова функции UNDUMP* можно нажать соответствующую кнопку панели инструментов или выбрать соответствующий пункт главного меню. Функция UNDUMP* всегда возвращает NIL

UNSTAT

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

UNTRACE

Функция UNTRACE выключает режим трассировки для фукнции, имя которой есть значение единственного аргумента UNTRACE. Функция возвращает имя функции, у которой выключается трассировка. Если функция, заданная аргументом UNTRACE, отсутствует, возвращается NIL.

VARLIST

Функция VARLIST (реализованная только в 13-м ядре) отображает список активных в момент вызова переменных вместе с их типами. Функция принимает один необязательный параметр строкового типа. Если параметр задан, то в список переменых попадают только переменные, в имени которых присутствует заданная строка. Рассмотрим пример:

(setq *g* 100)

==> 100
Создана глобальная переменная *g*

(defvar *d* -100)

==> *d*

(defun f (x y) (varlist) (list x y))

==> f

(f 222 333)

+-------------------+---------+-----------+
| Имя переменной    | Тип     | Значение  |
+-------------------+---------+-----------+
|y                  |Лексич.  |333        |
|x                  |Лексич.  |222        |
|*d*                |Динамич. |-100       |
|*g*                |Глобальн.|100        |
+-------------------+---------+-----------+

==> (222 333)

(varlist)

+-------------------+---------+-----------+
| Имя переменной    | Тип     | Значение  |
+-------------------+---------+-----------+
|*d*                |Динамич. |-100       |
|*g*                |Глобальн.|100        |
+-------------------+---------+-----------+

==> T

Здесь создана глобальная переменная *g*, динамическая переменная *d*, после чего вызвана функция f, в теле которой содержится вызов VARLIST. Хорошо видно, что в точке вызова VARLIST активны четыре переменные - глобальная, динамическая и две лексические (параметры функции). Вызов функции VARLIST на верхнем уровне показывает, естественно, только две переменные - *g* и *d*.

Разумеется, увидеть переменные *g* и *d* можно при работе в IDE, нажав кнопку "Просмотр переменных", но лексические переменные можно увидеть только в отладчике.

WRITEIMAGE

Функция WRITEIMAGE (реализованная только в 13-м ядре) сохраняет полный образ состояния Лисп-машины в двоичный файл на диске. В образ попадают все созданные объекты (включая графические окна с содержимым, диалоги, BLOB-ы, hash-таблицы). Функция принимает один необязательный параметр - имя файла-образа. Если параметр не задан, открывается стандартный диалог "Сохранить как". Если пользователь нажмет кнопку отказа, сохранение не выполняется. В качестве расширения файла-образа устанавливается .hli (HomeLisp Image).

Файл-образ, полученный в результате работы функции WRITEIMAGE, пригоден для последующей загрузки псевдо-функцией READIMAGE.

Важнейшее отличие функции WRITEIMAGE от функции WRS (описанной ниже) состоит в том, что функция WRS не способна сохранять циклические структуры. Это не представляется удивительным, поскольку функция WRS в сущности просто "печатает" все S-выражения в текстовый файл (а циклическая структура не может быть распечатана).

WRS

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

Если аргумент Функции WRS опущен, то выводится стандартное диалоговое операционной системы "Cохранить как". В этом случае пользователь может отказаться от выгрузки, нажав кнопку "Отмена" или выполнить выгрузку, выбрав директорию и файл, и нажав кнопку "Сохранить".

Полученный в результате работы функции WRS текстовый файл пригоден для загрузки данных посредством функции RDS.

При работе WEB-компоненты функция позволяет сохранить данные только в рабочую директорию авторизованного пользователя. Аргумент в этом случае является обязательным и не может содержать путь (диск и директорию).


HomeLisp - индекс

HomeLisp 13.4.1

Эта работа посвящается памяти Алексея Жаркова, моего друга и большого энтузиаста Лиспа, безвременно ушедшего из жизни в 1994 году.

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

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

Установка и удаление
Здесь описывается процедура инсталляции и деинсталляции программы. Кроме того, приводится список стандартных компонентов Microsoft, которые используются программой; описываются директории, которые создаются в процессе инсталляции. Приводится список файлов, входящих в инсталляцию.

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

Конфигурация
В этом разделе подробно описана структура конфигурационных файлов HomeLisp, а также режим конфигурирования среды разработки и библиотеки HomeLispLib.Exe. Конфигурирование Web-компоненты рассматривается в разделе, целиком посвященном Web-компоненте.

Ввод S-выражений
Самое главное, что должен знать пользователь любой Лисп-системы, это правила ввода S-выражений. В этом разделе разъясняется, как вводить S-выражения в системе HomeLisp; показывается, как контролировать баланс скобок, как пользоваться стеком введенных команд.

Шаблоны стандартных конструкций
Первым препятствием для изучающих Лисп обычно является запоминание синтаксиса основных конструкций Лиспа (COND, DEFUN, PROG и др). В разделе 5 описываются правила использования стандартных шаблонов. Приводится содержательный пример их использования и объясняется, как создавать свои шаблоны.

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

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

Менеджер графических окон
Здесь рассказывается о том, как управлять отображением, скрытием и уничтожением графических окон HomeLisp. Оконный менеджер доступен при работе в среде разработки (IDE), а также при выполнении exe-файлов, сгенерированных с диспетчерской формой.

Общие сведения о входном языке HomeLisp
Приступая к изучению любого языка программирования, начинать следует с основ - с алфавита, правил записи имен и выражений, встроенных типов данных. Все эти сведения и собраны в этом разделе. Хотя Лисп предполагает действия над числами двух типов (целых и вещественных), его арифметика весьма своеобразна. В этом разделе приводятся начальные сведения об арифметике Лиспа, рассказывается, как ведут себя арифметические функции Лиспа при задании аргументов различных типов. Описываются и другие типы данных - битовые шкалы, строки и большие двоичные объекты (BLOBы).

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

Встроенные функции стандартного Лиспа
Здесь приведено подробное, с примерами, описание встроенных функций стандартного Лиспа: функции действия со списками, элементарные арифметические, функции работы со списком свойств и уборки мусора. Описание каждой функции, кроме совсем элементарных, сопровождается примерами.

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

Отладчик (новый раздел)
Приличная (или претендующая быть таковой) система программирования обязана включать в себя отладчик. Описываемый в этом разделе отладчик HomeLisp имеет достаточно скромные возможности (точки останова, просмотр контекстов в процессе вычисления). Тем не менее, он может оказаться небесполезным в практической работе.

Графические функции
HomeLisp включает небольшой набор графических функций (30 штук), позволяющих рисовать простейшие объекты, выводить текст, сохранять графическое изображение в файлах и загружать изображение из файла. Поддерживается также и "черепашья" графика. Графическая подсистема позволяет управлять многими графическими окнами. Описание этих возможностей (с примерами и картинками) читатель найдет в этом разделе.

Действия с файлами
Полноценное программирование немыслимо без использования файлов. Для работы с файлами HomeLisp включает функции, позволяющие работать как с текстовыми, так и с двоичными файлами. В дополнение к стандартным методам доступа, описан метод, позволяющий читать входной текстовый файл, как массив строк (в произвольном порядке).

Действия с большими двоичными объектами
Кроме действия с объектами, традиционными для Лиспа, HomeLisp позволяет работать с блоками байтов произвольой длины. Такие объекты традиционно называются BLOB-ами (сокращение от английского binary large objects). HomeLisp позволяет создавать BLOB-ы, загружать в них информацию (как программно, так и из файлов), писать BLOB-ы на диск и т.д.

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

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

Действия с реестром
Реестр Windows хранит большинство системных надстроек операционной системы. Современное средство разработки должно представлять пользователю возможность работы с реестром. В HomeLisp-е имеется всего девять встроенных функций манипулирования реестром. Однако эти функции обеспечивают большинство необходимых возможностей.

Действия с COM-объектами
Полноценная работа в среде Windows немыслима без технологии COM. В этом разделе описывается небольшая библиотека встроенных функций HomeLisp-а, позволяющая создавать COM-объекты, вызывать их методы и использовать их свойства.

Организация диалогов
Функции, описываемые в этом разделе, служат для организации диалоговых программ использующих стандартный графический интерфейс Windows. Это означает, что HomeLisp позволяет писать нормальные Windows-приложения (а не консольные приложения, как большинство некоммерческих версий Лиспа).

Математические функции
Графика и вычисления требуют поддержки элементарных математических функций (экспонента, логарифм, тригонометрические функции и т.д.) В этом разделе приведен полный список поддерживаемых математических функций HomeLisp-а.

Функции работы с hash-массивами (новый раздел)
Hash-массивы (массивы, индексируемые произвольными объектами, а не только последовательными целыми) давно стали привычными средствами в современных языках программирования. Лисп здесь не составляет исключения. В этом разделе приводится описание небольшого набора функций для манипулирования hash-массивами.

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

Библиотека дополнительных функций
В этом разделе содержится описание дополнительных функций (написанных на HomeLisp-е). Эти функции выполняют нетривиальные действия со списками, организуют ассоциативные списки, обеспечивают множественный выбор и т.д. Большая часть этих функций взята из книги Лаврова и Силагадзе. Приводятся примеры вызовов функций с краткими комментариями.

Работа в конструкторе диалогов
Этот раздел описывает встроенный в среду разработки HomeLisp экранный дизайнер (конструктор диалогов). Визуальное проектирование диалогов служит неплохим дополнением к возможностям диалоговых функций HomeLisp. Читатель, знакомый с дизайнером VBA не встретит никаких трудностей при чтении этого раздела.

Работа с библиотекой HomeLispLib.Exe
Это раздел предназначен тем, кто хочет использовать возможности языка Лисп, программируя на других языках (VB, VBA, C++, Delphi - на любом языке, поддерживающем COM), а также тем, кто хотел бы писать скрипты (сценарии) на Лиспе. Приводятся сведения о том, как зарегистрировать библиотеку, как выполнить необходимые настройки для использования скриптов, а также подробно описываются свойства и методы класса clsLisp, "живущего" в библиотеке HomeLispLib.Exe.

Создание EXE-файлов
Здесь рассказывается, как можно создавать автономные исполняемые файлы для программ, написанных на HomeLisp. Такие исполняемые файлы содержат в своем составе исполнительную систему HomeLisp (т.е. все функции классов SUBR/FSUBR), а также библиотечные функции классов EXPR/FEXPR/MACRO, которые разработчик пожелает включить в состав исполняемого файла. При запуске такого файла, сначала инициализируется ядро HomeLisp, а затем начинает выполняться стартовое S-выражение (которое разработчик задает при создании EXE-файла). Из стартового выражения вызываются все необходимые функции.

HomeLisp и WEB
В этом разделе описывается Web-компонента HomeLisp, превращающая HomeLisp в серверное приложение Web-сервера IIS. Описываются особенности конфигурирования, запуска, администрирования и пользовательский интерфейс. Web-компонента, по замыслу автора, предназначена для использования в составе учебного класса (где на рабочих местах обучаемых могут стоять маломощные, дешевые компьютеры).

Приложения

Очень краткое введение в язык Лисп (Раздел находится в стадии доработки)
В этом разделе дается сжатое описание основных концепций языка Лисп. Раздел задуман исключительно в целях полноты изложения (хотя, как надеется автор, он все же будет нелишним).

Приемы программирования и избранные задачи (Раздел находится в стадии доработки)
Раздел содержит различные примеры программ на HomeLisp. Некоторые из них автор позаимствовал из литературы, а некоторые (более простые) - придумал сам. Подробный разбор задач призван показать все аспекты языка HomeLisp. Автор надеется, что раздел будет полезен читателям (особенно впервые изучающим Лисп).

Особенности 13-й редакции ядра HomeLisp
Для удобства пользователя здесь собраны ссылки на все особенности 13-й редкции ядра (новые функции и концептуальные особенности).
Алфавитный указатель функций HomeLisp
Здесь содержится сквозной алфавитный указатель всех функций HomeLisp (как встроенных, так и написанных на HomeLisp).
Список литературы (Раздел находится в стадии доработки)
Приводятся ссылки на печатные издания, а также на различные сетевые ресурсы, посвященные Лиспу.


Ввод S-выражений

Для ввода S-выражения следует установить курсор в область ввода, ввести (и, при необходимости, отредактировать) выражение, установить курсор после последней скобки S-выражения и нажать Enter. Текст введенного выражения будет передан интерпретатору только в случае, если скобки сбалансированы. Если же скобки несбалансированы или стоят в неправильном порядке, то выдается звуковой сигнал, а в шестой панели статус-бара появляется предупреждение:

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

Корректное (в смысле скобочной структуры) выражение передается интерпретатору. Если включен режим эха, введенное выражение дублируется в области вывода, а вслед за этим, после комбинации "==>" выводится результат вычисления:

Команды, введенные пользователем, запоминаются в специальном буфере. Размером этого буфера можно управлять - количество запоминаемых команд задается в конфигурационном файле homelisp.ini. Для вызова введенной ранее команды в область ввода, следует нажать сочетание клавиш Ctrl+ВасkSpace. Повторное нажатие этого сочетания клавиш вызовет команду, введенную до предыдущей, и т.д. После вызова самой ранней команды из буфера будет вызвана самая поздняя (предыдущая) команда. Если пользователь вводит команду (S-выражение), которое уже есть в буфере, то введенное выражение не дублируется. В противном случае последняя введенная команда запоминается, а самая старая удаляется из буфера.

В процессе вычисления S-выражений могут произойти ошибки. Сообщения об ошибках выводятся независимо от состояния режима эха. Разработчик обращает внимание на следующее важное обстоятельство. Не следует вводить несколько независимых S-выражений за раз. Точнее, в этом случае обработается только первое S-выражение; остальные будут проигнорированы. Игнорируются символы, стоящие правее правой скобки, закрывающей первое S-выражение. Об этом пользователь будет предупрежден. Так, если введено:

То пользователь увидит в области ответа:

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


Введение

Что такое HomeLisp?

HomeLisp - простой интерпретатор стандартного Лиспа, сделанный по лекалам, описанным в великолепной книге С.C. Лаврова и Г.С. Силагадзе [1].

Пакет HomeLisp может использоваться на правах FREEWARE.

Разработчик будет только рад, если кто-либо применит HomeLisp для обучения или других целей (включая коммерческие, но не нарушающие Конституцию Российской Федерации)! Разработчику можно написать по адресу: catstail@narod.ru; он с благодарностью воспримет любую конструктивную критику в адрес представляемой программы. Однако, следует предупредить, что программа распространяется "как есть" , и разработчик не несет ответственности за последствия использования HomeLisp.

Авторские права на программный продукт и документацию принадлежат разработчику Файфелю Борису Леонидовичу. Внесение любых изменений в программный код (за исключением asp-скриптов) не допускается. Тиражирование и цитирование документации разрешается только с указанием автора.

Происхождение названия

HomeLisp дословно означает "домашний Лисп". Прилагательное "домашний" в русском языке имеет по крайней мере два смысловых значения: во-первых, "домашний" означает "сделанный в домашних, а не в промышленных условиях"; во-вторых, "домашний" означет "упрощенный". Оба этих значения вполне применимы к HomeLisp. Разработчик надеется, что слово "домашний" будет вызывать у пользователей только позитивные ассоциации (вспомните домашние пельмени, грибки, огурчики...)

Что может HomeLisp?

В процессе поисков материалов по Лиспу в Internet-е, автор обратил внимание, что в России Лиспу учат по системам CommonLisp, XLisp, MuLisp и т.д. Отечественные разработки (хотя они и есть!) для обучения практически не применяются. Следует отметить, что большинство расхожих (некоммерческих) версий Лиспа оформлены как консольные приложения с соответствующим интерфейсом. Такое минималистское решение вызывает у изучающих Лисп ложное ощущение, что Лисп может решать только сугубо абстрактные задачи и совершенно непригоден для задач, встречающихся в повседневной жизни.

Именно для таких скептиков автор сообщает, что на HomeLisp можно без большого труда делать почти всё, что требуется от системы программирования в среде Windows - имеются средства графики, работы с файлами (включая двоичный доступ), обработки строк, создание COM-объектов.

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

HomeLisp позволяет строить автономные исполняемые файлы (т.н. псевдо-exe-файлы). Такой файл содержит код ядра лиспа, все необходимые функции и может запускаться на любой машине без установки на ней HomeLisp, достаточно только наличие виртуальной машины VB6 (MSVBVM60.DLL) и некоторых стандартных Микрософтовских компонент.

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

Интегрированная среда разработки (IDE) HomeLispIde.Exe . В этой среде поддерживаются абсолютно все декларированные выше возможности HomeLisp.

ActiveX-Exe библиотека HomeLispLib.Exe, которая позволяет использовать язык Лисп в любой среде программирования, поддерживающей COM. В частности, можно писать скрипты на языке Лисп!

Web-компонента HomeLispWeb.Exe, предназначенная для работы в среде WEB-сервера IIS. Web-компонента поддерживает все основные языковые возможности Лиспа, но не поддерживает системные функции, работу с COM-объектами, графику, файлы, BLOBы и другие не вполне уместные в среде WEB-сервера возможности. Основное назначение Web-компоненты - создание многопользовательской системы для обучения языку.

Что не принадлежит к числу сильных сторон HomeLisp?

Автор честно предупреждает, что HomeLisp не отличается высокой производительностью. На домашнем компьютере автора (1.7Ггц) число 100! (произведение всех чисел от 1 до 100 со всеми значащими цифрами) считается за 0.2 сек. На рабочем компьютере (2Duo 2.4Ггц) время вычислений составляет 0.06 сек. Это скромно. Возможно, в следующих версиях производительность удастся повысить. А пока автор может сказать в свое оправдание лишь то, что для учебных целей (а также для работы со скриптами) производительности HomeLisp вполне достаточно. Кроме того, не очень высокая производительность должна стимулировать мысль программиста в направлении совершенствования алгоритмов... (Шутка, но в каждой шутке есть доля шутки).

Что двигало разработчиком?

Во-первых - самоутверждение.

Во-вторых - интерес к языку (в одной статье, посвященной Лиспу, написано примерно следующее: "...даже если вы и не станете применять Лисп, изучив этот язык, вы будете смотреть на программирование другими глазами..." Автор настоящего руководства полностью разделяет эту точку зрения).

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

Пусть линуксоиды не думают, что бесплатное ПО бывает только в Linux!

Извинения перед читателями и пользователями

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

При разработке 13-й редакции ядра разработчик поддался на уговоры одного из пользователей пакета и переделал панель инструментов и главное меню IDE, не подумав о том, что это "незначительное изменение" потребует изменения многих рисунков в документации. К намеченной для публикации дате (09.02.2012) "перетряхнуть" документацию не удалось. Поэтому разработчик еще раз извиняется и обещает:

В обозримом будущем привести документацию в полное соответствие с описываемой программой;

Без очень веских оснований не "вестись" на пожелания изменить интерфейс программы (и, в частности, внешний вид основных окон).


Общие сведения о входном языке HomeLisp
Правила записи программ и отличия от стандарта Лисп

Алфавит HomeLisp включает латинские и русские буквы (заглавные и строчные), цифры и все спецзнаки, которые есть на стандартной клавиатуре. Имена атомов могут включать любые знаки (но числа должны начинаться с цифры и иметь "правильную" структуру). Так 1A - просто атом, 123 - число типа FIXED, 1e1 - число типа FLOAT (в научной нотации).

Заглавные и строчные буквы в именах атомов не различаются: CAR, car и Car - одно и то же. (А занятно звучит эта фраза!) Автор обращает особое внимание на то, что русские буквы могут входить в имена атомов (правда, это, на взгляд автора, порождает больше проблем, чем удобства: приходится часто переключать регистры; кроме того нечаянное употребление букв, совпадающих по начертанию, порождает крайне неприятные ошибки). В строках, заключенных в двойные кавычки, строчные и заглавные буквы различаются: "Car" и "CAR" - разные атомы.

Программы записываются в текстовых файлах в свободном формате. Комментарием является все содержимое строки после пар символов "//" или ";;".

Несколько слов о реализации языка. Разработчик придерживался стандарта Лиспа из книги Лаврова и Силагадзе (практически совпадающего с оригиналом великого Дж. МакКарти). Сделаны лишь некоторые дополнения (не затрагивающие основных концепций языка):

- всесто (QUOTE Нечто) можно писать 'Нечто ;
- вместо PLUS, DIFFERENCE, TIMES, QUOTIENT, REMAINDER можно писать "+ - * \ и %" соответственно;
- вместо LESSP, GREATERP , GREQP, EQ можно писать "< > >= и =" соответственно;
- для задания функции наряду с функцями SEXPR/SFEXPR можно использовать сокращенный вариант DEFUN/DEFUNF;
- добавлены функции типа MACRO, задаваемые с помощью функции SMACRO и сокращенный вариант этой функции DEFMACRO;

Также добавлено несколько встроенных функций, "парных" к описанным Лавровым и Силагадзе: (NEQ - "не равно" - парная к EQ; LEEQP - "меньше или равно" - парная к GREQP) и ряд других.

Несколько расширен список математических функций - добавлены тригонометрические функции, натуральный логарифм, экспонента, а также генератор псевдослучайных чисел;

Добавлен необходимый минимум строковых функций - извлечение и вставка подстроки, замена, получение ASCII-кода символа и символа по его ASCII-коду;

Добавлены функции простой графики, работы с файлами, работы с большими двоичными объектами (BLOBами), некоторые системные функции - словом, все то, чего не могло быть в далеком 1978-м , и что вполне естественно смотрится сейчас.

Типы данных и особенности арифметики

В HomeLisp имеются данные следующих типов: FIXED, FLOAT, BITS и STRING.

Данные типа FIXED - это целые числа практически неограниченной разрядности. Описываемые ниже функции PLUS, DIFFERENCE, TIMES, QUOTIENT, REMAINDER позволяют вычислить, например, величину "100 факториал" или "2 в сотой степени".

Данные типа FLOAT соответствуют числам с плавающей точкой двойной точности (DOUBLE). Для работы с данными типа FLOAT можно использовать функции PLUS, DIFFERENCE, TIMES, ADD1, SUB1. Для деления чисел типа FLOAT следует использовать функцию DIVIDE (отсутствующую в реализации Лаврова и Силагадзе). Попытка использовать для деления чисел с плавающей точкой функции QUOTIENT и REMAINDER приведет к ошибке.

Данные типа BITS представляют собой 32-х битовые шкалы. Над ними можно выполнять битовые поразрядные операции LOGAND, LOGOR, LOGXOR, а также сдвигать в обе стороны, используюя функцию с двумя аргументами LEFTSHIFT. Второй аргумент задает количество сдвигов (число типа FIXED). Если оно отрицательно - сдвиг выполняется вправо.

Арифметика HomeLisp частично полиморфна. У функций PLUS, DIFFERENCE, TIMES допустимы аргументы типов FIXED и/или FLOAT. При вычислении происходит "поглощение" типом FLOAT типа FIXED. Так, при вычислении выражения:

(+ 555 1.0)

будет получен результат 556.0, имеющий тип FLOAT. Тогда, как при вычислении выражения:

(+ 555 1)

будет получен результат 556, имеющий тип FIXED.

Функция EXPT (возведение в степень) возвращает результат типа FIXED только если оба ее аргумента имеют тип FIXED. В противном случае возвращается результат типа FLOAT. Дополнительно следует отметить, что попытка возведения отрицательного числа (любого типа) в вещественную степень (даже с нулевой дробной частью) вызовет ошибку. Тогда как возведение отрицательного числа в целую степень выполняется без проблем. Нуль в любой положительной степени равен нулю. Попытка возвести нуль в отрицательную степень вызовет ошибку. Возведение числа в нулевую степень дает единицу.

Функции инкремента и декремента ADD1, SUB1 возвращают результат того же типа, что и аргумент.

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


Библиотека дополнительных функций

В этом разделе описываются функции, приведенные в книге Лаврова и Силагадзе и написанные на Лиспе. Эти функции входят в "джентльменский набор" функций, которые должны быть реализованы в любой версии Лиспа. Исключение составляет только макро FOR, который реализовал автор.

Имя функции | Тип функции | К-во аргумен- тов | Тип аргумен- тов | Выполняемое действие
ADDIFNONE | EXPR | 2 | ANY | Проверяет, входит ли значение первого аргумента в список, заданный вторым аргументом. Если не входит, то возвращается список, заданный вторым аргументом, с добавлением в начало значения первого аргумента. В противном случае возвращается значение второго аргумента.
APPEND EXPR 2 1-LIST;
2-ANY 		Создает новый список, путем присоединения первого аргумента ко второму
ASSOC EXPR 2 1-ANY;
2-LIST 		Выбирает из ассоциативного списка, заданного вторым аргументом, первую пару, в которой первый элемент совпадает со значением первого аргумента.
ATOMLIST | EXPR | 1 | ANY | Возвращает Т, если значение аргумента есть список, составленный из атомов
ATTACH | EXPR | 2 | ANY | Функция выполняет те же действия, что и CONS, но значение функции закрепляется за ее вторым аргументом. У функции имеется побочный эффект.
CART | EXPR | 2 | LIST | Строит декартово произведение множеств , заданных первым и вторым аргументами.
COLLECT | EXPR | 1 | ANY | Перегруппировывает элементы списка-аргумента таким образом, чтобы одинаковые элементы стояли подряд.
COPY | EXPR | 1 | ANY | Создает копию своего аргумента
CYCLEP | EXPR | 1 | ANY | Проверяет, является ли значение аргумента функции циклической структурой.
DEFINE | FEXPR | 1 | LIST | Одновременное определение нескольких функций типа EXPR
DEFLIST FEXPR 2 1-LIST;
2-ATOM 		Одновременное задание нескольким атомам значений свойств с заданным индикатором.
DIFLIST | EXPR | 2 | LIST | Вычисляет разность множеств, заданных списками, соответствующими первому и второму аргументу.
DPAIR | EXPR | 2 | LIST | Функция работает аналогично PAIR, но значение первого аргумента заменяется значением функции (т.е. построенным ассоциативным списком)
DREMOVE | EXPR | 2 | ANY | Второй аргумент этой функции должен быть списком. Выполняется исключение из этого списка всех элементов, совпадающих с первым аргументом.
DREVERSE | EXPR | 1 | ANY | Функция выполняет те же действия, что и REVERSE, но разрушает свой аргумент.
EFFACE | EXPR | 2 | ANY | Второй аргумент этой функции должен быть списком. Выполняется исключение из этого списка первого элемента, совпадающего с первым аргументом.
EQUAL | EXPR | 2 | ANY | Проверяет совпадение значений своих аргументов. В отличие от встроенной функции EQ, применима не только к атомам, но и к произвольным списочным структурам. При совпадении значений возвращает T, в противном случае - Nil
EQUALSET | EXPR | 2 | LIST | Проверяет, равны ли множества, заданные списками, соответствующими первому и второму аргументам.
FIRST | EXPR | 2 | LIST | Среди элементов списка, заданого первым аргументом, выбирается тот, который раньше встречается в списке, заданным вторым аргументом.
FLAG EXPR 2 1-LIST;
2-АТОМ 		Заносит флаг, заданный значением второго аргумента, в список свойств каждого из атомов, входящих в список, заданный первым аргументом.
FLAGP | EXPR | 2 | АТОМ | Проверяет наличие в списке свойств атома, заданного первым аргументом, флага, заданного значением второго аргумента.
FLATTEN | EXPR | 1 | ANY | Устраняет скобочную структуру и точки, превращая S-выражение в одноуровневый список атомов
FOR MACRO 4 1-АТОМ;
2,3-FIXED;
4-LIST 		Простой оператор цикла. Параметр цикла задается первым параметром; начало цикла - вторым, конец - третьим. Шаг цикла всегда равен 1. Четвертый параметр задает список операторов, составляющих тело цикла.
FORALL EXPR 2 1-LIST;
2-F/LBD 		Фунция, заданная вторым (функциональным) аргументом, применяется ко всем элементам списка, заданного первым аргументом. Результат равен Т, если результат применения функционального аргумента к каждому элементу первого аргумента равен Т.
FORODD EXPR 2 1-LIST;
2-F/LBD 		Фунция, заданная вторым (функциональным) аргументом, применяется ко всем элементам списка, заданного первым аргументом. Результат равен Т, если результат применения функционального аргумента равен Т для нечетного количества элементов списка.
FORSOME EXPR 2 1-LIST;
2-F/LBD 		Фунция, заданная вторым (функциональным) аргументом, применяется ко всем элементам списка, заданного первым аргументом. Результат равен Т, если результат применения функционального аргумента хотя бы к одному элементу первого аргумента равен Т.
GETPROP | EXPR | 2 | АТОМ | Функция проверяет, есть ли у атома, заданного первым аргументом, свойство заданное значением второго аргумента. Если свойство есть - функция вернет в качестве значения это свойство. В противном случае функция вернет Nil.
INTERSECTION | EXPR | 2 | LIST | Строит пересечение множеств элементов списков, заданных первым и вторым аргументами.
LAST | EXPR | 1 | LIST | Выделяет последний элемент списка
LCYCLEP | EXPR | 1 | ANY | Проверяет значение своего аргумента на наличие цепочек по d-указателю. При наличии цепочки возвращает T, в противном случае - Nil.
LENGTH | EXPR | 1 | ANY | Возвращает длину списка (на верхнем уровне)
LEXORDER | EXPR | 3 | LIST | Сравнивает элементы списков, заданных первым и вторым аргументом, с использованием ORDER1. Если элементы списков, стоящих в одинаковых позициях первого и второго аргументов, различаются, то они сравниваются посредством ORDER1.
LEXORDER1 | EXPR | 3 | LIST | Функция работает аналогично LEXORDER, но третьим аргументом задается список списков, очередной элемент которого используется при необходимости сравнения элементов.
LISTP | EXPR | 1 | ANY | Если значение аргумента есть S-выражение, которое можно изобразить без употребления точек, функция возвращает T. В противном случае, функция возвращает Nil.
LUNION | EXPR | 1 | LIST | Строит объединение множества элементов из элементов списка, заданного аргументом.
MAKESET | EXPR | 1 | LIST | Строит список из неповторяющихся значений элементов списка, заданного аргументом. (то же, что и SETOF)
MAP EXPR 2 1-LIST;
2-F/LBD 		Аналогично MAPLIST, но всегда возвращается Nil. Имеет смысл в том случае, когда функция, заданная вторым аргументом имеет побочный эффект (например, печатает что-либо).
MAPCAR EXPR 2 1-LIST;
2-F/LBD 		Аналогично MAPLIST, но функция применяется не к хвостам списков, а к первым элементам.
MAPLIST EXPR 2 1-LIST;
2-F/LBD 		Функция, заданная вторым аргументом, последовательно применяется к значению первого аргумента, и ко всем спискам, полученным из значения первого аргумента путем отбрасывания первого элемента. Результатом функции является список полученных вызовов.
MEMB EXPR 2 1-ATOM;
2-ANY 		Проверяет, является ли атом, заданный первым аргументом, элементом списка, заданного вторым аргументом. Если является, функция возвращает T, иначе - Nil
MEMBER | EXPR | 2 | ANY | Проверяет, является ли первый аргумент элементом списка, заданного вторым аргументом. Если является, функция возвращает T, иначе - Nil
NCONC | EXPR | 2 | ANY | Функция выполняет те же действия, что и APPEND, но одновременно присваивает свой результат первому аргументу.
ORDER EXPR 3 1,2-АТОМ;
3-LIST 		Проверяет, встречаются ли атомы, заданные первым и вторым аргументами, в списке, заданном третьим аргументом в заданном порядке (в этом случае результат будет T, в противном случае - Nil)
ORDER1 EXPR 3 1,2-АТОМ;
3-LIST 		Работает аналогично ORDER, но при нарушении порядка возвращается атом ORDERUNDEF.
PAIR | EXPR | 2 | LIST | Строит ассоциативный список (список точечных пар) из элементов списков, заданных аргументами.
PAIRLIS | EXPR | 3 | LIST | Строит ассоциативный список из элементов списков, заданных первым и вторым аргументами и добавляет его к ассоциативному списку, заданному третьим аргументом.
POP | FEXPR | 1 | АТОМ | Результат вычисления функции полностью эквивалентен вычислению (SETQ A (CDR A)), где А - аргумент.
POPUP | FEXPR | 2 | АТОМ | Результат вычисления функции эквивалентен последовательному вычислению двух выражений: (SETQ A1 (CAR A2)) и (SETQ A2 (CDR A2)). В качестве результата функция возвращает Nil.
POSSESSING EXPR 2 1-F/LBD;
2-LIST 		Строит список тех элементов списка, заданного вторым аргументом, для которых истино значение, заданное первым аргументом.
PROP EXPR 3 1,2-ATOM;
3-F/LBD 		Если у атома, заданного первым аргументом, есть свойство, заданное значением второго аргумента, то функция вернет хвост списка свойств, следующий за найденным индикатором. Если свойства нет - будет произведен вызов функции, заданной третьим аргументом. Эта функция не должна принимать параметров.
PUSH FEXPR 2 1-ANY;
2-ATOM 		Результат вычисления функции полностью эквивалентен вычислению (SETQ A2 (CONS A1 A2)), где А1 - первый аргумент, А2 - второй.
PUTFLAG | EXPR | 2 | АТОМ | Помещает в список свойств атома, заданого первым аргументом, флаг, заданный значением второго аргумента.
PUTPROP EXPR 3 1-АТОМ;
2-АТОМ;
3-ANY 		Функция добавляет в список свойств атома, заданного первым аргументом, индикатор, заданный вторым аргументом и свойство, заданное третьим аргументом.
RANK | EXPR | 2 | LIST | Упорядочивает список, заданный первым аргументом в порядке, заданном вторым аргументом
REMFLAG EXPR 2 1-LIST;
2-АТОМ 		Удаляет флаг, заданный значением второго аргумента, из списка свойств каждого из атомов, входящих в список, заданный первым аргументом.
REMOVE | EXPR | 2 | ANY | Удаление ВСЕХ вхождений значения первого аргумента из списка, заданного значением второго.
REMOVEF | EXPR | 2 | ANY | Удаление ПЕРВОГО вхождения значения первого аргумента из списка, заданного значением второго.
REMPROP | EXPR | 2 | АТОМ | Удаляет из списка свойств атома, заданного первым аргументом, свойство, заданное вторым аргументом. Если такого свойства нет, список свойств атома не меняется. Функция возвращает значение первого аргумента.
REVERSE | EXPR | 1 | ANY | Переставляет элементы списка в обратном порядке (на верхнем уровне).
SASSOC EXPR 3 1-ANY;
2-LIST;
3-F/LBD 		Аналогично ASSOC, но в случае отсутствия ассоциации, выдается результат вызова функции, заданной третьим аргументом. Эта функция не должна принимать параметры.
SELECT EXPR 3 1-ANY;
2-LIST;
3-ANY 		Функция ищет значение первого аргумента в списке, заданным вторым аргументом на нечетных местах. Если значение найдено, то функция возвращает элемент списка, следующий за найденным. В противном случае возвращается значение третьего аргумента.
SETOF | EXPR | 1 | LIST | Строит список из неповторяющихся значений элементов списка, заданного аргументом.
SUBLIS EXPR 2 1-LIST;
2-ANY 		В выражении, заданном значением второго аргумента, заменяет все атомы на ассоциации из ассоциативного списка, заданного первым аргументом.
SUBSET | EXPR | 2 | LIST | Если каждый элемент списка, заданного первым аргументом, входит в список, заданный вторым аргументом, то функция возвращает T. В противном случае возвращается Nil.
SUBST | EXPR | 3 | LIST | Выполняет замену в выражении, заданном третьим аргументом все вхождения значения первого аргумента (на всех уровнях) на значение второго аргумента.
SUCHTHAT EXPR 2 1-F/LBD;
2-LIST 		Выбирает из списка, заданного вторым аргументом, первый элемент, для которого истино выражение, заданное первым аргументом.
SUCHTHAT1 EXPR 4 1-F/LBD;
2-LIST;
3,4-ANY 		Проверяет, содержится ли в списке, заданном вторым аргументом хотя бы один элемент, для которого истино выражение, заданное первым аргументом. Если такой элемент найден, то результатом будет значение четвертого аргумента. Если же такой элемент не найден, то возвращается значение третьего аргумента.
SUCHTHAT2 EXPR 3 1-F/LBD;
2-LIST;
3-F/LBD 		Проверяет содержится ли в списке, заданным вторым аргументом, элемент, для которого истино выражение, заданное первым аргументом. Если такой элемент найден, то к хвосту списка, начиная с найденного элемента применяется функция, заданная третьим аргументом. Если элемент не найден, возвращается Nil.
TCONC | EXPR | 2 | ANY | Функция помещает значение своего первого аргумента в конец очереди, заданной вторым аргументом
UNION | EXPR | 2 | LIST | Функция вычисляет объединение множеств, заданных списками-аргументами.
ADDIFNONE

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

Вот примеры работы с функцией ADDIFNONE :

(addifnone 'a '(a b c))

==> (a b c)

(addifnone 's '(a b c))

==> (s a b c)

(addifnone 'a nil)

==> (a)

В последнем случае происходит присоединение атома a в начало пустого списка.

APPEND

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

Вот примеры работы с функцией APPEND:

(append '(a b c) 'd)

==> (a b c . d)

(append 'd 'g)

Аргумент CAR - атом (d)
==> ERRSTATE

(append '(1 2 3) '(a b c))

==> (1 2 3 a b c)

Видно, что функция не работает, если первый аргумент - атом.

ASSOC

Функция ASSOC выбирает из ассоциативного списка, заданного вторым аргументом, первую пару, в которой первый элемент совпадает со значением первого аргумента. Как известно, ассоциативный список представляет собой список пар. Каждый элемент ассоциативного списка - есть точеная пара (в частности - список).

Вот примеры работы с функцией ASSOC :

(assoc 'c '((a . 1) (b . 2) (c . 3) (d . 4)))

==> (c . 3)

(assoc 'b '((a . 1) (b . 2) (c . 3) (d . 4)))

==> (b . 2)

(assoc 'q '((a . 1) (b . 2) (c . 3) (d . 4)))

==> NIL

(assoc 's '((a b c d) (s d f g) (1 2 3 4)))

==> (s d f g)

(assoc 1 '((a b c d) (s d f g) (1 2 3 4)))

==> (1 2 3 4)

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

ATOMLIST

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

Вот примеры работы с функцией ATOMLIST:

(atomlist '(1 2 3))

==> T

(atomlist '(1 2 (3)))

==> NIL

(atomlist '(a . b))

==> NIL
ATTACH

Функция ATTACH объединяет значения двух своих аргументов в точечную пару (аналогично CONS), но результат присваивается в качестве значения значению второго аргумента. Функции, которые не только возвращают значения, но и меняют состояние других объектов программы, называются функциями с побочным эффектом. Таким образом, ATTACH - функция с побочным эффектом.

Вот примеры работы с функцией ATTACH:

(setq c '(1 2 3))

==> (1 2 3)

(attach 'a c)

==> (a 1 2 3)

c

==> (a 1 2 3)

(setq z '(q w e))

==> (q w e)

(attach '(1 2) (cdr z))

==> ((1 2) w e)

z

==> (q (1 2) w e)

Видно, что первый вызова ATTACH приводит к тому, переменная c меняет свое значение. В последнем случае вызова ATTACH переменная z меняет свое значение таким образом, чтобы (cdr z) был бы равен результату ((1 2) w e).

CART

Функция CART строит лексикографически упорядоченный список, элементами которого являются всевозможные двухэлементные списки, первый элемент которых входит в первый аргумент, а второй элемент - во второй аргумент.

Вот примеры работы с функцией CART :

(cart '(a b c) '(1 2 3))

==> ((a 1) (a 2) (a 3) (b 1) (b 2) (b 3) (c 1) (c 2) (c 3))

(cart '(a b c) '(1 2 3 4))

==> ((a 1) (a 2) (a 3) (a 4) (b 1) (b 2) (b 3) (b 4) (c 1) (c 2) (c 3) (c 4))

Длина результата функции CART равна произведению длин сомножителей (на верхнем уровне).

COLLECT

Функция COLLECT перегруппировывает элементы списка-аргумента таким образом, чтобы одинаковые элементы стояли подряд.

Вот примеры работы с функцией COLLECT :

(collect '(a b c d e f a b c d e f))

==> (a a b b c c d d e e f f)

(collect '((a b) (c d) (a b)))

==> ((a b) (a b) (c d))

(collect '(a b c d e f a b c d e f (a b) (c d) (a b)))

==> (a a b b c c d d e e f f (a b) (a b) (c d))

Видно, что функция COLLECT перегруппировывает списки, состоящие не только из атомов, но и из списков.

COPY

Функция COPY создает в списочной памяти копию (второй экземпляр) своего аргумента. Эта функция может быть полезна в случае, когда исходный список требуется подвергнуть разрушающему преобразованию с помощью функций RPLACA/RPLACD.

Вот примеры работы с функцией COPY :

(setq z1 '(a b c))

==> (a b c)

(setq z2 z1)

==> (a b c)

z1

==> (a b c)

z2

==> (a b c)

(rplaca z1 'd)

==> (d b c)

z1

==> (d b c)

(rplaca z2 'd)

==> (d b c)

(setq z1 '(a b c))

==> (a b c)

(setq z2 (copy z1))

==> (a b c)

(rplaca z1 'd)

==> (d b c)

z1

==> (d b c)

z2

==> (a b c)

В этом примере происходит следующее: заводится переменная z1 со значением (a b c). Затем переменной z2 присваивается значение z1. Можно убедиться, что значением обеих переменных z1 и z2 будет один и тот же список (a b c). Если с помощью функции RPLACA заменить у значения переменной z1 голову списка с a на d, то изменят значения обе переменные z1 и z2.

Совсем по-другому будет обстоять дело в том случае, если переменной z2 присвоить значением копию значения переменной z1: теперь изменение головы списка-значения z1 не окажет никакого влияния на значение z2.

Следует отметить, что функция COPY неприменима к циклическим списочным структурам (выполнение вызовет зацикливание). Кроме того, если у значения аргумента есть цепочки указателей, сходящиеся к какой-либо одной структуре, то у результата таких цепочек не будет.

CYCLEP

Функция CYCLEP проверяет наличие в списке циклических структур. При наличии циклической структуры функция возвращает T, при отсутствии - Nil.

Вот примеры работы с функцией CYCLEP:

(setq z '(a b c))

==> (a b c)

(cyclep z)

==> NIL

(rplaca (cddr z) z)

==> ((a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     ...)))))))))))))))))))))))))))))))))))))))))
     ))))))))))))))))))))))))))))))))))))))))))))
     ))))))))))))))))))))))))))))))))))))))))))))
     )))))))))))))))))))))))))))))

(cyclep z)

==> T

Здесь сначала создан список (a b c). Он, естественно, не содержит циклов. Поэтому вызов CYCLEP возвращает Nil. Далее с помощью функции RPLACA список превращается в циклическую структуру. Печать таких структур должна выполняться бесконечное время. HomeLisp прекращает печать после вывода определенного количества атомов и ставит многоточие. Вызов CYCLEP c циклической структурой возвращает T - функция работает.

DEFINE

Функция DEFINE предназначена для массового определения функций класса EXPR. Она принимает один параметр, представляющий собой список вида ((F1 O1) (F2 O2)...), где Fi - имя i-й функции, а Oi - ее определяющее выражение. При успешном завершении функция создает заданные функции и возвращает список их имен (F1 F2 ...).

Вот примеры работы с функцией DEFINE :

(define ((квадрат (lambda (x) (* x x))) (куб (lambda (x) (* x x x)))))

==> (квадрат куб)

(квадрат 7)

==> 49

(куб 5)

==> 125

Естественно, что все задаваемые функции могут иметь разные количества аргументов. Важно лишь, что посредством DEFINE можно задать только функцию класса EXPR. Сама функция DEFINE по вполне понятным причинам принадлежит классу FEXPR.

DEFLIST

Функция DEFLIST предназначена для маcсового занесения информации в списки свойств атомов. Функция требует два аргумента: первый из них должен быть списком вида ((A1 P1) (A2 P2)...), а второй должен быть атомом. Успешное завершение этой функции приводит к тому, что у атома Ai в списке свойств появляется свойство Pi под индикатором, заданным вторым аргументом DEFLIST. Функция возвращает список (A1 A2 ...).

Вот примеры работы с функцией DEFLIST, наглядно объясняющий сказанное выше:

(deflist ((a1 красный) (a2 синий) (a3 зеленый)) цвет)

==> (a1 a2 a3)

(proplist 'a1)

==> (цвет красный)

(proplist 'a2)

==> (цвет синий)

(proplist 'a3)

==> (цвет зеленый)

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

DIFLIST

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

Вот примеры работы с функцией DIFLIST :

(diflist '(a b c) '(a c))

==> (b)

(diflist '(a b c) '(a b c))

==> NIL

(diflist '((a b) c) '(a b c))

==> ((a b))

(diflist '((a b) c) '((a b) c))

==> NIL

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

DPAIR

Функция DPAIR (как и функция PAIR) объединяет значения двух своих аргументов в ассоциативный список. При этом, значение первого аргумента заменяется результатом выполнения функции.

Вот примеры работы с функцией DPAIR:

(setq z1 '(a b c d))

==> (a b c d)

(setq z2 '(1 2 3 4))

==> (1 2 3 4)

(pair z1 z2)

==> ((a . 1) (b . 2) (c . 3) (d . 4))

z1

==> (a b c d)

(dpair z1 z2)

==> ((a . 1) (b . 2) (c . 3) (d . 4))

z1

==> ((a . 1) (b . 2) (c . 3) (d . 4))

Здесь переменным z1 и z2 присвоены четырехэлементные атомарные списки. Вызов функции (pair z1 z2) приводит к образованию ассоциативного списка (списка точечных пар). При этом значение переменной z1 остается прежним. А при вызове (dpair z1 z2) результат получается точно таким же, но значение переменной z1 меняется.

DREMOVE

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

Вот примеры работы с функцией DREMOVE:

(dremove 2 '(1 2 3))

==> (1 3)

(dremove 2 '(2 2 2))

==> NIL

(dremove '(2 3) '(1 2 3 (2 3)))

==> (1 2 3)

(dremove '(2 . 3) '(1 2 3 (2 3) (2 . 3) (2 . 3) (2 . 3)))

==> (1 2 3 (2 3))

В последних примерах продемонстрировано, что функция исключает не только атомы, но и любые S-выражения.

DREVERSE

Функция DREVERSE выполняет оборачивание списка на всех уровнях (аналогично функции REVERSE). Разница заключается в том, что DREVERSE разрушает значение своего аргумента.

Вот примеры работы с функцией DREVERSE:

(setq z1 '(1 2 3 4))

==> (1 2 3 4)

(reverse z1)

==> (4 3 2 1)

z1

==> (1 2 3 4)

(dreverse z1)

==> (4 3 2 1)

z1

==> (1)

Здесь переменной z1 присваивается значение (1 2 3 4). Применение REVERSE обращает список, но значение z1 остается прежним. А применение DREVERSE тоже обращает список, но значение z1 меняется.

EFFACE

Функция EFFACE принимает два аргумента. Второй аргумент должен быть списком, а первый может быть произвольным S-выражением. Функция исключает из значения второго аргумента первый элемент, совпадающий со значением первого аргумента. Значением функции является преобразованный список. Если удаленный элемент стоит не на первом месте в списке-значении второго аргумента, то значением второго аргумента становится преобразованный список.

Вот примеры работы с функцией EFFACE :

(setq z1 '(1 2 3 4))

==> (1 2 3 4)

(efface 1 z1)

==> (2 3 4)

z1

==> (1 2 3 4)

(efface 2 z1)

==> (1 3 4)

z1

==> (1 3 4)

Переменной z1 присвоено значение (1 2 3 4). Удаление из этого списка элемента 1 не меняет значение переменной z1. Удаление из этого же списка элемента 2 приводит к изменению значения переменной z1.

EQUAL

Функция EQUAL (в отличие от встроенной функции EQ) сравнивает два произвольных S-выражения на идентичность. Функция возвращает T, если выражения идентичны и Nil в противном случае.

Вот примеры работы с функцией EQUAL :

(equal '(1 2 3) '(1 2 3))

==> T

(equal '(1 2 3) '(1 (2) 3))

==> NIL

(equal '(1 (2) 3) '(1 (2) 3))

==> T

(equal '(a . b) '(a . b))

==> T

(setq z '(a . b))

==> (a . b)

(equal '(a . b) z)

==> T

Видно, что функция корректно работает не только для списков, но и для точечных пар.

EQUALSET

Функция EQUALSET проверяет, совпадают ли множества, заданные значениями первого и второго аргументов. При совпадении функция возвращает T, при несовпадении - Nil.

Вот примеры работы с функцией EQUALSET :

(equalset '(1 2 3) '(3 2 1))

==> T

(equalset '(1 2 3) '(3 2))

==> NIL
FIRST

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

Вот примеры работы с функцией FIRST :

(first '(1 2 3 4) '(a 3 b 1))

==> 3

(first '(1 2 3 4) '(a b c))

==> 1

При первом вызове возвращается 3, поскольку в списке (a 3 b 1) тройка встречается раньше единицы.

FLAG

Функция FLAG требует два аргумента. Значение первого аргумента должно быть списком атомов. Функция помещает в список свойств каждого атома первого списка флаг, заданный вторым аргументом. В качестве значения функция возвращает Nil.

Вот примеры работы с функцией FLAG:

(flag '(a b c) 'цвет)

==> NIL

(proplist 'a)

==> (цвет)

(proplist 'b)

==> (цвет)

(proplist 'c)

==> (цвет)

(flag '((a b) (c d)) 're)

Аргумент PROPLIST - не атом
==> ERRSTATE

(flag '(a b c d) '(re))

==> NIL

(proplist 'a)

==> ((re) цвет)

(flag '(a b c) 'цвет)

==> NIL

(proplist 'a)

==> ((re) цвет)

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

FLAGP

Функция FLAGP является предикатом, проверяющим, содержится ли в списке свойств атома, заданного значением первого аргумента, флаг, заданный значением второго аргумента.

Вот примеры работы с функцией FLAGP:

(flag '(a) 'цвет)

==> NIL

(flagp 'a 'цвет)

==> T

(flagp 'a 'звук)

==> NIL

У атома a в список свойств занесен индикатор цвет, но не занесен индикатор звук. Функция FLAGP успешно это демонстрирует.

FLATTEN

Функция FLATTEN устраняет у произвольного списка скобочную структуру, перенося все атомы списка на один уровень (делая список "плоским").

Вот примеры работы с функцией FLATTEN:

(flatten '((a b) (c d) (e f)))

==> (a b c d e f)

(flatten '((a . b) (c . d) (e . f)))

==> (a b c d e f)

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

FOR

Макро FOR - это простая реализация оператора цикла. Макро требует четыре параметра. Первый параметр задает параметр цикла; он должен быть атомом. Второй параметр задает начальное значение параметра цикла, а третий - конечное значение. Шаг цикла всегда равен 1. Четвертый параметр задает тело цикла. Тело цикла представляет собой список вызовов произвольных функций. Выполнение макро заключается в циклическом повторении тела цикла при значениях параметра цикла от начального до конечного.

Вот примеры работы с функцией FOR :

(for i 1 10 ((print 'i=)(printline i)))
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9
i=10

==> 10

(setq s 0)

==> 0

(for i 1 10 ((setq s (+ s i))))

==> 10

s

==> 55

(macroexpand for i 1 10 ((setq s (+ s i))))

==> (PROG NIL (SETQ i 1)
     beg_loop (SETQ s (+ s i))
              (SETQ i (ADD1 i))
              (COND ((GREATERP i 10) (RETURN 10)))
              (GO beg_loop))

В первом примере показана простая печать значений переменной цикла. Далее заводится переменная s и ей присваивается значение 0. Затем в цикле в этой переменной накапливается сумма чисел от единицы до десяти (получается 55). Далее показывается, какая PROG-конструкция выполняется на самом деле. Следует обратить внимание на то, что метки локализуются в теле PROG, поэтому макро FOR можно использовать для организации вложенных циклов (а вот параметры вложенных циклов, разумеется, не могут совпадать!)

FORALL

Функционал FORALL последовательно применяет свой второй (функциональный) аргумент к элементам списка, заданного значением первого аргумента. Если результат применения во всех случаях равен T, то возвращается результат T. В противном случае возвращается результат Nil.

Вот примеры работы с функцией FORALL:

(forall '(1 3 5 7) (function (lambda (x)
                               (cond ((= (% x 2) 1) T)
                                     (T nil)))))

==> T

(forall '(1 3 5 7 8) (function (lambda (x)
                                  (cond ((= (% x 2) 1) T)
                                        (T nil)))))

==> NIL

(forall '(1 3 4 5 7) (function (lambda (x)
                                  (cond ((= (% x 2) 1) T)
                                        (T nil)))))

==> NIL

Здесь при первом вызове проверяется на нечетность каждого элемента список (1 3 5 7). Результат, естественно, равен T. Введение в список четного числа (в любой позиции) меняет результат на Nil.

FORODD

Функционал FORODD применяет второй (функциональный) аргумент к элементам списка, заданным значением первого аргумента. Если результат применения равен T для нечетного количества элементов, то FORODD возвращает T, в противном случае - Nil.

Вот примеры работы с функцией FORODD :

(forodd '(1 3 5 7) (function (lambda (x)
                                (cond ((= (% x 2) 1) T)
                                      (T nil)))))

==> NIL

(forodd '(1 3 5 8) (function (lambda (x)
                                (cond ((= (% x 2) 1) T)
                                      (T nil)))))
==> T

Здесь функциональный аргумент проверяет нечетность очередного элемента списка. Поскольку в первом вызове количество нечетных чисел равно четырем, функция возвращает Nil. Во втором вызове количество нечетных чисел равно трем (нечетное), поэтому результа равен T.

FORSOME

Функционал FORSOME применяет свой второй функциональный аргумент к элементам списка, заданным значением первого аргумента. Если хотя бы для одного элемента значение оказывается равным T, функция возвращает T; в противном случае результат будет Nil.

Вот примеры работы с функцией FORSOME:

(forsome '((a b) (c d) (e f)) (function atom))

==> NIL

(forsome '((a b) (c d) e f) (function atom))

==> T

В первом случае к элементам неатомарного списка применяется функция ATOM. Поскольку все элементы списка не являются атомами, то FORSOME возвращает Nil. Во втором случае в списке встречаются атомы, поэтому результат равен T.

GETPROP

Функция GETPROP извлекает из списка свойств атома, задаваемого значением первого аргумента свойство, заданное значением второго аргумента. Если такое свойство отсутствует в списке свойств, - функция возвращает Nil.

Вот примеры работы с функцией GETPROP:

(spropl 'a '(цвет красный звук громкий))

==> (цвет красный звук громкий)

(getprop 'a 'цвет)

==> красный

(getprop 'a 'звук)

==> громкий

(getprop 'a 'место)

==> NIL

Здесь в список свойств атома a заносится свойство цвет со значением красный и свойство звук со значением громкий. Далее, вызов GETPROP успешно излекает из списка свойств атома a значения свойств цвет и звук. Попытка извлечь значение свойства место возвращает Nil, поскольку такого свойства у атома a нет. Кстати, отсюда следует что крайне нежелательно использовать атом Nil, как значение свойства: не имеется возможности различать случай отсутствия свойства и наличия свойства Nil.

INTERSECTION

Функция INTERSECTION требует два аргумента. Оба должны быть списками. Функция возвращает список, состоящий из элементов, входящих в оба списка (теоретико-множественное пересечение).

Вот примеры работы с функцией INTERSECTION:

(intersection '(1 2 3 4 5 6) '(3 5 7))

==> (3 5)

(intersection '((1 2) (3 4) 5 6) '(3 5))

==> (5)

Во втором случае общим элементом, входящим в оба списка будет только атом 5.

LAST

Функция LAST принимает один аргумент, значение которого должно быть списком. Функция возвращает последний элемент этого списка (на верхнем уровне списка).

Вот примеры работы с функцией LAST:

(last '(a  b))

==> b

(last '(a))

==> a

(last '(a (b c)))

==> (b c)

У списка (a (b c)) последний элемент на верхнем уровне - это список (b c), а не атом c, как могло бы показаться.

LCYCLEP

Функция LCYCLEP проверяет значение своего аргумента на наличие циклов по цепочке d-указателей. Если циклы имеются - функция возвращает T, в противном случае - Nil.

Вот примеры работы с функцией LCYCLEP :

(setq z '(a b c))

==> (a b c)

(lcyclep z)

==> Nil

(rplacd (cddr z) z)

==> (c a b c a b c a b c a b c a b c a b c a b c
     a b c a b c a b c a b c a b c a b c a b c a
     b c a b c a b c a b c a b c a b c a b c a b
     c a b c a b c a b c a b c a b c a b c a b c
     ...)

(cyclep z)

==> T

(lcyclep z)

==> T

(setq z '(a b c))

==> (a b c)

(rplaca (cddr z) z)

==> ((a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b
     (a b (a b (a b (a b (a b (a b (a b (a b (a b  ...
     ))))))))))))))))))))))))))))))))))))))))))))))))

(cyclep z)

==> T

(lcyclep z)

==> NIL

В обоих случаях за основу берется линейный список (a b c). В первом случае он "зацикливается" по d-указателю. Вычисление предикатов CYCLEP и LCYCLEP дает в результате T. Во втром случае "зацикливание" выполняется по a-указателю. Предикат CYCLEP дает при вычислении T (циклы есть), а предикат LCYCLEP возвращает Nil - циклов по d-указателю нет.

LENGTH

Функция LENGTH определяет длину списка (количество элементов) на верхнем уровне. Следует отметить, что значение единственного аргумента функции LENGTH должно быть списком. Для точечной пары функция возвращает неправильный результат. Если же значение аргумента - атом, то функция вернет нуль.

Вот примеры работы с функцией LENGTH :

(length '(q w e r t y))

==> 6

(length '(q (w e r t y)))

==> 2

(length '(a . b))

==> 1

(length 'a)

==> 0

Во втором случае на вход функции подан двухэлементный список: первый элемент - атом q, а второй - список (w e r t y). Поэтому функция LENGTH возвращает 2.

LEXORDER

Функция LEXORDER принимает три аргумента. Значения аргументов должны быть списками. Функция сравнивает попарно первые два списка (первый элемент с первым, второй - со вторым, и т.д.). Сравниваются только различные атомы с помощью функции ORDER1. В качестве упорядочивающего списка используется значение третьего аргумента. Результат сравнения (T, Nil или ORDERUNDEF) выдается в качестве результата. Если раньше, чем встретятся различные элементы, исчерпается первый список, то функция вернет T. Если же раньше исчерпается второй список, то функция вернет Nil.

Вот примеры работы с функцией LEXORDER:

(lexorder '(a b c) '(d e f) '(a b c d e f))

==> T

(lexorder '(a b c) '(a b) '(a b c d e f))

==> NIL

(lexorder '(a b) '(a b c) '(a b c d e f))

==> T

(lexorder '(b a) '(a b c) '(a b c d e f))

==> NIL

(lexorder '(g b) '(z b c) '(a b c d e f))

==> orderundef

В первом случае атомы пары a и d стоят в правильном порядке, поэтому результат равен T. Во втором случае раньше исчерпался второй список - результат Nil. В третьем случае раньше исчерпался первый список - результат T. В следующем случае первая пара атомов b и a стоит в "неправильном" порядке - результат Nil. И, наконец, в последнем случае атомы g и z отсутствуют в упорядочивающем списке, поэтому функция возвращает ORDERUNDEF.

LEXORDER1

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

Вот примеры работы с функцией LEXORDER1 :

(lexorder1 '(b c) '(d e) '((d b u v) (a b c d)))

==> NIL

(lexorder1 '(a b c) '(a d e) '((a b c d) (b d u v)) (1 2 3))

==> T

(lexorder1 '(a b w) '(a b z) '((a b c d) (1 2 3) (b d u v)))

==> orderundef

Первый вызов возвращает Nil, поскольку пара атомов b и d стоит в "неправильном" порядке. Второй вызов возвращает T, поскольку пара атомов b и d стоит в "правильном" порядке. В третьем вызове оба атома w и z отсутствуют в управляющем списке (b d u v), поэтому результат равен ORDERUNDEF.

LISTP

Функция LISTP применяется к произвольному S-выражению. Она возвращает T, если выражение есть список (т.е. может быть изображено без точек). В противном случае (когда значение аргумента есть атом или точечная пара, которую нельзя изобразить без точек), - функция возвращает Nil.

Вот примеры работы с функцией LISTP :

(listp 'a)

==> NIL

(listp '(a))

==> T

(listp '(a . b))

==> NIL

(listp '((a . b) (c . d)))

==> NIL

(listp '(a.b))

==> T

Комментария требует последний вызов. Дело в том, что конструкция a.b есть атом, поскольку отсутствуют пробелы между точкой и буквами. Поэтому (a.b) есть список, отсюда и результат - T.

LUNION

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

Вот примеры работы с функцией LUNION:

(lunion '(1 1 1 2 2 3 3 3 3))

Аргумент CAR - атом (3)
==> ERRSTATE

(lunion '((1 1) (1 2) (2 3) (3 3 3)))

==> (2 3 1)

(lunion '((a b) (c d) ((a b) c c) (d d d)))

==> (b a c (a b) d)

Первый (ошибочный!) вызов показывает разницу между функцией LUNION и MAKESET, описанной ниже: список, подаваемый на вход LUNION должен состоять из списков, а не из атомов. Два других вызова показывают, как LUNION строит объединение множеств.

MAKESET

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

Вот примеры работы с функцией MAKESET:

(makeset '(1 1 1 2 2 2 3 3 3 3))

==> (3 2 1)

(makeset '(1 1 1 2 2 2 (3) 3 3 3))

==> (3 (3) 2 1)

(makeset '(1 1 1 2 2 2 (3) (3) 3 3))

==> (3 (3) 2 1)

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

MAP

Функционал MAP принимает два аргумента: произвольный список и функциональное выражение. Функциональный аргумент последовательно применяется к значению первого аргумента, к значению первого аргумента без первого элемента, без второго элемента и т.д. до исчерпания списка. Функционал MAP всегда возвращает Nil; он похож по действию на функционал MAPLIST, но его имеет смысл использовать в случае, когда функциональный аргумент вызывается только ради побочного эффекта (например, ради печати какой-либо информации).

Вот примеры работы с функцией MAP:

(map '(1 2 3 4 5 6 7) (function (lambda (x)
                                   (printline (eval (cons '+ x))))))

28
27
25
22
18
13
7

==> NIL

В этом примере функциональный аргумент принимает на вход список чисел, "приклеивает" к началу знак плюс, вычисляет и печатает значение полученной формы (т.е. сумму чисел). Сначала вычисляется форма (+ 1 2 3 4 5 6 7) - печатается 28; затем функциональный аргумент применяется к остатку исходного списка без первого элемента, т.е. вычисляется форма (+ 2 3 4 5 6 7) - печатается 27 и т.д.

MAPCAR

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

Вот примеры работы с функцией MAPCAR :

(mapcar '(1 2 3 4 5 6 7 8) (function fact))

==> (1 2 6 24 120 720 5040 40320)

(mapcar '(1 2 3 4 5 6 7 8) (function (lambda (x)
                                        (cond ((= (% x 2) 0) 'Чет)
                                              (T 'Нечет)))))

==> (Нечет Чет Нечет Чет Нечет Чет Нечет Чет)

В первом примере функциональный аргумент задан функцией fact, вычисляющей факториал своего аргумента. Легко видеть, что результат MAPCAR в этом случае предствляет собой список факториалов чисел, составляющих исходный список. Во втором примере функциональный аргумент задан лямбда-выражением, которое проверяет значение аргумента на четность. Исходный список заменяется списком атомов Чет и Нечет (в зависмости от того четно или нечетно очередное число исходного списка).

MAPLIST

Функционал MAPLIST применяет свой второй (функциональный) аргумент к значению первого аргумента, и ко всем спискам, получающимся из значения первого аргумента отбрасыванием головы списка. Результаты вычисления объединяются в список, который возвращается в качестве результата.

Вот примеры работы с функцией MAPLIST:

(maplist '(1 2 3 4 5 6 7) (function (lambda (x)
                                    (eval (cons '+ x)))))

==> (28 27 25 22 18 13 7)

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

MEMB

Функция MEMB требует два аргумента. Первый аргумент должен быть атомом, а второй - списком. Функция проверяет, входит ли значение первого аргумента в список, заданный вторым аргументом. Если входит - MEMB возвращает T, иначе Nil.

Вот примеры работы с функцией MEMB:

(memb 1 '(1 2 3 4))

==> T

(memb 5 '(1 2 3 4))

==> NIL

(memb '(5) '(1 2 3 4 (5)))

==> NIL

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

MEMBER

Функция MEMBER принимает два аргумента, оба могут быть списками. Функция проверяет, входит ли список, заданный первым аргументом, в список, заданный вторым аргументом. Если это так, функция возвращает T, а иначе - Nil.

Вот примеры работы с функцией MEMBER:

(member '(5) '(1 2 3 (5) 4))

==> T

(member '(6) '(1 2 3 (5) 4))

==> NIL
NCONC

Функция NCONC принимает два аргумента, значениями которых должны быть списки. Функция "склеивает" списки-аргументы в единый список (выполняя то же действие, что и APPEND. Отличие состоит в том, что NCONC имеет побочный эффект - присваивает значение результата первому аргументу.

Вот примеры работы с функцией NCONC :

(setq z1 '(1 2 3))

==> (1 2 3)

(setq z2 '(4 5 6))

==> (4 5 6)

(nconc z1 z2)

==> (1 2 3 4 5 6)

z1

==> (1 2 3 4 5 6)

Значение переменной z1 изменилось (чего не было бы при использовании функции APPEND).

ORDER

Функция ORDER принимает три аргумента: значения первых двух должны быть атомами, а третьего - списком (будем называть его упорядочивающим списком). Функция проверяет, встречаются ли атомы, заданные первым и вторым аргументами, в списке, заданном третьим аргументом в заданном порядке (в этом случае результат будет T, в противном случае - Nil).

Вот примеры работы с функцией ORDER:

(order 'f 'g '(a b c d e f g h))

==> T

(order 'g 'f '(a b c d e f g h))

==> NIL

(order 'g 'g '(a b c d e f g h))

==> T

(order 'f 'g '(a b c d))

==> NIL

(order 'f 'z '(a b c d e f g h))

==> T

(order 'z 'h '(a b c d e f g h))

==> NIL

В первом вызове атомы f и g встречаются в упорядочивающем списке в "правильном" порядке. Функция возвращает T. Во втром вызове атомы f и g встречаются "неправильном" порядке. Функция возвращает Nil. В третьем случае атомы f и g вообще не встречаются в упорядочивающем списке. В этом случае функция возвращает Nil, что неудобно (поскольку нет возможности отличить неверный порядок от отсутствия атомов в списке). Этого недостатка лишена функция ORDER1, описываемая ниже.

Следующие два вызова показывают, что если первый из атомом найден в упорядочивающем списке, а второй - нет, то функция возвращает T. А если первый атом не найден, а второй найден - результат будет Nil.

ORDER1

Функция ORDER1 (как и описанная выше функция ORDER) принимает три аргумента: значения первых двух должны быть атомами, а третьего - списком (будем называть его упорядочивающим списком). Функция проверяет, встречаются ли атомы, заданные первым и вторым аргументами, в списке, заданном третьим аргументом в заданном порядке (в этом случае результат будет T, в противном случае - Nil). Отличие от функции ORDER заключается в том, если оба атома не найдены в упорядочивающем списке, то функция возвращает не Nil, а специальный атом ORDERUNDEF.

Вот примеры работы с функцией ORDER1 :

(order1 'f 'w '(1 2 3 f g h w))

==> T

(order1 'w 'f '(1 2 3 f g h w))

==> NIL

(order1 'd 'f '(1 2 3 f g h w))

==> NIL

(order1 'd 'z '(1 2 3 f g h w))

==> orderundef

В последнем случае оба атома отсутствуют в списке и функция возвращает ORDERUNDEF.

PAIR

Функция PAIR строит из двух списков список пар (ассоциативный список). Оба аргумента функции должны быть списками.

Вот примеры работы с функцией PAIR :

(pair '(a b c d) '(1 2 3 4))

==> ((a . 1) (b . 2) (c . 3) (d . 4))

(pair '(a b c d e f) '(1 2 3))

==> ((a . 1) (b . 2) (c . 3) (d) (e) (f))

(pair '(a b c d) '(1 2 3 4 5))

==> ((a . 1) (b . 2) (c . 3) (d . 4))

Если первый список длинее второго, то элементы первого списка, лишенные пар, объединяются с атомом Nil. Если же длиннее второй список, то элементы, лишенные пар, просто отбрасываются.

PAIRLIS

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

Вот примеры работы с функцией PAIRLIS:

(pairlis '(a b c) '(1 2 3) '((q . 1) (w . 2) (e . 3)))

==> ((a . 1) (b . 2) (c . 3) (q . 1) (w . 2) (e . 3))

(pairlis '(a b c) '(1 2) '((q . 1) (w . 2) (e . 3)))

==> ((a . 1) (b . 2) (c) (q . 1) (w . 2) (e . 3))

(pairlis '(a b) '(1 2 3) '((q . 1) (w . 2) (e . 3)))

==> ((a . 1) (b . 2) (q . 1) (w . 2) (e . 3))

Видно, что функция PAIRLIS объединяет значения первых двух аргументов в точности, описанная выше функция PAIR.

POP

Функция POP (также, как и функция PUSH) предназначена для организации стека. В качестве хранилища используется линейный список. Результат вычисления (POP A) полностью эквивалентен вычислению (SETQ A (CDR A)) (другими словами, функция имеет побочный эффект).

Вот примеры работы с функцией POP :

(setq Stack '(a b c d))

==> (a b c d)

(pop Stack)

==> (b c d)

Следует отметить, что хотя функция POP принадлежит классу FEXPR, переменная Stack корректно заменяется своим значением. Но происходит это не до вычисления, а уже в процессе вычисления функции. Функция POP по своему действию немного непохожа на традиционную функцию POP: она убирает со стека верхий элемент, но не возвращает его. Более близкой к традиционной функции является функция POPUP, описываемая ниже.

POPUP

Функция POPUP, в отличие от описываемой выше функции POP, не только удаляет из стека верхний элемент, но и присваивает заданной переменной значение этого элемента. Если переменная STACK имеет своим значением список, то результат вычисления (POPUP A STACK) эквивалентен последовательному вычислению двух выражений: (SETQ A (CAR STACK)) и (SETQ STACK (CDR STACK)). В качестве результата функция возвращает Nil.

Вот примеры работы с функцией POPUP:

(setq stack '(a b c d))

==> (a b c d)

(popup z stack)

==> NIL

z

==> a

stack

==> (b c d)

(popup z stack)

==> NIL

z

==> b

stack

==> (c d)

Видно, что каждый вызов POPUP удаляет из списка первый элемент и присваивает его первому аргументу функции POPUP.

POSSESSING

Функционал POSSESSING, используя значение второго аргумента (список), строит новый список, включая в него только те элементы исходного списка, для которого истино значение первого (функционального) аргумента, вычисленной с очередным элементом исходного списка. Функциональный аргумент должен задавать функцию одного переменного, возвражающую T или Nil.

Вот примеры работы с функцией POSSESSING:

(possessing (function (lambda (x) (= (% x 2) 0))) '(1 2 3 4 5 6 7 8))

==> (2 4 6 8)

(possessing (function (lambda (x) (= (% x 2) 1))) '(1 2 3 4 5 6 7 8))

==> (1 3 5 7)

(possessing (function (lambda (x) (= (% x 2) 1))) '(2 4 6 8))

==> NIL

В первом примере функциональный аргумент "отфильтровывает" четные элементы исходного списка. Во втором - нечетные. А в третьем случае - снова нечетные (и, поскольку список не содержит нечетных элементов, результат равен пустому списку).

PROP

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

Вот примеры работы с функцией PROP :

(putprop 'a 'color 'red)

==> a

(proplist 'a)

==> (color red)

(prop 'a 'place (function (lambda nil (printline "Свойства нет"))))
"Свойства нет"

==> "Свойства нет"

(prop 'a 'color (function (lambda nil (printline "Свойства нет"))))

==> (red)

У чистого атома a формируется свойство color со значением red. Вызов списка свойств атома подтверждает успешность формирования. Далее для атома a вызывается функция PROP c запросом свойства place. Поскольку такого свойств нет, вызывается функция, заданная третьим аргументом. Следующий вызов PROP запрашивает свойство color; такое свойство есть, поэтому возвращается хвост списка свойств атома a (после свойства color).

PUSH

Функция PUSH вместе с функциями POP и POPUP предназначена для организации стеков. Результат вычисления (PUSH A1 A2) полностью эквивалентен вычислению (SETQ A2 (CONS A1 A2)). Другими словами, функция "приклеивает" значение своего первого аргумента к началу списка, заданного вторым аргументом.

Вот примеры работы с функцией PUSH:

(push 1 stack)

*Атом stack не имеет значения (не связан).
==> ERRSTATE

(setq stack nil)

==> NIL

(push 1 stack)

==> (1)

(push 2 stack)

==> (2 1)

(push '(3 4) stack)

==> ((3 4) 2 1)

Первый вызов функции выполнен в предположении, что переменная stack не имеет значения. Хотя функция PUSH принадлежит к классу FEXPR, значения ее аргументов вычисляются (уже в процессе вычисления тела функции). Поэтому первый вызов завершается ошибкой. После присвоения переменной stack значения Nil, функция начинает работать правильно. Последний вызов показывает, что в стек можно помещать не только атомы, но и списки.

PUTFLAG

Функция PUTFLAG помещает в список свойств атома, заданного первым аргументом, флаг, заданный значением второго аргумента. В качестве результата, функция выдает модифицированный список атома, заданного первым аргументом.

Вот примеры работы с функцией PUTFLAG:

(putflag 'a 'color)

==> (color)

(putflag 'a 'x)

==> (x color)

(putflag 'a 'y)

==> (y x color)

(proplist 'a)

==> (y x color)

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

PUTPROP

Функция PUTPROP принимает три аргумента. Первый аргумент задает, атом, список свойств которого модифицируется. Второй аргумент задает имя свойства (т.н. индикатор). Третий аргумент (произвольное S-выражение) задает значение свойства. Функция возвращает в качестве значения атом, список свойств которого подвергается изменениям.

Вот примеры работы с функцией PUTPROP:

(putprop 'a 'x 10)

==> a

(putprop 'a 'y 20)

==> a

(proplist 'a)

==> (x 10 y 20)

(putprop 'a 'y 200)

==> a

(proplist 'a)

==> (x 10 y 200)

В список свойств атома a добавляются два индикатора: x и y. Можно убедиться, что повторное добавление индикатора с другим значением проходит корректно (просто меняется значение индикатора).

RANK

Функция RANK принимает два аргумента (значения которых должны быть списками). Функция упорядочивает значение первого аргумента, используя значение второго, как упорядочивающий список. Если в первом списке есть элементы, которые не входят в упорядочивающий список, то такие элементы при упорядочении просто переносятся в конец списка (без изменения относительного расположения).

Вот примеры работы с функцией RANK:

(rank '(b c a a d a c b) '(a b c d))

==> (a a a b b c c d)

(rank '(b c a a d a c b x y z) '(a b c d))

==> (a a a b b c c d x y z)

(rank '(x y z b c a a d a c b) '(a b c d))

==> (a a a b b c c d x y z)

(rank '(z x y z b c a a d a c b) '(a b c d))

==> (a a a b b c c d z x y z)

(rank '(z x y z b c a a d a c b) '((a b) c d))

==> (c c d z x y z b a a a b)

Во всех случаях красным цветом выделены атомы, не участвующие в уорядочении.

REMFLAG

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

Вот примеры работы с функцией REMFLAG :

(putflag 'a 'i1)

==> (i1)

(putflag 'a 'i2)

==> (i2 i1)

(putflag 'a 'i3)

==> (i3 i2 i1)

(remflag '(a) 'i1)

==> NIL

(proplist 'a)

==> (i3 i2)

(remflag '(a) 'i2)

==> NIL

(proplist 'a)

==> (i3)

(remflag '(a) 'i3)

==> NIL

(proplist 'a)

==> NIL

Здесь в список свойств чистого атома a последовательно заносятся флаги i1, i2 и i3. Затем они удаляются с использованием функции REMFLAG.

REMOVE

Функция REMOVE удаляет из списка, заданного значением второго аргумента, все вхождения значения первого аргумента.

Вот примеры работы с функцией REMOVE:

(remove  'a '(a b c d a a b c d))

==> (b c d b c d)

(remove  '(a b) '(a b (a b) c d a (a b) c d))

==> (a b c d a c d)

В первом примере из списка (a b c d a a b c d) удаляются все вхождения атома a. Во втором случае из списка (a b (a b) c d a (a b) c d) удаляются все вхождения списка (a b).

REMOVEF

Функция REMOVEF похожа на функцию REMOVE, но, в отличие от нее, удаляет из значения второго аргумента не все вхождения значения первого аргумента, а только первое вхождение. Если удаляемое значение отсутствует, то список не меняется.

Вот примеры работы с функцией REMOVEF :

(removef  'a '(z a b c d a a b c d))

==> (z b c d a a b c d)

Удален только первый атом a, остальные остались на своих местах.

REMPROP

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

Вот примеры работы с функцией REMPROP:

(putprop 'a 'i1 '(x y z))

==> a

(putprop 'a 'i2 '(1 2 3))

==> a

(proplist 'a)

==> (i1 (x y z) i2 (1 2 3))

(remprop 'a 'i1)

==> a

(proplist 'a)

==> (i2 (1 2 3))

(remprop 'a 'i3)

==> a

(proplist 'a)

==> (i2 (1 2 3))

(remprop 'a 'i2)

==> a

(proplist 'a)

==> NIL

К списку свойств атома a (изначально пустому) добавляются свойства (x y z) под индикатором i1 и (1 2 3) под индикатором i2. Затем с помощью REMPROP удаляется индикатор i1. Последующий вызов PROPLIST показывает, что индикатор и свойство успешно удалены. Далее делается попытка удалить индикатор i3. Поскольку такого индикатора в списке свойств атома a нет, то список свойств не меняется. И, наконец, удаление индикатора i2 очищает список свойств полностью.

REVERSE

Функция REVERSE обращает список, заданный значением своего аргумента. Обращение заключается в том, что элементы списка (на верхнем уровне) перестраиваются в обратном порядке: первый элемент становится последним, второй - предпоследним и т.д.

Вот примеры работы с функцией REVERSE :

(reverse '(1 2 3 4 5))

==> (5 4 3 2 1)

(reverse '(1 (2 3) 4 5))

==> (5 4 (2 3) 1)

Во втором примере видно, что подсписок (2 3) входного списка перемещается на новое место, но не обращается.

SASSOC

Функция SASSOC работает аналогично описанной выше функции ASSOC, но примает третий (функциональный аргумент). Этот функциональный аргумент должен задавать вызов функции без параметров. Функция будет вызвана, если S-выражение, заданное значением первого аргумента, не будет найдено в ассоциативном списке, заданным значением второго аргумента.

Вот примеры работы с функцией SASSOC:

(sassoc 'a '((1 . 2) (3 . 4) (a . b) (c . d)) (function (lambda
                                                 nil (printsline "не найдено!"))))

==> (a . b)

(sassoc 'w '((1 . 2) (3 . 4) (a . b) (c . d)) (function (lambda
                                                 nil (printsline "не найдено!"))))
не найдено!

==> "не найдено!"

В первом случае атом a найден в ассоциативном списке. Во втором случае, атом w отсутствует в ассоциативном списке, поэтому вызывается функция, заданная третьим аргументом.

SELECT

Функция SELECT принимает три аргумента. Значение первого аргумента - произвольное S-выражение. Значением второго аргумента должен быть список. Значение третьего аргумента - тоже произвольное S-выражение. Функция SELECT ищет значение первого аргумента в списке, заданным значением второго аргумента, на нечетных местах. Если выражение найдено - выдается элемент списка, стоящий на следующем (четном) месте. Если выражение не найдено - то в качестве значения выдается значение третьего аргумента.

Вот примеры работы с функцией SELECT:

(select 'c '(a 1 b 2 c 3 d 4) 'NotFound)

==> 3

(select 'a '(a 1 b 2 c 3 d 4) 'NotFound)

==> 1

(select 'z '(a 1 b 2 c 3 d 4) 'NotFound)

==> NotFound

(select '(b b) '((a a) (1 1) (b b) (2 2) (c c) (3 3)) 'NotFound)

==> (2 2)

В последнем случае выражение (b b) найдено на третьем месте. А на следующем (четвертом) месте стоит выражение (2 2), - оно и выдается в качестве результата.

SETOF

Функция SETOF делает в точности то же самое, что и описанная выше функция MAKESET - строит список элементов, из которых состоит значение единственного аргумента. Отличие лишь в том, что порядок следования элементов результата у SETOF обратный.

Вот примеры работы с функцией SETOF:

(makeset '(1 1 1 2 2 2 (3) (3) 3 3))

==> (3 (3) 2 1)

(setof '(1 1 1 2 2 2 (3) (3) 3 3))

==> (1 2 (3) 3)

(makeset '(1 1 1 2 2 2 3 3))

==> (3 2 1)

(setof '(1 1 1 2 2 2 3 3))

==> (1 2 3)
SUBLIS

Функция SUBLIS принимает два аргумента. Значением первого аргумента должен быть ассоциативный список (список пар). Значением второго аргумента может быть произвольное S-выражение. Функция берет каждый атом, входящий в значение второго аргумента, и заменяет его на ассоциацию из ассоциативного списка, заданного значением первого аргумента. Функция возвращает результат замены.

Вот примеры работы с функцией SUBLIS :

(sublis '((a . 1) (b . 2) (c . 3)) '(a b c))

==> (1 2 3)

(sublis '((a . 1) (b . 2) (c . 3)) '(a . b))

==> (1 . 2)

(sublis '((a . 1) (b . 2) (c . 3)) 'a)

==> 1

(sublis '((a . 1) (b . 2) (c . 3)) '(c a z))

==> (3 1 z)

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

SUBSET

Если каждый элемент списка, заданного первым аргументом Функции SUBSET , входит в список, заданный вторым аргументом, то функция возвращает T. В противном случае возвращается Nil. Другими словами, функция проверяет, является ли множество элементов первого списка подмножеством элементов второго.

Вот примеры работы с функцией SUBSET:

(subset '(1 2 3) '(1 2 3 4 5 6))

==> T

(subset '(1 2 3 8) '(1 2 3 4 5 6))

==> NIL

(subset '(1 2 3 3 2 2 1 1 1) '(1 2 3 4 5 6))

==> T

Второй вызов возвращает Nil, поскольку 8 не входит во второй список. Третий пример показыаает, что функция сравнивает множества уникальных элементов. Хотя список (1 2 3 3 2 2 1 1 1) длиннее списка (1 2 3 4 5 6), множество элементов {1 2 3} входит в множество {1 2 3 4 5 6}.

SUBST

Функция SUBST выполняет замену в выражении, заданном третьим аргументом все вхождения значения второго аргумента (на всех уровнях) на значение первого аргумента.

Вот примеры работы с функцией SUBST:

(subst 'a 'b '(a b c))

==> (a a c)

(subst 'b 'a '(a b c))

==> (b b c)

(subst '(1 2) '(a b) '((a b) (c d) (a b)))

==> ((1 2) (c d) (1 2))
SUCHTHAT

Функционал SUCHTHAT принимает два аргумента. Первый аргумент - функциональный, задающий вызов функции одного аргумента (будем называть ее проверяющей функцией). Значение второго аргумента SUCHTHAT должно быть списокм. Функционал SUCHTHAT последовательно применяет функциональный аргумент к элементам списка, заданного значением второго аргумента, и возвращает первый элемент списка, для которого значение проверяющей функции истино.

Вот примеры работы с функцией SUCHTHAT :

(suchthat (function (lambda (x) (= (% x 2) 0))) '(1 3 5 6 7 8))

==> 6

(suchthat (function (lambda (x) (= (% x 2) 1))) '(1 3 5 6 7 8))

==> 1

(suchthat (function (lambda (x) (= (% x 2) 1))) '(0 3 5 6 7 8))

==> 3

(suchthat (function atom) '((a b) (c d) e (1 2)))

==> e

(suchthat (function listp) '((a b) (c d) e (1 2)))

==> (a b)

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

SUCHTHAT1

Функционал SUCHTHAT1 принимает четыре аргумента. Первый из них функциональный (задает проверяющую функцию). Значение второго аргумента задает проверяемый список. Третий и четвертый аргументы могут быть произвольными S-выражениями. Функционал SUCHTHAT1 последовательно применяет функциональный аргумент к элементам проверяемого списка. Если хотя бы для одного из этих элементов, значение проверяющей функции истино, то функционал SUCHTHAT1 возвращает значение своего четвертого аргумента. Если же для всех элементов проверяемого списка значение проверяющей функции ложно, то функционал SUCHTHAT1 возвращает значение своего третьего аргумента.

Вот примеры работы с функцией SUCHTHAT1:

(suchthat1 (function listp) '((a b) (c d) e (1 2)) 'нет 'есть)

==> есть

(suchthat1 (function listp) '((a . b) (c . d) e (1 . 2)) 'нет 'есть)

==> нет

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

SUCHTHAT2

Функционал SUCHTHAT2 принимает три аргумента. Первый аргумент задает проверяющую функцию. Второй - проверяемый список. Третий аргумент, также, как и первый - функциональный. Проверяющая функция последовательно применяется к элементам проверяемого списка. Как только значение проверяющей функции оказывается истиным, к остатку списка, начиная с этого элемента, применяется третий аргумент. Результат применения третьего аргумента к остатку проверяемого списка и возвращается в качестве результата функционал SUCHTHAT2. Если значение проверяющей функции ложно для всех элементов проверяемого списка, то функционал SUCHTHAT2 возвращает Nil.

Вот примеры работы с функцией SUCHTHAT2:

(suchthat2 (function atom) '((1 2) (3 4) 5 6 7 8) (function sumlist))

==> 26

(suchthat2 (function atom) '((1 2) (3 4) (5 6) (7 8)) (function sumlist))

==> NIL

Выполняется суммирование хвоста списка, начиная с первого атома. В первом примере список содержит атомы, а во втором - нет.

TCONC

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

Функция TCONC предназначена для работы с очередью: она помещает значение своего первого аргумента в очередь, заданную значением второго аргумента.

Вот примеры работы с функцией TCONC :

(setq que Nil)

==> NIL

(tconc 'a que)

==> ((a) a)

(tconc 'b que)

==> ((b) b)

(setq que (tconc 'a que))

==> ((a) a)

(setq que (tconc 'b que))

==> ((a b) b)

(setq que (tconc 'c que))

==> ((a b c) c)

Сначала переменной que присваивается значение Nil. Следующий вызов строит очередь из одного элемента. Попытка добавить в эту очередь элемент b может удивить: непонятно, куда исчез элемент a. Но ничего удивительного в полученном результате нет - ведь функция TCONC не модифицирует второй аргумент. Поэтому новая очередь после возврата уничтожается. Чтобы очередь сохранялась, следует результат TCONC явно присвоить второму аргументу (как это и делается при последующих вызовах).

UNION

Функция UNION является теоретико-множественной. Она строит список, содержащий объединение элементов, входящих в значения двух ее списков-аргументов. Естественно, что каждый из элементов входит в объединение в едиственном числе.

Вот примеры работы с функцией UNION :

(union '(1 1 1 2 2 2) '(1 2 3))

==> (3 2 1)

Видно, что функция UNION строит список, содержащий атомы, входящие в списки (1 1 1 2 2 2) и (1 2 3).


Введение в Лисп

Очень краткое введение в язык Лисп
Обращение к читателю.

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

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

Наиболее просто синтаксис Лиспа можно было бы описать с помощью Бэкусовых Нормальных Форм (БНФ), но такое описание слишком лаконично для новичка. Поэтому пришлось пойти на компромисс: вместо Бэкусовых форм основы Лиспа описываются словами. При изучении начальных разделов, описывающих архитектуру языка, читателю рекомендуется смотреть на язык Лисп, как на формальную знаковую систему. Автор полагает, что это - самый простой способ осознанного понимания правила записи выражений Лиспа. После развернутого изложения правил составления выражений Лиспа приводятся сведения о внутреннем представлении выражений. Начиная с этого момента формальная знаковая система наполняется неформальным содержанием.

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

В заключение, автор просит извинения у искушенного читателя (если он сюда забредет!) за навязчивое объяснение элементарных вещей...

О г л а в л е н и е
Лисп-машина.
Алфавит языка Лисп.
Атомы.
Точечные пары - "молекулы" Лиспа.
S-выражения.
Списки.
Внутреннее представление списков.
Взаимодействие с Лисп-машиной. Вычисление значений.
Вычисление значений функций Лиспа.
Проблема вычисляемых аргументов. Классификация функций Лиспа.
Диалог с Лисп-машиной.
Блокировка вычислений. Функция QUOTE.
Присвоение значений атомам. Функции SET, SETQ и CSETQ.
Разбор списков на составные части. Функции CAR, CDR и их комбинации.
Построение списков из составных частей. Функции CONS и LIST.
Проверка на "атомность". Функция АТОМ.
Сравнение атомов. Функции EQ, NEQ, NOT и NULL.
Функция COND.
Арифметические функции Лиспа.
Универсальная функция EVAL.
Создание собственных функций. Функция DEFUN.
Приемы программирования на Лиспе. Рекурсия.
Рекурсия "изнутри". Трассировка выполнения. Функции TRACE и UNTRACE.
Другие примеры рекурсивных функций.
Безымянные функции. Конструкция LAMBDA.
Функциональные аргументы. Функционалы.
Применяющие функционалы. Функции FUNCALL и APPLY.
Отображающие функционалы. Функции MAPLIST и MAPCAR.
Две парадигмы программирования. Функциональное и процедурное программирование.
Процедурное программирование в Лиспе. Функция PROG.
Функции типа FEXPR. Функция DEFUNF.
Функции типа MACRO. Функция DEFMACRO.
Лисп - язык символьного программирования.
Контекст вычисления в HomeLisp.
Динамические и лексические переменные.
Списки свойств атомов.

Лисп-машина.

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

Именно так работает Лисп.

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

Алфавит языка Лисп.

Алфавит языка Лисп включает в себя заглавные и строчные латинские буквы, цифры и все специальные знаки, которые есть на клавиатуре. Буквы национальных языков традиционно в алфавит не входят, хотя нет никаких особых запретов на этот счет. В частности, в алфавит HomeLisp входят все русские строчные и заглавные буквы.

Среди всех символов алфавита выделяются следующие шесть символов, которые используются особым образом: это пробел, точка, открывающая и закрывающая КРУГЛЫЕ скобки, апостроф и двойная кавычка. Остальные символы, в общем, "равноправны".

Атомы.

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

отдельно стоящей точки

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

отдельно стоящего пробела или группы пробелов

отдельно стоящего апострофа или двойной кавычки

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

Среди всех мыслимых атомов Лиспа сразу выделим четыре специальные группы атомов:

Десятичные числа - это атомы, которые представляют собой корректное изображение десятичного числа (целого или с дробной частью; в качестве разделителя целой и дробной части используется точка).

Шестнадцатеричные (битовые) константы представляются атомами вида: &Hnnnn, где nnnn - от одного до восьми символов из набора:

0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f

Строки - это атомы, первый и последний символ которых - двойная кавычка. Между этими кавычками могут располагаться все символы алфавита (включая пробелы и скобки).

Атомы Nil и T. Эти атомы (особенно Nil) используются для разнообразных целей.

Ниже приводится таблица, иллюстрирующая правила построения атомов Лиспа.

Строка | Что это
Abc | Это обычный атом
1Abc | И это - тоже атом (хотя имя и начинается с цифры)
Q$W | И это - тоже атом (хотя имя и содержит знак доллара)
123 | Это атом - число
-12.3 | И это атом - число
6.02E+23 | И это атом - число
A.A | Это атом
A A | А это - не атом. Пробел в имени недопустим!
A( | И это - не атом. Скобки в имени недопустимы!
A'B | И это - не атом. Апостроф в имени недопустим!
() | Как ни странно, это - атом. Почему, будет ясно из дальнейшего.
"Проба пера" | Это - атом-строка. Внутри строки пробелы вполне допустимы
"Проба "пера"" | Это - не атом. Кавычки, стоящие внутри строки, при записи должны удваиваться.
"Проба ""пера""" | Теперь верно.
"Проба 'пера'" | Можно и так. Апостроф внутри строки - обычный символ.
&HFFFFFF | Это - атом-битовая шкала.
&H1122334455667788 | Это - просто атом (а не битовая шкала, как могло бы показаться; слишком много цифр)

Автор надеется, что читатель вполне уяснил правила составления имен атомов.

Точечные пары - "молекулы" Лиспа.

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

(Нечто . Нечто)

Здесь Нечто - это атом или точечная пара. Схема построения точечных пар иллюстрируется таблицей, в которой представлены все типы точечных пар, а также приведены ошибочные построения.

Строка | Что это
(a . b) | Это правильная точечная пара.
((1 . 2) . b) | Это тоже правильная точечная пара.
((1 . 2) . (3 . 4)) | И это тоже правильная точечная пара.
(x . (y . z)) | И это...
(1 .) | А вот это - не точечная пара. После точки до скобки должен стоять атом или точечная пара.
( . 2) | И это - тоже не точечная пара. После скобки до точки должен стоять атом или точечная пара.
(1 . 2 | Не точечная пара. Скобки должны быть сбалансированы.
(name . "Анатолий") | Это снова правильная точечная пара.

Следует обратить внимание на то, что образование точечной пары - это бинарная операция. Запись вида:

( A . B . C)

бессмысленна (по крайней мере, в HomeLisp). Однако, две следующие записи представляют собой корректные точечные пары:

(A . (B . C))

((A . B) . C)

В первой из приведенных выше точечных пар, пара (B . C) является составной частью пары (A . (B . C)). Будем говорить, что пара (B . C) вложена в пару (A . (B . C)).

Введем важное определение: часть точечной пары, расположенную между левой скобкой и точкой будем называть A-частью или А-компонентой. Соответственно, часть пары, расположенную между точкой и правой скобкой будем называть D-частью или D-компонентой.

Что можно сказать о конструкции (1.2)? Здесь точка не отделена пробелами от окружения, а является частью атома 1.2 . Несмотря на внешнее сходство с точечной парой, эта конструкция не соответствует данному выше формальному определению. Мы еще вернемся к этой конструкции при рассмотрении списков, и вскроем ее истиную природу!

S-выражения.

Атом или точечная пара называются S-выражением. В "мире Лиспа" нет ничего, кроме S-выражений; S-выражениями являются и программы и данные. В памяти компьютера все конструкции, кроме атомов, хранятся и обрабатываются в виде точечных пар.

Cписки.

Точечная пара - универсальный способ построения агрегатов из атомов. Однако, точечная запись не очень удобна для человека: в ней слишком много скобок и точек. Было предложено правило, позволяющее записывать S-выражения практически без точек и с использованием значительно меньшего количества скобок.

Этих правил всего два:

Цепочки . Nil просто удаляем;

Цепочки . ( удаляем вместе с соответствующей закрывающей скобкой.

Рассмотрим применение этих правил к записи S-выражения:

(A . (B . (C . Nil)))

На приведенном ниже рисунке показана последовательность упрощений:

Использование описанных правил упрощения привело к тому, что большая часть S-выражений в Лиспе записывается в чисто скобочной нотации и называется списками.

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

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

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

Это последнее определение обычно и приводится в курсах Лиспа. Оно, разумеется, правильно, но не следует забывать, что все S-выражения хранятся в памяти компьютера в виде точечных пар. Точечная запись "незримо присутствует" при работе Лисп-системы (а иногда и неожиданно проявляется; такой пример будет приведен ниже).

Еще раз следует отметить, что всякий список может быть представлен в точечной записи, но не всякая точечная пара является списком. В ряде случаев правила упрощения, приведенные выше, могут сделать точечную запись даже менее наглядной. Вот пример на эту тему: точечная пара ((a . b) . (c . d)) после применения правил упрощения превращается в малонаглядную запись: ((a . b) c . d). Исходная запись этой точечной пары нагляднее упрощенной. К счастью, такие конструкции в реальных программах почти не встречаются.

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

( Нечто-1 Нечто-2 Нечто-3 ...)

где Нечто - атом или список, а многоточие означает повторение. Легко убедиться, что эквивалентной точечной формой такого представления будет:

( Нечто-1 . ( Нечто-2 . ( Нечто-3 . ... . Nil) . Nil) . Nil )

Далее подобному преобразованию следует подвергнуть каждое "Нечто", при условии, что это "Нечто" - список, а не атом. Ниже приводится последовательность преобразований позволяющая получить для списка ((A B) (C D) E F) эквивалентную точечную форму. При этом красным цветом выделены добавляемые точки и скобки на очередном шаге преобразования.

Вот несколько примеров списков в точечной и списковой записи:

Списковая запись | Точечная запись
(A) | (A . Nil)
(A B C) | (A . (B . (C . Nil)))
(A (B C) (E F)) | (A . ((B . (C . Nil)) . ((E . (F . Nil)) . Nil)))

Снова вернемся к рассмотрению конструкции (1.2). Она полностью соответствует определению списка из одного элемента - атома 1.2 (и, следовательно, является на самом деле точечной парой (1.2 . Nil) !).

В заключение дадим еще три важных определения.

Конструкция () соответствует определению списка. Такой список называется пустым списком. В Лиспе принято считать, что пустой список эквивалентен атому Nil.

Первый элемент списка (он может, в свою очередь, быть атомом или списком) называется головой списка.

Часть списка, за исключением головы называется хвостом списка. Если кроме головы список не содержит других элементов, то хвост такого списка есть пустой список.

Внутреннее представление списков.

Оперативная память, в которой хранятся S-выражения, обычно делится на две больших области: список объектов и область списочных ячеек.

В списке объектов хранятся атомы. Каждый атом занимает блок памяти переменного размера. В этом блоке хранится символьное изображение атома и ряд его дополнительных характеристик.

Область списочных ячеек состоит из блоков фиксированного размера. Каждая списочная ячейка хранит два адреса, которые по историческим причинам называются А-указатель и D-указатель. Эти адреса могут указывать как на атомы (т.е. хранить адреса областей из списка объектов), так и на другие списочные ячейки.

Для наглядного изображения списков существуют уже устоявшаяся традиция. Списочная ячейка предствляется прямоугольником, разделеным вертикальной линией на две равные части. В левой части хранится A-указатель, в правой - D-указатель. Атомы изображаются символами (буквами или цифрами). Теперь можно сказать, что точечная пара (A . B) - это одна списочная ячейка, в A-указателе которой находится адрес атома A, а в D-указателе - адрес атома B.

Графически точечную пару изображают так:

или так:

А как можно представить графически список (A)? Поскольку список (A) есть точечная пара (A . Nil), то графическое изображение строится сразу:

Не представляет труда построить графическое изображение и более сложных списков. Так, например, список (A B C D) устроен так:

Полезно соотнести это представление с точечной записью списка (A B C D), которое имеет вид (A . (B . (C . (D . Nil)))). Вот еще один пример внутреннего представленния списка более сложной структуры. Список ((A B) (C D)) хранится в памяти в следующем виде:

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

Как видно из рисунка, S-выражение состоит из двух списочных ячеек и двух атомов. В D-указателе второй списковой ячейки содержится указатель на первую списковую ячейку. Записать это S-выражение в скобочной записи нельзя, но его можно создать (с помощью функций RPLACA и RPLACD; автор счел нецелесообразным описывать эти функции в элементарном введении в Лисп...)

Говоря о внутреннем представлении S-выражений, следует добавить, что каждый атом уникален. Даже если в каком-либо списке атом A встречается многократно, в списке объектов он представлен в единственном экземпляре (т.е. во всех списочных ячейках из которых исходят стрелки, указывающие на атом A, будет содержаться один и тот же адрес).

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

Взаимодействие с Лисп-машиной - вычисление значений.

Лисп-машина читает входящие команды, имеющие вид S-выражений, вычисляет значение каждого из введеных выражений, и выводит результат. Значением S-выражения является, разумеется, тоже S-выражение. (Кроме S-выражений в мире Лиспа ничего нет!) Побочным эффектом вычисления входящих S-выражений является изменение состояния Лисп-машины.

Вычисление значения выполняется по следующим формальным правилам:

Если входное S-выражение является атомом то:

- для атомов T и Nil их значением является сами атомы T и Nil соответственно;

- для атомов, представляющих корректное изображение числа, строки или битовой шкалы значением также является сам атом. Такие атомы (Nil, T, числа, строки и битовые шкалы) будем далее называть самоопределенными.

- все прочие атомы могут иметь значением S-выражение, которое было присвоено атому вызовом функций SET, SETQ или CSETQ. Если атому не было присвоено значения перечисленными выше функциями, то такой атом не имеет значения.

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

Если входное S-выражение является списком, но голова списка представляет собой список, то этот список рассматривается как т.н. лямбда-выражение. Лямбда выражение задает безымянную функцию. Хвост исходного S-выражения задает список параметров этой безымянной функции. Разумеется, лямбда-выражение составляется по строгим правилам, которые будут описаны ниже. Если голова списка не является корректным лямбда-выражением, то вычисление завершается ошибкой.

Все остальные S-выражения значений не имеют. Попытка вычисления таких выражений вызывает ошибку.

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

S-выражения, которые имеют значения, называются формами.

Вычисление значений функций Лиспа.

Что означает "вычислить функцию"? Это означает по S-выражениям - параметрам получить результирующее S-выражение. Процесс вычисления зависит от типа функции. А функции бывают следующих типов:

Встроенные в ядро Лиспа функции, реализованные в машинных кодах (или на языке реализации ядра Лиспа). Будем далее называть такие функции функциями типа SUBR (от Subroutine - подпрограмма);

Реализованные на языке Лисп. Будем далее называть такие функции функциями типа EXPR (от Expression - выражение);

Вызовы функций типа SUBR и типа EXPR не отличаются по форме и выглядят следующим образом:

(Функция Аргумент1 Аргумент2 ... Аргументn)

Многоточие здесь означает повторение. Каждый из аргументов может быть атомом или списоком. Функция "знает" сколько аргументов ей требуется. Если количество аргументов оказывается больше или меньше необходимого, возникает ошибка вычисления функции и выдается соответствующее сообщение. Бывают функции с переменным числом аргументов, а бывают и функции без аргументов. Обращение к такой функции выглядит так:

(Функция)

В математике широко применяется конструкция "функция от функции". Например, запись F(G(x)) означает вычисление G(x), а затем вычисление функции F, с аргументом, равным G(x). Как записать эту конструкцию в обозначениях Лиспа? Очевидно, что запись:

(F G X)

абсолютно неверна. Эта запись означает вычисление функции F с двумя аргументами: первый - G второй X! Более логично было бы записать конструкцию F(G(x)) в виде:

(F (G X))

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

Именно так Лисп-машина и поступает!

Вычисление выражения общего вида:

(F А1 А2 ... Аn)

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

Рассмотрим, например, вычисление следующего S-выражения:

(F (G 1 2 (H "q" "p")) (I 12 -1))

Вычисление будет проходить через следующие этапы:

Вычисляется первый аргумент функции F - S-выражение (G 1 2 (H "q" "P")). Это выражение, в свою очередь, является вызовом функции G с аргументами 1 2 и (H "q" "P"). Значением атома 1 является сам атом 1, а значением атома 2 является сам атом 2. А вот значение S-выражения (H "q" "p") нужно вычислять.

S-выражение (H "q" "p") является вызовом функции H с двумя аргументами "q" и "p". Значением атома "q" является сам атом "q", а значением атома "p" является сам атом "p". Далее вычисляется значение функции H с двумя аргументами "p" и "q". Предположим, что это значение равно S-выражению Рез_Н. Таким образом, первый аргумент исходного вызова функции F принимает вид (G 1 2 Рез_Н). Это выражение вычисляется, предположим, что результат вычисления равен Рез_G.

Далее исходное выражение приобретает вид (F Рез_G (I 12 -1)). Первый аргумент вызова функции F вычислен. Вычисляется второй аргумент. Значение второго аргумента равно значению функции I с двумя аргументами: 1 и 2. Предположим, что это значение равно Рез_I.

Теперь исходное выражение приобрело вид: (F Рез_G Рез_I). Вычисляется значение функции F с заданными аргументами. Это значение и есть значение исходного S-выражения (F (G 1 2 Рез_Н) (I 12 -1))

Проблема вычисляемых аргументов. Классификация функций Лиспа.

Но как быть в том случае, когда некоторой функции требуется не значения аргументов, а сами аргументы? Предположим, есть функция, возвращающая сумму элементов числового списка. Пусть имя этой функции - SUMLIST. Тогда вызов (SUMLIST (1 2 3 4 5)) должен вернуть атом 15. Однако, в соответствии с написанным выше, Лисп сделает попытку вычислить значение списка (1 2 3 4 5), что, в свою очередь, повлечет за собой попытку вычисления значения функции 1 со списком аргументов (2 3 4 5). Это, естественно, вызовет ошибку.

Получается, что никакой функции Лиспа принципиально нельзя передать параметр вида (1 2 3 4 5)?

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

В Лиспе есть класс встроенных функций, которые не вычислют значения своих аргументов, а используют сами аргументы. Встроенные функции, вычисляющие значения аргументов, называются функциями класса SUBR, а встроенные функции, НЕ вычисляющие значения некоторых или всех аргументов, называются функциями класса FSUBR.

Соответственно, функции, написанные на Лиспе, тоже могут не вычислять значения своих аргументов. Функции, написанные на Лиспе и вычисляющие значения аргументов, называются функциями класса EXPR, а функции, написанные на Лиспе и НЕ вычисляющие значения некоторых или всех аргументов, называются функциями класса FEXPR.

Чтобы классификация функций Лиспа стала полностью завершенной, следует добавить, что существует еще один класс функций - MACRO. Эти функции ближе всего к функциям FEXPR, - они тоже не вычисляют значения своих аргументов. Однако вычисление функций типа MACRO имеет особенность, которая позволяет выделить эти функции в отдельный класс (он будет рассмотрен ниже).

В целом, классификация функций Лиспа может быть изображена следующим рисунком:

Диалог с Лисп-машиной.

Большинство Лисп-машин работают по "телетайпному" принципу: пользователь вводит S-выражение, Лисп-вычисляет и выводит ответ (тоже, естественно, S-выражение). Простейшим примером такого диалога может служить консольное окно Windows (или UNIX/LINUX). В HomeLisp пользователь вводит S-выражение в область ввода и получает ответ в области вывода. Подробности диалога описаны в здесь. Введенное выражение дублируется в области вывода, поэтому область вывода содержит полный протокол диалога с пользователем.

Важно отметить следующее: если вычисление введенного завершено с ошибкой, то HomeLisp возвращает в качестве результата специальный атом ERRSTATE (ведь результат должен быть тоже S-выражением!). Если вычисления результата заняли слишком много времени, происходит принудительная остановка Лисп-машины и в качестве результата возвращается специальный атом BRKSTATE

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

Ниже приводится пример взаимодействия с Лисп-машиной. Ответ Лисп-машины предваряется набором символов "==>".

_ver

==> "HomeLisp Вер. 1.11.1 (Файфель Б.Л.)"

Здесь пользователь ввел имя атома _ver (версия ядра) и получил ответ.

Блокировка вычисления - функция QUOTE.

Функция QUOTE принимает ровно один аргумент и возвращает S-выражение, совпадающее с аргументом. Основное назначение функции QUOTE - выборочная блокировка вычисления аргументов при вызове функций класса SUBR и EXPR.

Понятно, что сама функция QUOTE должна принадлежать к классу FSUBR, - она не вычисляет значение своего аргумента, а возвращает сам аргумент.

Выше был приведен пример функции SUMLIST, вычисляющей сумму элементов списка, заданного единственным аргументом. Если функция SUMLIST принадлежит к классу EXPR или SUBR, то попытка вычислить (SUMLIST (1 2 3 4 5)) вызовет ошибку. Ведь сначала будет предпринята попытка вычислить значение списка (1 2 3 4 5), а это S-выражение не имеет значения. А вот вызов (SUMLIST (QUOTE (1 2 3 4 5))) не вызовет ошибки, ведь сначала будет вычислено значение (QUOTE (1 2 3 4 5)), результат вычисления равен (1 2 3 4 5). Этот результат без повторного вычисления будет передан на вход функции SUMLIST, которая вычислит сумму.

Ниже приводится пример использования функции QUOTE. Предполагается, что функция SUMLIST имеется в системе, принадлежит к классу EXPR или SUBR (т.е. вычисляет свои аргументы).

(sumlist (1 2 3))

Не найдена функция 1
==> ERRSTATE

(sumlist (quote (1 2 3)))

==> 6

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

(QUOTE Нечто)
допустимо писать:
'Нечто

Далее в этом разделе будет употребляться именно такая запись. Следует обратить внимание на то, что скобки перед апострофом не ставятся.

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

Присвоение значений атомам. Функции SET, SETQ и CSETQ.

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

Функция SET класса SUBR требует ровно два аргумента. Значением первого аргумента должен быть атом, а значением второго - произвольное S-выражение. Функция присваивает атому, являющемуся значением первого аргумента, значение второго аргумента. Это значение функция одновременно возвращает в качестве результата. Будем называть атом, которому присваивается значение, целевым атомом. Если до вызова функции SET целевой атом уже имел значение, то это значение будет заменено новым.

Рассмотрим несколько примеров использования функции SET:

(set a 1)

Символ a не имеет значения (не связан).
==> ERRSTATE

(set 'a 1)

==> 1

a

==> 1

(set 'a a)

==> 1

a

==> 1

(set 'a 'a)

==> a

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

Следующая команда (set 'a a) должна была бы присвоить атому a значение a. Однако, этого не происходит. Причина заключается в том, что, поскольку втрой аргумент функции SET не квотирован, произойдет его вычисление. Атом a имеет значение 1, поэтому фактически будет выполнена команда (SET 'a 1). Значение атома а не изменится.

А вот команда (set 'a 'a) присваивает атому a значение a. ЧуднО, но логично!

Функция SETQ (класса FSUBR) отличается от SET тем, что не вычисляет значение первого аргумента. Поэтому первый аргумент при вызове SETQ не нужно квотировать. В остальном функция SETQ эквивалентна функции SET.

Атом, которому присвоено значение вызовом функций SET/SETQ обычно называется переменной.

Функция СSETQ (класса FSUBR) отличается от SETQ тем, что целевой атом после возврата получает значение, которое нельзя изменить (превращается в константу). Попытка изменить значение константы с помощью команд SET/SETQ/CSETQ вызывает ошибку. Соответственно, если атом является переменой, превратить его в константу уже не удастся. Рассмотрим примеры:

(csetq ZZ 111)

==> 111

(setq ZZ 9)

Попытка превратить константу ZZ в переменную
==> ERRSTATE

(csetq ZZ 222)

Csetq - попытка изменить значение константы
==> ERRSTATE

(setq xx 9)

==> 9

(csetq xx -11)

Csetq - попытка превратить переменную в константу
==> ERRSTATE

Здесь успешно создана константа ZZ. Попытка изменить ее значение или превратить в переменную, вызывает ошибку.

Разбор списков на составные части. Функции CAR, CDR и их комбинации.

Функции CAR и CDR служат для выделения головы и хвоста списка соответственно. Обе эти функции принадлежат к классу SUBR, т.е. они вычисляют значение своего единственного аргумента. Несколько странные названия этих функций обусловлены историческими причинами (функции названы в честь регистров компьютера IBM-709, на котором была выполнена первая реализация Лиспа). В свременных языках, допускающих обработку списков, эти функции называются HEAD и TAIL. Лисп, однако, остается верен традициям...

Напомним, что головой списка называется его первый элемент (голова может быть атомом или списком). Хвостом списка называется остаток списка без первого элемента. Хвост списка всегда является списком - даже если голова списка является единственным значимым элеменом списка, хвост такого списка есть пустой список или, что равнозначно, - атом NIL.

Функции CAR и CDR можно применить и к точечной паре, не являющейся списком. В этом случае, CAR вернет А-компоненту значения аргумента, а CDR - соответственно D-компоненту.

Попытка применить функции CAR и CDR к атому вызовут состояние ошибки.

Вот исчерпывающий набор примеров вызова функций CAR и CDR:

(car '(1 2 3))

==> 1

(cdr '(1 2 3))

==> (2 3)

(cdr (cdr '(1 2 3)))

==> (3)

(cdr (cdr (cdr '(1 2 3))))

==> NIL

(car '(a . b))

==> a

(cdr '(a . b))

==> b

(car 1)

Аргумент CAR - атом (1)
==> ERRSTATE

(cdr 6)

Аргумент CDR - атом (6)
==> ERRSTATE

Чтобы выделить второй, третий и т.д. элементы списка, можно применять комбинации CAR и CDR. Так например, для того, чтобы получить второй элемент списка S, нужно вычислить форму (CAR (CDR S)). В связи с тем, что различные комбинации вызовов CAR и CDR встречаются в реальных программах достаточно часто, в Лисп обычно вноятся их стандартные комбинации, приведенные ниже.

Вызов | Результат
(CDAR '((1 2) (3 4))) | (2)
(CAAR '((1 2) (3 4))) | 1
(CADR '((1 2) (3 4))) | (3 4)
(CDDR '((1 2) (3 4))) | Nil
(CDDDR '(1 2 3 4)) | (4)
(CADAR '((1 2) (3 4) (5 6))) | 2
(CADDR '((1 2) (3 4) (5 6))) | (5 6)
(CADDDR '(1 2 3 4 5 6)) | 4

Построение списков из составных частей. Функции CONS и LIST.

Функция CONS, принадлежащая к классу SUBR, объединяет значения двух своих аргументов в точечную пару. Если значением первого аргумента является атом, а второго - список, то результатом функции CONS будет список, голова которого есть значение первого аргумента, а хвост - значение второго.

Распространенной ошибкой начинающих является ожидание, что результатом вызова (CONS 'a 'b) будет список (a b). На самом деле результатом будет точечная пара (a . b). Чтобы в результате вызова получился список, нужно вызвать функцию CONS так: (CONS 'a '(b)). Ясно также, что вызов (CONS 'a Nil) вернет точечную пару (a . Nil), что, в соответствии с правилами упрощения, есть список из одного элемента (a).

Вот реальные примеры вызова CONS:

(cons 'a 'b)

==> (a . b)

(cons 'a '(b))

==> (a b)

(cons 'a Nil)

==> (a)

(cons '(a b) '(c d))

==> ((a b) c d)

В последнем случае можно было ожидать, что должен был бы получиться список (a b c d), но это не так! Чтобы понять, почему так происходит, достаточно представить оба аргумента CONS в виде точечных пар, построить результат (тоже в виде пары), и применить к нему правила упрощения.

Список (a b) в точечной нотации имеет вид (a . (b . nil)). Соответственно, список (c d) в точечной записи представляется как (c . (d . nil)). Таким образом, результат (cons '(a b) '(c d)) будет следующим:

((a . (b . nil)) . (c . (d . nil)))

Цепочка упрощений показана ниже (удаляемые элементы выделены красным):

((a . ( b . nil)) . (c . (d . nil)))

((a b . nil) . (c . (d . nil)))

((a b) . (c . (d . nil)))

((a b) c . (d . nil))

((a b) c d . nil)

((a b) c d )

Для получения из двух списков (a b) и (c d) списка (a b c d) служит функция APPEND, описываемая ниже.

В отличие от функции CONS, функция LIST принимает произвольное число аргументов. Эта функция принадлежит классу SUBR (ее аргументы вычисляются). Функция возвращает список, состоящий из значений аргументов.

На первый, поверхностный взгляд, функция LIST бесполезна, - ведь чтобы построить, например список из чисел 1, 2 и 3 достаточно написать '(1 2 3) (или, что то-же самое, (QUOTE (1 2 3))). Однако, если созданы переменные z1, z2 и z3 со значениями 1, 2 и 3 соответственно, то вызов '(z1 z2 z3) вернет результат (z1 z2 z3) (не произойдет замены атомов значениями). А вот вызов (LIST z1 z2 z3) вернет результат (1 2 3). Вот развернутая иллюстрация всему сказанному выше:

(setq z1 1)

==> 1

(setq z2 2)

==> 2

(setq z3 3)

==> 3

'(1 2 3)

==> (1 2 3)

(list 1 2 3)

==> (1 2 3)

'(z1 z2 z3)

==> (z1 z2 z3)

(list z1 z2 z3)

==> (1 2 3)

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

Проверка на "атомность". Функция АТОМ.

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

По-видимому, самый простой вопрос, который можнет относиться к произвольному S-выражению, это вопрос: "Является ли S-выражение атомом или нет?". На этот вопрос отвечает функция ATOM (класса SUBR). Эта функция возвращает атом T, если значение ее единственного аргумента есть атом, и Nil в противном случае.

Вот примеры вызова функции ATOM:

(atom 1)

==> T

(atom '(1 2 3))

==> NIL

(atom (car '(1 2 3)))

==> T

(atom (cdr '(1 2 3)))

==> NIL

Первый и второй примеры понятны без комментариев. Некоторое удивление может вызвать третий пример, ведь аргументом функции ATOM является список (car '(1 2 3)), тем не менее, функция возвращает T. Все дело в том, что функции ATOM на вход попадает не сам аргумент, а его значение (функция принадлежит классу SUBR!). Значением же S-выражения (car '(1 2 3)) является атом 1. Поэтому функция ATOM "увидит" на входе не (car '(1 2 3)), а простую единицу. Естественно, результат вычисления будет T (единица есть атом).

Сравнение атомов. Функции EQ, NEQ, NOT и NULL.

Функция EQ, относящаяся к классу SUBR принимает два аргумента. Она работает следующим образом:

Если значением первого и второго аргумента является один и тот же атом, то функция возвращает в качестве результата атом T;

Во всех остальных случаях функция возвращает атом Nil.

Все остальные случаи включают ситуации, когда значение одного или обоих аргументов не есть атом. Функция EQ вернет Nil даже в случае, когда значением обоих аргументов является одно и то же S-выражение, но не атом!

Функция EQ позволяет корректно сравнивать только атомы; для сравнения S-выражений (списков) служит другая функция - EQUAL.

Вот примеры вызова функции EQ:

(eq 'a 'b)

==> NIL

(eq 'a 'a)

==> T

(eq '(a b) '(a b))

==> NIL

(eq Nil Nil)

==> T

(eq T T)

==> T

При вызове функции атомы Nil и T не квотируются, поскольку, как было отмечено выше, эти атомы являются самоопределенными.

Символы EQ при вызове одноименной функции можно заменить знаком равенства (=). Таким образом, запись (EQ a b) полностью эквивалентна записи (= a b).

Функция NEQ, относящаяся к классу SUBR, принимает два аргумента. Она выполняет действия, в точности противоположные функции EQ:

Если значением первого и второго аргумента является один и тот же атом, то функция возвращает в качестве результата атом Nil;

Во всех остальных случаях функция возвращает атом T.

Все остальные случаи, как и для функции EQ, включают ситуации, когда значение одного или обоих аргументов не есть атом. Следует обратить внимание на то, что функция вернет T даже в случае, когда значением обоих аргументов функции является одно и то же S-выражение, но не атом.

Вместо символов NEQ можно писать знак неравенства <>. Вот несколько примеров вызова функции NEQ:

(neq 'a 'b)

==> T

(neq 'a 'a)

==> Nil

(eq '(a b) '(a b))

==> T

(neq Nil Nil)

==> Nil

(neq T T)

==> Nil

Из рассмотрения функций EQ и NEQ следует, что в Лиспе атомы Nil и T используются как логические индикаторы: атом T обозначает логическую истину; атом Nil - ложь.

Функции NULL и NOT (класса SUBR) представляют собой в сущности, одну и ту же функцию. Действие, выполняемое этой функцией, очень простое. Если значение единственного аргумента функции есть Nil, то функция возвращает T. Во всех остальных случаях (когда значение аргумента НЕ есть Nil, функция возвращает Nil. Примеры вызова:

(not T)

==> NIL

(not Nil)

==> T

(Null Nil)

==> T

(Null 'a)

==> NIL

(Null '(1 2 3))

==> NIL

(Not nil)

==> T

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

Функция COND.

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

(COND (Условие1 Результат1) (Условие2 Результат2) ... (Условиеn Результатn))

В свою очередь, каждая из конструкций Условиеi и Результатi могут быть произвольными S-выражениями (обычно - списками или атомами).

Функция вычисляет свое значение следующим образом:

Вычисляется значение выражения Условие1;

Если это значение есть атом T, то вычисляется значение выражения Результат1 и это значение возвращается в качестве результата функции COND;

Если это значение НЕ есть атом T, то вычисляется значение выражения Условие2;

Если это значение есть атом T, то вычисляется значение выражения Результат2 и это значение возвращается в качестве результата функции COND;

Процесс повторяется до тех пор, пока список аргументов будет исчерпан, либо пока значение одного из условий не окажется атомом T.

Если не одно из условий не дает в результате вычисления атом T, то функция COND вернет атом Nil. (Так реализована функция COND в HomeLisp; в других версиях Лиспа реализация может несколько отличаться. Возможна, например, не сравнения результата условия с атомом T, а несравнение с атомом Nil.)

Легко видеть, что конструкция:

(COND (Условие1 Результат1) (Условие2 Результат2) ... (Условиеn Результатn))

очень похожа на конструкцию оператора IF языка Visual Basic:

IF Условие1 Then Результат1 ELSEIF Условие2 Результат2 ... ELSEIF Условиеn Результатn END IF

Впрочем, знакомые с языком Visual Basic могут заметить, что в Visual Basic перед завершающим END IF может стоять конструкция ELSE Результат, которая получит управление, если все условия оказались ложными. Для этих целей в функции COND обычно в качестве последнего условия ставят атом T. В этом случае последнее условие оказывается гарантированно выполненым.

Рассмотрим примеры вызова COND (пока, весьма элементарые):

(setq z1 1)

==> 1

(cond ((atom z1) "z1 - атом") (T "z1 - не атом"))

==> "z1 - атом"

(setq z1 '(1 2))

==> (1 2)

(cond ((atom z1) "z1 - атом") (T "z1 - не атом"))

==> "z1 - не атом"

Здесь сначала атому z1 присваивается значение 1. При последующем вычислении COND первое условие (atom z1) оказывается истиным. Первый результат представляет собой строку "z1 - атом". Строка есть самоопределенный атом - значение такого атома совпадает с ним самим. Поэтому, строка "z1 - атом" выдается в качестве значения функции COND. Затем атому z1 присваивается значение (1 2) (список). Условие (atom z1) оказывается ложным (значение z1 есть список). Поэтому вычисляется второе условие (атом T). Оно всегда истино, - выдается результат "z1 - не атом", что соответствует действительности.

Арифметические функции Лиспа.

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

В приведенной ниже таблице представлены элементарные арифметические функции HomeLisp.

Имя функции | Сокращение | К-во аргументов | Результат
PLUS | + | переменное | сумма значений
DIFFERENCE | - | переменное | значение первого минус сумма значений остальных
TIMES | * | переменное | произведение значений
QUOTIENT | \ | 2 | целочисленное частное
REMAINDER | % | 2 | целочисленный остаток
DIVIDE | / | переменное | значение первого аргумента делится на произведение значений оставшихся (с плавающей точкой)
EXPT | ^ | 2 | значение первого аргумента, возведенное в степень, равную значению второго.
GREATERP | > | 2 | Т если значение первого аргумента больше значения второго; Nil в противном случае
GREQP | >= | 2 | Т если значение первого аргумента больше или равно значению второго; Nil в противном случае
LESSP | < | 2 | Т если значение первого аргумента меньше значения второго; Nil в противном случае
LEEQP | <= | 2 | Т если значение первого аргумента меньше или равно значению второго; Nil в противном случае
EQ | = | 2 | Т если значение первого аргумента равно значению второго; Nil в противном случае
NEQ | <> | 2 | Т если значение первого аргумента не равно значению второго; Nil в противном случае

Вот примеры использования всех перечисленых функций:

(+ 1 2 3 4)

==> 10

(- 1 2)

==> -1

(* 1 2 3)

==> 6

(/ 1 2)

==> 0.5

(\ 1 2)

==> 0

(% 1 2)

==> 1

(> 1 2)

==> NIL

(< 1 2)

==> T

(<= 1 1)

==> T

(>= 1 1)

==> T

(> 1 1)

==> NIL

(^ 5 3)

==> 125

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

Универсальная функция EVAL.

Функция EVAL, относящаяся к классу SUBR, вычисляет значение своего единственного аргумента и возвращает его в качестве результата. Ядро Лиспа, в сущности, работает следующим образом:

1. Ожидает ввода S-выражения;

2. Передает введенное S-выражение функции EVAL;

3. Выводит полученный результат;

4. Переходит к п.1

Функцию EVAL можно употреблять, например, при необходимости вычислить значение S-выражения, построенного динамически (в процессе вычислений). Другое применение функции EVAL состоит в вычислении (при необходимости) значений аргументов функции класса FEXPR в теле самой функции.

Функция EVAL в известном смысле противоположна по своему действию функции QUOTE - для любого S-выражения x результат вызова (EVAL (QUOTE x)) совпадает с x.

Создание собственных функций. Функция DEFUN.

Язык программирования, который не позволяет программисту создавать собственые функции, бесполезен. В Лиспе пользователь может создавать свои функции трех классов - EXPR, FEXPR и MACRO. В этом разделе будет рассмотрено создание функций класса EXPR.

Для создания функции класса EXPR служит специальная встроенная функция DEFUN (определить функцию). Сама функция DEFUN принадлежит классу FSUBR. Вызов этой функции требует трех аргументов:

Первый аргумент должен атомом;

Второй аргумент должен быть списком атомов;

Третий аргумент может быть произвольной формой (S-выражением, имеющим значение).

Атом, который задан первым аргументом функции DEFUN, после возврата "превратится в функцию". Второй аргумент функции DEFUN называется списком формальных параметров. Третий аргумент функции DEFUN называется телом функции или определяющим выражением.

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

Итак, чтобы создать функцию, необходимо обратиться к порождающей функции DEFUN:

(DEFUN имя_функции (список_параметров) (тело_функции))

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

(* (- x y) (+ x y))

Собирая все всместе, получим:
(DEFUN QDIF (x y) (* (- x y) (+ x y)))

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

Как работает функция? При вызове:

(QDIF Арг-1 Арг-2)

сначала значение Арг-1 присваивается атому x, значение Арг-2 - атому y. Затем вычисляется тело функции. Результат вычисления возвращается в качестве результата вызова. Важно подчеркнуть, что значения аргументов вычисляет не наша функция QDIF, а ядро Лиспа. Поскольку функция QDIF, создана вызовом DEFUN, то она принадлежит к классу EXPR, т.е. ядро Лиспа "знает", что значения аргументов функции QDIF перед вызовом нужно вычислять.

Теперь испытаем нашу функцию:

(DEFUN QDIF (x y) (* (- x y) (+ x y)))

==> QDIF

(QDIF 7 8)

==> -15

(QDIF 8 7)

==> 15

(QDIF 11111 22222)

==> -370362963

(QDIF 11111)

Список формальных параметров и список фактических
параметров имеют разную длину
==> ERRSTATE

(QDIF 11111 22222 33333)

Список формальных параметров и список фактических
параметров имеют разную длину
==> ERRSTATE

(setq x 6)

==> 6

(setq y 6)

==> 6

(QDIF 12 11)

==> 23

x

==> 6

y

==> 6

Рассмотрим приведенную врезку подробнее. Введенная команда DEFUN возвращает в качестве результата атом QDIF. Это является признаком успешного создания функции QDIF. Следующие затем три вызова "свежесозданной" функции QDIF подтверждают ее работспособность.

Последующие попытки вызвать функции QDIF с одним или тремя аргументами вызывают ошибку. Признак ошибки - возврат атома ERRSTATE в качестве результата. Как правило, ошибка сопровождается диагностическим сообщением (которое не нуждается в комментариях).

Далее заводятся две переменные x и y и им присваивается значение 6. Последующий вызов функции с аргументами 12 и 11 дает правильный результат. А вот проверка значений атомов x и y может удивить. Ведь, в соответствии со сказанным выше, переменным x и y при вызове функции должны были присвоиться значения 12 и 11. Тем не менее, значения переменных x и y остаются теми же, какими были до вызова функции!

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

Испытание нашей функции QDIF демонстрирует важный принцип Лиспа: после возврата из функции значения связанных в теле функции переменных будут теми же самыми, что и до вызова функции.

А вот если функция каким-либо образом изменит значение свободной переменной, то это изменение останется и после выхода из функции. Как же функция может изменить значение свободной переменной? Употреблением функций SET/SETQ. Изменим тело нашей функции QDIF следующим образом:

(DEFUN QDIF (x y) (setq z (* (- x y) (+ x y))))

Здесь тело функции представляет собой вызов SETQ, который присваивает свободной переменной z значение разности квадратов, которое вычисляется по прежней формуле. Поскольку SETQ возвращает значение второго аргумента, функция QDIF не теряет работоспособности, однако, если приcвоить атому z какое-либо значение, то после вызова оно будет заменено разностью квадратов:

(DEFUN QDIF (x y) (setq z (* (- x y) (+ x y))))

==> QDIF

(setq z 0)

==> 0

(qdif 5 3)

==> 16

z

==> 16

Приемы программирования на Лиспе. Рекурсия.

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

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

На самом деле, поставленная задача допускает простое и элегантное решение. Обозначим нашу будущую функция SUMLIST. Тогда, если входной список пуст, то функция должна возвращать нуль. Это понятно. А если список непуст, то представляется вполне очевидным, что сумма элементов списка равна его первому элементу (CAR) плюс сумма элементов остатка (CDR). Другими словами, нашу функцию можно представить так:

(DEFUN SUMLIST (x) (COND ((NULL x) 0)
                         (T (+ (CAR x) (SUMLIST (CDR x))))))

==> sumlist

Поскольку эта функция - первый нетривиальный пример лисповской функции, опишем ее подробнее. Тело функции представляет собой COND-конструкцию из двух условий. Первое условие проверяет, не пуст ли список, поданный на вход функции. Если значение выражения (NULL x) истино (равно T), то функция возвращает нуль. В противном случае происходит переход к проверке второго условия. На месте второго условного выражения стоит T (условие всегда истино), поэтому вычисляется сумма первого элемента списка x и значения функции SUMLIST, примененной к списку x без первого элемента. Все! Задача решена.

Пусть читатель обратит внимание на лаконичность определения функции. Всего две строки, причем очень понятного кода! В каком еще языке программирования такое возможно?!

Чтобы убедиться в работоспособности нашей функции, попробуем вычислить сумму списка (1 2 3 4 5):

(sumlist '(1 2 3 4 5))

==> 15

Функция работает. Можно убедиться, что функция будет работать правильно с любым одноуровневым списком чисел. Следует отметить, что функция SUMLIST вызывает сама себя. Такие функции называются рекурсивными.

Может возникнуть вполне естственный вопрос: почему функция, вызывающая сама себя, не "зацикливается"? Это происходит потому, что в теле функции первым условием стоит проверка значения входного параметра на пустоту. Если значение входного параметра есть пустой список (Nil), то происходит выход из функции с возвратом значения 0. А теперь пусть читатель обратит внимание на то, что повоторное обращение к функции SUMLIST происходит не к исходному списку, а к его остатку (после отделения головы). Последующее обращение происходит с остатком остатка и т.д. Поскольку список имеет конечное число элементов, то рано или поздно остаток окажется пустым списком. Это гарантирует завершение цепочки вызовов.

Обычно тело рекурсивной функции состоит из проверки различных условий. Условие, гарантирующее выход, называется терминальным условием. Обычно, хотя и не обязательно, терминальное условие располагается в списке условий первым (как в функции SUMLIST). Что произойдет, если терминальное условие будет опущено? Ответ на этот вопрос дает следующая врезка:

(DEFUN BAD_SUMLIST (x) (+ (CAR x) (BAD_SUMLIST (CDR x))))

==> bad_sumlist

(bad_sumlist '(1 2 3))

Переполнение внутреннего стека
==> ERRSTATE

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

Рекурсия "изнутри". Трассировка выполнения. Функции TRACE и UNTRACE.

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

Трассировка заключается в том, что при входе в трассируемую функцию печатаются значения входных параметров, а при выходе - результат вычисления.

Чтобы включить режим трассировки, существует специальная функция TRACE, принадлежащая классу FSUBR и требующая единственный аргумент - имя функции. Если выполнение TRACE завершено удачно, то возвращается имя трассируемой функции. В противном случае возвращается Nil. Попробуем включить трассировку нашей функции SUMLIST и вновь вычислить сумму элементов списка (1 2 3 4 5):

(trace sumlist)

==> sumlist

(sumlist '(1 2 3 4 5))

Вход в функцию sumlist Аргументы: (1 2 3 4 5)
  Вход в функцию sumlist Аргументы: (2 3 4 5)
    Вход в функцию sumlist Аргументы: (3 4 5)
      Вход в функцию sumlist Аргументы: (4 5)
        Вход в функцию sumlist Аргументы: (5)
          Вход в функцию sumlist Аргументы: NIL
          Возврат из функции sumlist Результат: 0
        Возврат из функции sumlist Результат: 5
      Возврат из функции sumlist Результат: 9
    Возврат из функции sumlist Результат: 12
  Возврат из функции sumlist Результат: 14
Возврат из функции sumlist Результат: 15

==> 15

Видно, что рекурсивная функция SUMLIST, "точит исходный список, как карандаш". Суммирование элементов происходит от последнего к первому.

Если для функции включена трассировка, то любой вызов функции будет сопровождаться выдачей трассировочной информации. Чтобы прекратить трассировку, следует вызвать функцию UNTRACE. Эта функция, как и функция TRACE, принадлежит к классу FSUBR. Функция UNTRACE принимает единственный параметр - имя функции, трассировка которой выключается. При успешном завершении функция возвращает входной параметр. Если запрошенной функции нет в системе, UNTRACE вернет атом Nil.

Другие примеры рекурсивных функций.

Функция SUMLIST верно работает только для атомных одноуровневых списков (т.е. списков, состоящих из атомов). Если попытаться вызвать эту функцию для списков другого типа, то она окажется неработоспособной:

(sumlist '( 1 2 3 (4 5) 6))

Один из аргументов PLUS - не атом
==> ERRSTATE

(trace sumlist)

==> sumlist

(sumlist '( 1 2 3 (4 5) 6))

Вход в функцию sumlist Аргументы: (1 2 3 (4 5) 6)
  Вход в функцию sumlist Аргументы: (2 3 (4 5) 6)
    Вход в функцию sumlist Аргументы: (3 (4 5) 6)
      Вход в функцию sumlist Аргументы: ((4 5) 6)
        Вход в функцию sumlist Аргументы: (6)
          Вход в функцию sumlist Аргументы: NIL
          Возврат из функции sumlist Результат: 0
        Возврат из функции sumlist Результат: 6
Один из аргументов PLUS - не атом
==> ERRSTATE

В протоколе трассировки видно, что ошибка происходит, когда функция пытается к числу 6 прибавить список (4 5).

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

(DEFUN SUMLIST (x)
       (COND ((NULL x) 0)
             (T (+ (CAR x) (SUMLIST (CDR x))))))

Читатель, вероятно, уже догадывается, что для того, чтобы не возникала ошибка, нужно перед сложением применить к выражению (CAR x) функцию SUMLIST. Таким образом, наша функция станет "дважды рекурсивной":

(DEFUN SUMLIST (x)
      (COND ((NULL x) 0)
            (T (+ (SUMLIST (CAR x)) (SUMLIST (CDR x))))))

Это, однако, еще не решает проблему, а, напротив, порождает новые ошибки:

(sumlist '(1 2 3 4 5))

Аргумент CAR - атом (1)
==> ERRSTATE

Функция "разладилась" - совсем перестала работать. Чтобы понять суть ошибки, включим трассировку, и попытаемся вновь вычислить (sumlist '(1 2 3 4 5)):

(trace sumlist)

==> sumlist

(sumlist '( 1 2 3 4 5))

            Вход в функцию sumlist Аргументы: (1 2 3 4 5)
              Вход в функцию sumlist Аргументы: 1
Аргумент CAR - атом (1)
==> ERRSTATE

Функция правильно выделила голову списка (атом 1), но сразу же попыталась передать этот атом на вход себе при рекурсивном вызове. При повторном входе будет вычисляться выражение(SUMLIST 1), и снова будет сделана попытка вычислить первый элемент значения аргумента. Но беда в том, что теперь значение аргумента - уже не список, а атом! Попытка вычислить функцию CAR с атомным аргументом вызывает, естественно, ошибку.

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

(DEFUN SUMLIST (x)
      (COND ((NULL x) 0)
            ((ATOM x) x)
            (T (+ SUMLIST (CAR x)) (SUMLIST (CDR x))))))

Прежде чем активизировать суммирование, проверяем, атом у нас на входе, или список. Если атом - просто возвращаем его в качестве результата.

Можно убедиться, что функция работоспособна:

(sumlist '( 1 2 3 4 5))

==> 15

(sumlist '( 1 (2 3 (4)) 5))

==> 15

Теперь на вход функции можно подавать сколь угодно сложные списки (лишь бы они состояли только из чисел) - результат всегда будет правильным! Если же протрассировать вычисление выражения (sumlist '( 1 2 3 4 5)), то можно убедиться, что за универсальность пришлось заплатить значительно возросшим объемом вычислений, - теперь каждый аргумент проверяется на атомность:

(sumlist '( 1 2 3 4 5))

                Вход в функцию sumlist Аргументы: (1 2 3 4 5)
                  Вход в функцию sumlist Аргументы: 1
                  Возврат из функции sumlist Результат: 1
                  Вход в функцию sumlist Аргументы: (2 3 4 5)
                    Вход в функцию sumlist Аргументы: 2
                    Возврат из функции sumlist Результат: 2
                    Вход в функцию sumlist Аргументы: (3 4 5)
                      Вход в функцию sumlist Аргументы: 3
                      Возврат из функции sumlist Результат: 3
                      Вход в функцию sumlist Аргументы: (4 5)
                        Вход в функцию sumlist Аргументы: 4
                        Возврат из функции sumlist Результат: 4
                        Вход в функцию sumlist Аргументы: (5)
                          Вход в функцию sumlist Аргументы: 5
                          Возврат из функции sumlist Результат: 5
                          Вход в функцию sumlist Аргументы: NIL
                          Возврат из функции sumlist Результат: 0
                        Возврат из функции sumlist Результат: 5
                      Возврат из функции sumlist Результат: 9
                    Возврат из функции sumlist Результат: 12
                  Возврат из функции sumlist Результат: 14
                Возврат из функции sumlist Результат: 15
==> 15

(sumlist '( 1 (2 3 (4)) 5))

                Вход в функцию sumlist Аргументы: (1 (2 3 (4)) 5)
                  Вход в функцию sumlist Аргументы: 1
                  Возврат из функции sumlist Результат: 1
                  Вход в функцию sumlist Аргументы: ((2 3 (4)) 5)
                    Вход в функцию sumlist Аргументы: (2 3 (4))
                      Вход в функцию sumlist Аргументы: 2
                      Возврат из функции sumlist Результат: 2
                      Вход в функцию sumlist Аргументы: (3 (4))
                        Вход в функцию sumlist Аргументы: 3
                        Возврат из функции sumlist Результат: 3
                        Вход в функцию sumlist Аргументы: ((4))
                          Вход в функцию sumlist Аргументы: (4)
                            Вход в функцию sumlist Аргументы: 4
                            Возврат из функции sumlist Результат: 4
                            Вход в функцию sumlist Аргументы: NIL
                            Возврат из функции sumlist Результат: 0
                          Возврат из функции sumlist Результат: 4
                          Вход в функцию sumlist Аргументы: NIL
                          Возврат из функции sumlist Результат: 0
                        Возврат из функции sumlist Результат: 4
                      Возврат из функции sumlist Результат: 7
                    Возврат из функции sumlist Результат: 9
                    Вход в функцию sumlist Аргументы: (5)
                      Вход в функцию sumlist Аргументы: 5
                      Возврат из функции sumlist Результат: 5
                      Вход в функцию sumlist Аргументы: NIL
                      Возврат из функции sumlist Результат: 0
                    Возврат из функции sumlist Результат: 5
                  Возврат из функции sumlist Результат: 14
                Возврат из функции sumlist Результат: 15
==> 15

Хорошо видно, что теперь функция работает корректно с любыми списками, подаваемыми на ее вход.

Рассмотрим еще одну задачу обработки списков. Пусть заданы два произвольных списка. Требуется их объединить (т.е. из списков (a b c) и (d e f) получить список (a b c d e f).

Сразу обратим внимание читателя на то, что "лобовое" применение функции CONS не дает требуемого результата. Значением выражения (CONS '(a b c) '(d e f)) будет список ((a b c) d e f), а отнюдь не (a b c d e f).

Продемонстрируем рекурсивный подход к задаче. Составим функцию APPEND, которая будет принимать два параметра: список первый и список второй. Очевидно, что если первый список пуст (равен Nil), то результат равен второму списку. Если первый спискок непуст, то результат функции APPEND должен быть равен голове первого списка, присоединенной посредством функции CONS к значению функции APPEND, с хвостом первого списка (первый параметр) и исходным вторым списком (второй параметр). И это - все:

(DEFUN APPEND (x y)
       (COND ((NULL x) y)
             (T (CONS (CAR x) (APPEND (CDR x) y)))))

==> APPEND

(APPEND '(a b c) '(d e f))

==> (a b c d e f)

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

Безымянные функции. Конструкция LAMBDA.

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

(LAMBDA (список параметров) (тело функции))

Список параметров - это одноуровневый атомный список (т.е. список, состоящий только из атомов). Тело функции - это S-выражение, зависящее от атомов, входящих в список параметров.

Вот пример задания безымянной функции:

(LAMBDA (x y) (+ (* x x) (* y y) ) )

Легко видеть, что тело функции обеспечивает вычисление величины x2+y2. Как пользоваться безымянными функциями? Если передать приведенное выше выражение Лисп-машине, то мы получим обескураживающий результат:

(LAMBDA (x y) (+ (* x x) (* y y)))

Не найдена функция LAMBDA
==> ERRSTATE

Впрочем, этот результат удивителен только на первый взгляд. Лисп просто пытается вычислить функцию LAMBDA с двумя аргументами: (x y) и (+ (* x x) (* y y)). Функции LAMBDA в системе нет, отсюда и ошибка. Чтобы вычислить значение безымянной функции, нужно составить S-выражение, представляющее собой список, головой которого является вся LAМBDA-конструкция, а последующими элементами - фактические параметры. Ниже показан правильный вызов безымянной функции:

( (LAMBDA (x y) (+ (* x x) (* y y) ) ) 3 4 )

==> 25

Легко видеть, что приведенная выше LAMBDA-конструкция полностью эквивалентна традиционной именованной функции:

(defun SQ (x y) (+ (* x x) (* y y)))

==> SQ

(SQ 3 4)

==> 25

Разница в том, что при задании именованной функции соответствующий атом (в примере - SQ) становится именем функции, при этом, определяющее выражение (+ (* x x) (* y y)) сохраняется в теле функции. А безымянная функция просто вычисляется и "исчезает".

Впрочем, HomeLisp позволяет использовать безымянные функции нетрадиционным образом: если присвоить какому-либо атому в качестве значения корректное LAMBDA-выражение, то появляется возможность использовать это LAMBDA-выражение без повторного задания:

(setq sq2 '(LAMBDA (x y) (+ (* x x) (* y y) )))

==> (LAMBDA (x y) (+ (* x x) (* y y)))

sq2

==> (LAMBDA (x y) (+ (* x x) (* y y)))

(sq2 6 7)

==> 85

Последний вызов (sq2 6 7) очень похож на вызов функции sq2. Сходство, однако, чисто внешнее. Атом sq2 не является именем функции (что будет разъяснено ниже, при рассмотрении списков свойств).

Главное применение безымянных функций - рассматриваемое ниже функциональные аргументы.

Буква греческого алфавита лямбда взята из т.н. лямбда-исчисления разработанного английским математиком Чёрчем. Изображение буквы лямбда давно стало негласным символом языка Лисп.

Функциональные аргументы. Функционалы.

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

(defun p2 (x) (cond ((null x) nil)
                    (T (cons (^ (car x) 2) (p2 (cdr x))))))

==> p2

(p2 '(1 2 3 4 5))

==> (1 4 9 16 25)

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

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

(defun p3 (x) (cond ((null x) nil)
                    (T (cons (^ (car x) 3) (p3 (cdr x))))))

==> p3

(p3 '(1 2 3 4 5))

==> (1 8 27 64 125)

Если сравнить определяющие выражения функций p2 и p3, то легко увидеть, что эти функции различаются только действием, применяющимся к голове списка, в остальном эти функции идентичны. А что, если действие, применяющееся к голове списка, сделать параметром функции? Тогда вместо двух (или большего числа) разных функций достаточно будет составить единственную функцию:

(defun pf (x f) (cond ((null x) nil)
                      (T (cons (f (car x)) (pf (cdr x) f)))))

==> pf

Для того, чтобы воспользоваться новой функцией pf, нужно составить функцию, возводящую одно число в квадрат (или куб):

(defun f^2 (x) (^ x 2))

==> f^2

(defun f^3 (x) (^ x 3))

==> f^3

Читатель вероятно уже догадался, что для возведения элементов списка в квадрат, нужно вызвать функцию pf следующим образом: (pf '(1 2 3 4) f^2). Однако такой вызов приводит к ошибке:

(pf '(1 2 3 4) f^2)

Символ f^2 не имеет значения (не связан).
==> ERRSTATE

И понятно, почему это происходит: функция pf принадлежит к классу EXPR, поэтому, перед вызовом ядро Лиспа делает попытку вычислить значение атома f^2. Чтобы предотвратить эту ошибку, атом f^2 при вызове нужно квотировать:

(pf '(1 2 3 4) 'f^2)

==> (1 4 9 16)

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

Для вызова функционалов предназначена специальная "функция" FUNCTION. С использованием этой "функции" последний вызов записывается так:

(pf '(1 2 3 4) (function f^2))

==> (1 4 9 16)

Cлово "функция" заключается в кавычки, поскольку S-выражение (FUNCTION f^2) не вычисляется в привычном смысле этого слова. Конструкция (FUNCTION f^2) сообщает ядру Лиспа, что f^2 - функциональный аргумент. Такая запись нагляднее квотирования, поскольку функциональный аргумент отмечается явно. Кроме того, использование FUNCTION предпочтительнее еще по одной причине (подробно рассмотренной в соответствующем разделе, посвященном описанию встроенных функций Лиспа.

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

(pf '(1 2 3 4 5 6) (function (lambda (x) (^ x 2))))

==> (1 4 9 16 25 36)

Применяющие функционалы. Функции FUNCALL и APPLY.

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

В ядро Лиспа встраиваются два стандартных функционала FUNCALL и APPLY. Рассмотрим их подробнее.

Функция FUNCALL принадлежит классу SUBR и принимает произвольное количество аргументов. Эта функция применяет свой первый (функциональный) аргумент к оставшемуся списку аргументов. Это выглядит примерно так:

(funcall '* 1 2 3 4 5 6 7 8 9 10)

==> 3628800

Использование FUNCALL позволяет, например, давать именам функций синонимы (примеры взяты из книги Э.Хювенен, Й.Сеппянен [6]):

(setq сложить '+)

==> +

(funcall сложить 1 2 3)

==> 6

Более того, допустимо использовать имя стандартной функции для хранения ссылки на какую-либо другую функцию:

(setq cons '*)

==> *

(cons 2 3)

==> (2 . 3)

(funcall cons 2 3)

==> 6

(funcall 'cons 2 3)

==> (2 . 3)

Здесь атому CONS присвоено значение * (что вполне допустимо!). Следующий вызов показывает, что традиционная функция CONS не теряет работоспособности (образуется точечная пара). А вот при вызове посредством FUNCALL производится замена CONS на * - образуется произведение. В третьем вызове квотирование блокирует замену CONS на *, и вновь образуется точечная пара.

Любопытно, что если вернуться к предыдущему примеру (с присвоением значения + атому сложить), то легко убедиться, что квотирование здесь не дает эффекта:

(setq сложить '+)

==> +

(funcall сложить 2 3 4)

==> 9

(funcall 'сложить 2 3 4)

==> 9

Это связано с особенностью реализации HomeLisp, - в других версиях Лиспа вызов (funcall 'сложить 2 3 4) завершится ошибкой.

С функцией FUNCALL очень схожа функция APPLY (также принадлежащая классу SUBR). Отличие заключается в том, что у функции APPLY ровно два аргумента: первый - функциональный, а второй является списком произвольной длины. Вызов APPLY заключается в том, что вычисляется функция, заданная первым аргументом, со списком параметров, заданным вторым аргументом APPLY. Вот пример вызова этой функции:

(setq mult '*)

==> *

mult

==> *

(apply mult '(1 2 3 4 5 6 7 8 9 10))

==> 3628800

Отображающие функционалы. Функции MAPLIST и MAPCAR.

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

Здесь будут рассмотрены два отображающих функционала: MAPLIST и MAPCAR.

Функционал MAPLIST принимает два аргумента. Значение первого аргумента должно быть списком. Второй аргумент - функциональный. Функция, задаваемая вторым аргументом, должна принимать на вход список. Выполнение функционала заключается в том, что функция, заданная вторым аргументом, последовательно применяется к значению первого аргумента, к этому списку без первого элемента, без первых двух элементов и т.д. до исчерпания списка. Результаты вызова функции объединяются в список, который функционал вернет в качестве значения. Вот примеры вызова MAPLIST:

(maplist '(1 2 3 4 5 6) (function sumlist))

==> (21 20 18 15 11 6)

(maplist '(1 2 3 4 5 6) (function reverse))

==> ((6 5 4 3 2 1) (6 5 4 3 2) (6 5 4 3) (6 5 4) (6 5) (6))

В первом примере используется функция, вычисляющая сумму элементов списка. Первый раз суммируется список (1 2 3 4 5 6); получается 21. Затем суммированию подвергается список (2 3 4 5 6); получается 20 и т.д. Полученные результаты объединяются в список. Результат равен (21 20 18 15 11 6).

Во втором примере используется функция, обращающая список. Эта функция возвращает не атом, а список. При первом вызове обращается список (1 2 3 4 5 6); получается (6 5 4 3 2 1). Затем обращается список (2 3 4 5 6); получается (6 5 4 3 2) и т.д. В результате получается список, состоящий из списков.

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

(mapcar '(1 2 3 4 5 6) (function (lambda (x) (* x x))))

==> (1 4 9 16 25 36)

Функциональное выражение, заданное вторым аргументом MAPCAR, возводит свой аргумент в квадрат. Вызов MAPCAR отображает исходный список на список своих квадратов.

Две парадигмы программирования - функциональная и процедурная.

Читатель, вероятно, обратил внимание на то, что программирование на Лиспе весьма отличается от программирования на традиционных языках (таких, как C/C++, Паскаль, Бэйсик). Вместо того, чтобы разбивать задачу на элементарные шаги, мы составляем одну или более функций, вызов которых приводит к требуемому результату. Часто такие функции оказываются рекурсивными. Подобный подход (или, как сейчас модно говорить - парадигма) называется функциональным программированием. Чистое функциональное программирование не признает операторов присваивания, не использует ветвления и циклы (вместо последних применяется рекурсия).

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

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

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

Но не следует забывать, что первым функциональным языком был все-таки Лисп....

Процедурное программирование в Лиспе. Функция PROG.

Разумеется, Лисп имеет в своем составе и средства для процедурного программирования. Как известно, краеугольным камнем процедурного подхода является понятие оператора. Оператор выполняет различные действия над значениями переменных. Для присвоения переменным значений служат функции SET/SETQ. А для моделирования блока операторов служит конструкция PROG, которая описывается ниже.

Конструкция PROG имет следующий общий вид:

   (PROG
          (Список локальных переменных)

          Атом или вызов функции

          Атом или вызов функции

          Атом или вызов функции

          Атом или вызов функции

          ...
   )

Опишем составные части этой конструкции подробнее. Список локальных переменных представляет собой одноуровневый список атомов. Когда конструкция PROG начинает вычисляться, каждый атом из списка локальных переменных получает значение Nil. Если этот атом уже имел значение, то прежнее значение становится недоступным. Другое название списка локальных переменных - список связанных переменных. В противоположность таким переменным, остальные переменные, использованные в теле функции, называются свободными.

Как видно из врезки, далее в теле PROG подряд располагаются атомы или произвольные вызовы функций. Эти, отдельно стоящие атомы, называются метками. Выполнение тела PROG заключается в том, что последовательно выполняются вызовы функций, а метки пропускаются. Обычно отдельные вызовы в теле PROG-конструкции называются операторами. Таким образом, можно сказаь, что тело PROG-конструкции состоит из операторов и меток.

Могут быть вызваны любые доступные функции, а кроме того, имеются две специфические функции, которые могут употреблятся только в теле PROG. Это функции GO и RETURN. Обе эти функции принадлежат к классу SUBR.

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

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

Если последний вызов функции в теле PROG-конструкции (перед закрывающей скобкой) не есть вызов RETURN, то происходит принудительный выход из PROG-конструкции, а в качестве результата возвращается Nil.

В качестве первого примера PROG-конструкции рассмотрим уже решенную ранее задачу подсчета суммы элементов одноуровнего списка. Решение получится вполне традиционным: заводим локальную переменную для суммы (x) и для остатка списка (y). Далее:

1) Присваиваем переменной x значение нуль, а переменной y - исходный список;

2) Прибавляем к значению x голову y;

3) Присваиваем переменной y значение хвоста y;

4) Если значение y оказывается равным Nil, возвращаем значение х;

5) Переходим к шагу 2.

Ниже показывается, как работает такая конструкция:

(setq lst '(1 2 3 4 5 6 7 8 9 10))

==> (1 2 3 4 5 6 7 8 9 10)

(prog (x y) (setq x 0)
            (setq y lst)
        @   (setq x (+ x (car y)))
            (setq y (cdr y))
            (cond ((null y) (return x))
                  (t (go @)))
)

==> 55

(setq x 777)

==> 777

(prog (x y) (setq x 0)
            (setq y lst)
        @   (setq x (+ x (car y)))
            (setq y (cdr y))
            (cond ((null y) (return x))
                  (t (go @)))
)

==> 55

x

==> 777

Здесь сначала переменной lst присваивается суммируемый список. Далее PROG-конструкция вычисляет сумму его элементов. Как можно убедиться, получается правильный результат. Далее, переменной x "нарочно" присваивается значение 777, после чего снова вычисляется сумма элементов списка. И, хотя переменная x меняет свое значение в теле PROG, после возврата значение x сохраняется.

Функции типа FEXPR. Функция DEFUNF.

Функции типа FEXPR отличаются от функций типа EXPR тем, что при их вычислении, ядро Лиспа не вычисляет значения аргументов, а передает их функции как есть.

Функции типа FEXPR не являются строго необходимыми, - если требуется передать в функцию не значение аргумента, а сам аргумент, то можно использовать функцию типа EXPR с квотированным аргументом. Если функции при любом вызове требуются аргументы, а не их значения, то для простоты вызова, можно создать функцию типа FEXPR.

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

(defunf cosq (x y) (cons x y))

==> consq

(consq 11 22)

==> (11 . 22)

(consq a b)

==> (a . b)

(consq a (b))

==> (a b)

(setq a 111)

==> 111

(consq a (b))

==> (a b)

Первые три вызова новой функции показывают ее работоспособность - функция строит точечную пару из своих аргументов не хуже, чем стандартная функция CONS. Но если присвоить атому a какое-либо значение, то попытка вызова CONSQ с атомом a в качестве одного из аргументов приводит к неожиданному результату: замены аргумента значением не происходит. Впрочем, почему неожиданному? Это - закономерный результат использования функции класса FEXPR. Значения аргументов, переданных функции класса FEXPR, можно вычислить в теле самой функции (используя универсальную функцию EVAL). Можно даже ввести в список параметров функции дополнительный параметр-флаг, управляющий процессом вычисления аргументов:

(defunf consq (x y f) (cond ((null (eval f)) (cons (eval x) (eval y)))
                            ( T (cons x y))))

==> consq

(consq a b t)

==> (a . b)

(consq a b nil)

Символ a не имеет значения (не связан).
==> ERRSTATE

(setq a 111)

==> 111

(setq b 222)

==> 222

(consq a b nil)

==> (111 . 222)

(consq a b t)

==> (a . b)

Если значение параметра f (обязательно вычисляемого в теле функции!), равно Nil, строится точечная пара из значений аргументов. Если же значение параметра f равно T, то строится точечная пара из самих аргументов. Вызов CONSQ с параметром f=Nil требует вычисления значений аргументов (и вызывает ошибку, если хотя бы один из аргументов не имеет значения).

Легко видеть, что при необходимости выборочного вычисления значений аргументов проще использовать функцию класса EXPR/SUBR и просто квотировать аргументы, значения которых не нужно вычислять. Поэтому функции класса FEXPR используются не часто. Гораздо большее применение находит в Лиспе другой тип функций, не вычисляющий своих аргументов - функции типа MACRO.

Функции типа MACRO. Функция DEFMACRO.

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

Если преобразуемый текст есть текст программы на каком-либо языке программирования, то применение средств макрогенерации позволяет, например, на основе одного исходного текста генерировать программы, рассчитанные на различные аппаратные платформы (Windows, Unix, Linux). Практически все современые средства разработки программ имеют в своем составе более или менее мощные макрогенераторы. Если такой макрогенератор автоматически запускается перед компиляцией, то его принято называть препроцессором.

Изобразительные возможности языка препроцессора, как правило, заметно слабее изобразительных возможностей базового языка. Исключение, пожалуй, составляет язык PL/I, в составе которого имеется мощный препроцессор, очень сходный по синтаксису с базовым языком. В языках C/С++, VB, Delphi возможности препроцессора весьма скромны.

Совсем по-иному обстоит дело в Лиспе. Поскольку в Лиспе нет принципиальной разницы между программами и данными (и то, и другое представляет собой списки), то появляется уникальная возможность создания функций, которые выполняются в два этапа. На первом этапе строится S-выражение, которое и вычисляется на втором этапе. Про такие функции еще говорят, что они вычисляют свое тело. Такие функции в Лиспе называются макросами. Лисп не содержит в своем составе препроцессора, - он просто оказывается ненужным!

Функции-макросы начинают вычисляться, как обычные функции класса FEXPR, но результат вычисления вновь подается на вход универсальной функции EVAL. Введение таких функций в Лисп открывает любопытные возможности.

Вернемся к задаче вычисления суммы элементов произвольного списка (уже решенной выше). Оказывается, с использованием макро задачу о суммировании элементов списка можно решить значительно проще! В самом деле, если имеется числовой одноуровневый список произвольной длины, то для вычисления суммы элементов достаточно вычислить значение S-выражения, которое получается простым добавлением плюса в начало исходного списка.

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

(defmacro msumlist (x) (cons '+ x))

==> msumlist

(msumlist (1 2 3 4))

==> 10

В приведенном выше фрагменте использована функция DEFMACRO. Эта функция ничем не отличается от функций DEFUN и DEFUNF, кроме того, что порождает функции, вычисляемые в два этапа (как описано выше). Аргументы функций типа MACRO не вычисляются, поэтому при вызове список (1 2 3 4) не квотируется. Результат получается правильным (и заметно быстрее, чем при рекурсивном суммировании!).

При отладке MACRO бывает полезно увидеть промежуточное S-выражение, которое подается затем на вход EVAL. Увидеть это выражение позволяет встроенная функция MACROEXPAND:

(macroexpand msumlist (1 2 3 4))

==> (+ 1 2 3 4)

Функция MACROEXPAND принадлежит классу FSUBR - ее аргументы не вычисляются. Первый аргумент функции должен быть именем MACRO; последующие - параметрами вызова этого MACRO. Функция возвращает промежуточное S-выражение (т.н. макрорасширение).

Поверхностный взгляд может привести к заключению, что функции типа MACRO бесполезны: ведь не составляет большого труда написать обычную функцию (типа EXPR/FEXPR), которая готовит промежуточное S-выражение, а затем явно вызывает EVAL для его вычисления:

(defun esumlist (x) (eval (cons '+ x)))

==> esumlist

(esumlist '(1 2 3 4))

==> 10

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

(defun ssumlist (x y) (setq y (eval (cons '+ x))))

==> ssumlist

(ssumlist '(1 2 3 4) 'z)

==> 10

z

Символ z не имеет значения (не связан).
==> ERRSTATE

Функция верно считает сумму, но не присваивает результат в качестве значения второму параметру. Понятно, почему это происходит: ведь функция SETQ не вычисляет значения первого аргумента. В результате, присвоение выполняется, но значение получает не атом z, указанный при вызове, а атом y (связанная переменная). При выходе из функции связанная переменная y уничтожается, а с атомом z ничего не происходит. Исправить ситуацию можно, употребив вместо функции SETQ функцию SET:

(defun ssumlist (x y) (set y (eval (cons '+ x))))

==> ssumlist

(ssumlist '(1 2 3 4) 'z)

==> 10

z

==> 10

Все хорошо? Не будем торопиться с выводами... Проверим, как будет работать наша функция, если мы захотим присвоить результат переменной y:

(ssumlist '(1 2 3 4) 'y)

==> 10

y

Символ y не имеет значения (не связан).
==> ERRSTATE

Результат прежний - вычисление верное, но присвоения не происходит. И причина этой неудачи более глубокая: поскольку переменная y связана в теле функции SSUMLIST, то отсутствует возможность присвоить из тела функции значение одноименной свободной переменной. Для решения этой проблемы иногда предлагается давать параметрам функции экзотические названия типа %1, %2 и т.д., в предоположении, что никому не придет в голову называть переменные таким образом...

А вот что получится, если вместо функции EXPR-типа воспользоваться MACRO:

(defmacro msumlist (x y) (list 'setq y (eval (cons '+ (eval x)))))

==> msumlist

(msumlist '(1 2 3 4) s)

==> 10

s

==> 10

(setq a '(1 2 3 4))

==> (1 2 3 4)

(msumlist a xx)

==> 10

xx

==> 10

(msumlist '(1 2 3 4) y)

==> 10

y

==> 10

(macroexpand msumlist '(1 2 3 4) y)

==> (SETQ y 10)

Видно, что функция работает правильно, причем при использовании любых имен. Пусть читатель обратит внимание на то, что в теле макро значение x перевычисляется (... (cons '+ (eval x)) ...) Это позволяет обращаться к функции MSUMLIST с переменной на месте первого параметра, но при явном задании списка требуется квотировать первый аргумент. Последний вызов показывает результат макрорасширения. Видно, что собственно суммирование выполняется на этапе подготовки макрорасширения.

Но почему же присвоение переменной y дает эффект, недостижимый для функций типа EXPR/FEXPR? Все дело в том, что результат макрогенерации выполняется, когда связанные переменные уже уничтожены (как говорят лисперы - в исходном контексте). Поэтому вызов (SETQ y 10) присваивает значение не связанной переменной, а свободной. Вот почему и получается правильный результат.

Было бы неверно полагать, что поскольку MACRO выполняется в два этапа, выполнение макро-функций требует больше времени, чем вычисление функций типа EXPR/FEXPR. Это далеко не всегда так. Например, рекурсивное суммирование списка чисел от единицы до двухсот на компьютере автора выполняется примерно за 60 мсек, тогда, как функция MSUMLIST вычисляет тот же результат менее чем за 9 мсек. (почти в 7 раз быстрее). В данном случае это происходит потому, что рекурсивная программа много времени тратит на рекурсивные вызовы, тогда как при использовании макро рекурсии нет совсем, а вычисление происходит за один вызов функции PLUS, которая принадлежит классу SUBR и работает быстро. Сказанное, разумеется, не означает, что автор противопоставляет макро рекурсивному подходу; просто в ряде случаев макро может оказаться эффективнее.

Одним из наиболее важных применений макро является создание "новых конструкций" в языке. Кавычки здесь употребляются не случайно: макровызов все равно в процессе вычисления преобразуется в одну из базовых конструкций Лиспа.

В качестве иллюстрации сказанного попробуем составить макро, добавляющий к конструкциям базового Лиспа простой "оператор" IF. Синтаксис этого "оператора" определим так:

(IF (условие) THEN (...условие истино...) ELSE (...условие ложно...))

Напишем макро, которое реализует требуемые действия:

(setq a 100)

==> 100

(setq b 200)

==> 200

(defmacro if (cc dummy1 tr dummy2 fl) (list 'cond (list cc tr) (list 'T fl)))

==> if

(if (= a b) Then (setq c a) Else (setq c (/ (+ a b) 2)))

==> 150.0

(if (= a b)
  Then
    (setq c a)
  Else
     (if (> a b)
       Then
           (setq c (/ (+ a b) 2))
       Else
           (setq c (/ (- a b) 2))
     )
)

==> -50.0

Легко убедиться, что макро IF просто генерирует простую COND-конструкцию. Атомы Then и Else при вызове макро связываются в переменных dummy1 и dummy2. Эти переменые в теле макро никак не анализируются. При необходимости можно было бы ужесточить синтаксис "оператора" IF, анализируя значения переменных dummy1 и dummy2.

Из приведенного примера видно, что макро IF допускает вложенный вызов и работает правильно. Кстати, понятно, почему при вызове макро не вычисляются аргументы. Если бы это было бы не так, то в приведенном выше примере пришлось бы квотировать атомы Then и Else, что нельзя признать изящным...

Лисп - язык символьного программирования.

Автор обращает внимание на важное отличие сути арифметики Лиспа от арифметики традиционных языков программирования (таких, как C, Паскаль, Бэйсик и т.д.) В традиционных языках для хранения значений служат переменные, которые являются просто областями памяти (ячейками с размерами, соответствующими типу данных). Присвоение переменной значения означает занесение этого значения в соответствующую ячейку. Если переменная x имеет значение 1, а переменная y - значение 2, то оператор x=x+y вызовет сложение значений переменной x и переменной y c последующим занесением результата в ячейку x.

В Лиспе все обстоит не так. Конструкция, соответствующая оператору x=x+y, в Лиспе имеет вид (SETQ x (+ x y)). При вычислении выражения (+ x y) (при значениях x=1 и y=2) получится 3, что в идеологии Лиспа означает, что будет создан атом 3. Именно этот атом и будет присвоен в качестве значения атому x.

Возникает вопрос: атом 3 может получаться в результате самых разных вычислений, не означает ли сказанное, что в системе может существовать несколько идентичных атомов? Ответ звучит так: "категорически - нет"!. Несмотря на то, что любой атом Лиспа может встречаться в самых разных S-выражениях, в системе он существует в единственном экземпляре. В S-выражении, содержащем атом, хранится не сам атом, а ссылка на него.

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

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

Контекст вычисления в HomeLisp.

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

Ниже речь будет идти в основном о HomeLisp; тонкости других известных автору реализаций будут оговорены явно.

Рассмотрим очень простой пример:

(defun g1 (x y) (cons x y))

==> g1

(setq x 1)

==> 1

(setq y 2)

==> 2

(setq z 3)

==> 3

(g1 x y)

==> (1 . 2)

(g1 y y)

==> (2 . 2)

(g1 z z)

==> (3 . 3)

(g1 'x 'y)

==> (x . y)

Здесь определена функция, которая строит из своих аргументов точечную пару. Функция ведет себя абсолютно предсказуемо. Рассмотрим, к примеру, вызов (g1 x y). Поскольку функция g1 принадлежит классу EXPR, то значения аргументов x и y будут вычислены ядром Лиспа до передачи управления в функцию g1; связанные в теле функции переменные x и y получат значения 1 и 2 соответственно.

Вызов (g1 'x 'y) приведет к тому, что связанные в теле функции переменные x и y получат значения x и y. В результате вычисления будет получен верный результат (x . y).

А теперь чуть-чуть подправим функцию g1 и создадим функцию g2:

(defun g2 (x y) (cons (eval x) (eval y)))

==> g2

Отличие заключается в том, что к значениям аргументов дополнительно применяется функция eval. Испытаем нашу функцию, предполагая, что переменные x, y и z имеют прежние значения (1, 2 и 3) соответственно:

(g2 x y)

==> (1 . 2)

(g2 'x 'y)

==> (x . y)

(g2 'x 'z)

==> (x . 3)

(g2 'y 'x)

==> (x . y)

Результат вызова (g2 x y) не отличается от результата вызова (g1 x y) и это неудивительно: при вычислении значений атомов x и y получается 1 и 2 соответственно. Связанные в теле функции переменные x и y получат значения 1 и 2. И, поскольку (eval 1) равно 1, а (eval 2) равно 2, результат получается такой же, как и при вызове g1.

А вот результат вызова (g2 'x 'y) уже вызывает удивление: получается, что дополнительные вызовы eval в теле функции g2 не срабатывают. Но на самом деле, при вычислении значений аргументов перед вычислением тела функции g2 связанные в теле функции переменные x и y получат значения x и y. Теперь вызов (eval x) вернет результат x, а вызов (eval y) - соответственно y. Из этих значений и образуется точечная пара (x . y).

Еще интереснее результат вызова (g2 'x 'z): атом x не заменяется своим значением (по причине, описанной выше), а атом z почему-то заменяется... Хотя в этом нет ничего удивительного: перед вызовом (g2 'x 'z) происходит вычисление аргументов; получается x и z. Далее связанная переменная x получает значение x, а связанная переменная y - значение z. При вычислении (eval x) снова получится x, а при вычислении (eval z) получится 3. Этим и объясняется результат.

Последний вызов (g2 'y 'x) приводит к совсем парадоксальному результату. На первый взгляд кажется, что должно получиться (y . x), а получается снова (x . y).

Разберемся, почему это происходит.

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

Получается, что функции типа приведенной выше g2 ("довычисляющие" значения аргументов в теле функции) будут работать неверно, если значение фактического параметра совпадет с именем какого-либо формального параметра. В действительности дело обстоит еще хуже: если значение фактического параметра есть выражение, содержащие свободные переменные, имена которых совпадают с именами связанных переменных, то при вычислении в теле функции будут подставлены значения связанных переменных. Крайне неприятная ситуация, совершенно немыслимая для традиционных языков типа Паскаля, Си или Бэйсика (несимвольных языков)! Правда, в Лиспе "довычисление" значений аргументов характерно скорее для класса FEXPR, но ситуация от этого не становится более легкой.

Очевидный способ борьбы с отмеченной проблемой состоит в том, чтобы выбирать для связанных переменных такие имена, которые не придет в голову использовать при вызове функции (благо в Лиспе имя переменной может содержать любые символы). Этот способ, правда нельзя признать изящным. Пусть читатель представит, что он разработал функцию на Cи, в спецификации которой говорится: "не вызывать с фактическим параметром A"... Лавров и Силагадзе в своей книге [1] предупреждают читателей, что одна из функций библиотеки будет работать неверно, если, например, имя формального параметра совпадет с именем фактического. Э.Хювенен и Й.Сеппянен [6] также отмечают эту проблему и указывают надежный способ ее предупреждения - использовать вместо функции макрос.

Определим новую функцию-макрос g2m следующим образом:

(defmacro g2m (x y) (list 'cons (list 'eval x) (list 'eval y)))

==> g2m

то ситуация нормализуется (предполагается, что переменные x, y и z имеют прежние значения):

(g2m x y)

==> (1 . 2)

(g2m y x)

==> (2 . 1)

Использование макросов в Лиспе - совершенно законный прием, однако написание и отладка макроса заметно более трудоемкий процесс, чем написание и отладка обычной функции.

Ситуация дополнительно усложняется при использовании конструкции PROG и локальных переменных. Рассмотрим следующую врезку:

(setq z 111)

==> 111

(defun fqq (n) (prog (z) (setq z 222) (return (eval n))))

==> fqq

(fqq 5)

==> 5

(fqq 'z)

==> 222

Здесь тело функции fqq представляет собой PROG-конструкцию с локальной переменной z. Этой локальной переменной в теле функции присваивается значение 222. Когда функция fqq вычисляется со значением аргумента, равным 5, не происходит ничего неожиданного: результат равен пяти. А вот если вычисляется выражение (fqq 'z), то происходит следующее: сначала связанная переменная n (формальный параметр функции) получает значение z; когда в теле функции будет выполняться оператор RETURN, произойдет обращение к функции eval с аргументом z. В даный момент атом z будет иметь значение 222, поэтому функция вернет результат 222. То, что свободная переменная z имеет значение 111, не окажет никакого влияния на результат: значение свободной переменной "перекрыто" значением одноименной локальной переменной.

Снова получается, что значение выражения зависит от места, где оно вычисляется (т.е. от контекста вычисления). Если функция "довычисляет" значения своих аргументов, то имена локальных PROG-переменных могут повлиять на результат... Поэтому, некоторые версии Лиспа не поддерживают PROG-переменных.

А вот в системах XLisp Дэвида Бетца (David Betz) и Томаса Алми (Thomas Almy), и в Российской реализации Лиспа YLisp Дмитрия Иванова и Арсения Слободюка аналогичный пример дает другой результат:

В этих системах использование локальных переменных с любыми именами не окажет влияния на "довычисление" значений аргументов в теле функции. Видно, что при вычислении (eval n) использовалось значение z, как глобальной переменной.

Вот еще одна достаточно сложная ситуация, которая сводится к проблеме контекста. Рассмотрим следующую врезку:

(defun sqq (n) (prog (x z) (setq x 111) (set n x) (printline z)))

==> sqq

(sqq 'z)
111

==> NIL

z

Символ z не имеет значения (не связан).
==> ERRSTATE

Здесь происходит неявное "довычисление" значения параметра в теле функции. Поскольку функция SET принадлежит к классу SUBR, то при вычислении (set n x) ядро Лиспа сначала вычислит значение n (при этом получится z), затем вычислит значение второго аргумента (получится 111). Функция SET будет вычисляться с аргументами z и 111. Естественно, атом z получит значение 111, что и будет напечатано. Поскольку z является локальной PROG-переменной, то после выхода из функции атом z не будет иметь никакого значения (или не изменит прежнего значения, если до вызова символ z был переменной).

Совсем по-другому эта функция будет вычисляться в среде XLisp:

Вызов функции SET создает здесь переменную, которая сохраняет значение после выхода из функции. Казалось бы, вызов (set n x) при значении n, равном z, должен был бы изменить значение локальной переменной z, а вместо этого создается свободная переменная z, а локальная переменная не меняет значения. Получается, что set действует в другом контексте.

Отмеченные расхождения долго не давали покоя автору этих строк. Наконец было принято решение ввести в ядро языка две функции geval (вычислить в глобальном контексте) и gset (присвоить в глобальном контексте).

Функция geval работает совершенно аналогично функции eval за одним исключением: при вычислении значений атомов сначала проверяется, не существует ли свободная переменная с соответствующим именем. И, если такая переменная существует, используется ее значение; в противном случае используется значение локальной PROG переменной. Если же одноименная локальная переменная не найдена, то функция geval "полагает", что атом не имеет значения.

Приведенный выше пример, в котором вместо eval используется geval, дает точно такой же результат, что и в XLisp:

(setq z 111)

==> 111

(defun fqq (n) (prog (z) (setq z 222) (return (geval n))))

==> fqq

(fqq 'z)

==> 111

Функция gset работает аналогично функции set за одним исключением: при присвоении значения атому (значению первого аргумента) локальные переменные игнорируются. Если соответствующая глобальная переменная существует - она изменит значение; если глобальной переменной не существует - она будет создана.

Вот как выглядит модифицированная функция sqq:

(defun sqq (n) (prog (z x) (setq x 111) (gset n x) (printline z)))

==> sqq

z

Символ z не имеет значения (не связан).
==> ERRSTATE

(sqq 'z)
NIL

==> NIL

z

==> 111

Предполагается, что до вызова (sqq 'z) переменная z отсутствовала. Вызов (gset n x) создает переменную z в глобальном контексте. Вызов (printline z) не имеет доступа к переменной z в глобальном контексте, - он печатает значение локальной переменной z (равное, естественно, атому Nil). А свободная переменная z продолжает жить и после выхода из функции sqq.

Динамические и лексические переменные.

Рассмотрим следующий пример:

(defun f (x) (+ x y))

==> f

(f 5)

Символ y не имеет значения (не связан).
==> ERRSTATE

(defun g (y) (f 5))

==> g

(g 10)

==> 15

Здесь символ x связан в теле функции f (является параметром), а символ y свободен. Если не существует глобальной переменной y (созданной вызовом SET/SETQ на верхнем уровне), то вычисление (f x) завершится ошибкой (т.к. свободный символ символ y не имеет значения). При вычислении функции g символ y получит значение и последующее вычисление значения f завершится нормально. Чтобы разобраться, почему это происходит, имеет смысл включить дампирование и посмотреть на пары, возникающие в ассоциативном списке при вычислении выражения (+ x y):

........EVLAM вход: A1=((x) (+ x y)) A2=(5)
.........PAIRLIS вход: ptrAasso=1 A1=(x) A2=(5) Flg= 1
.........
......... Ассоциативный список:
.........
.........  2  -> (x . 5) Параметр
.........  1  -> (y . 10) Параметр

Теперь при вычислении значения символа x получится 5, а при вычислении значения символа y - 10. Получается, что переменная y видима во всех формах, которые вычисляются в течение времени жизни этой переменной. Если переменная создана вызовом SET/SETQ на верхнем уровне (является глобальной), то она будет видима во всех вычисляемых формах текущего сеанса.

Переменная, которая видима во всех формах в течение своего времени жизни, называется динамической. Как можно убедиться, все переменые в HomeLisp (по крайней мере в редакциях ядра 11.*.*) являются динамическими.

Если попытаться выполнить приведенный выше пример в системе XLisp или YLisp, то результат будет другим:

Здесь переменная y, связанная в функции g оказывается невидимой в форме (f 5).

Переменная, которая видима только в форме, в которой определена, называется лексической. В системах XLisp, YLisp (и всех других, следующих стандарту CommonLisp) все переменные по умолчанию являются лексическими. Соблюдения стандарта CommonLisp в части лексических переменных планируется реализовать в редакциях ядра HomeLisp, начиная с версии 13.1.*.

Кстати, в стандарте CommonLisp лексическую переменную легко превратить в динамическую. Для этого достаточно вызвать функцию defvar в форме: (defvar Имя_переменной). После успешного завершения этой команды переменная, имя которой задано при вызове defvar начинает вести себя как динамическая:

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

Сразу видно, что в MuLisp переменные по умолчанию динамические.

Лексические, динамические и глобальные переменные в 13-й редакции ядра HomeLisp

Понятия лексической и динамической переменной принадлежат к числу наиболее тонких понятий Лиспа. В принципе, лексические переменные Лиспа являются аналогами локальных переменных традиционных языков (Паскаля, Бэйсика, Си). В Лиспе, однако, имеются ньюансы, поэтому сформулировать точное определение лексической переменной не так просто, как может показаться. Именно поэтому, И. Болдырев в статье [20] предпочитает определять лексические переменные "от противного" (определяя сначала понятие динамической переменной, и заявляя потом, что все остальные переменные - лексические).

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

Чтобы сказанное не казалось слишком абстрактным, рассмотрим простой пример:

(defun f (x) (* x a))

==> f

(let ((a 1)) (f 5))

Внутри LET: Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

(let ((a 1)) (printline (+ a 7)) (f 5))
8

Внутри LET: Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

Определяется функция f с параметром x. Символ a является свободным по отношению к функции f. Далее следует вызов f в составе LET-предложения. Казалось бы, вызов (f 5) должен вернуть 5, но на самом деле возникает ошибка (Символ a не имеет значения). Это происходит потому, что переменная a имеет лексическую область видимости - она видима только в форме LET; видимость "не просачивается" в тело вложенного вызова. Следующая строка показывает, что переменная a в LET-предложении "жива"; можно вычислить и напечатать a+1, но в теле функции f переменная a невидима.

Такое поведение переменной a вовсе не связано с тем, что эта переменная создана при помощи LET-конструкции. Вот другой пример, дающий тот же результат:

(defun g (a) (f 5))

==> g

(g 1)

Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

Здесь переменная a (равная после вызова g единице) снова невидима в теле функции f. Описанное и задает суть понятия лексическая видимость. Можно несколько "оживить" данное выше определение:

Лексическая переменная, это такая переменная, которая, будучи свободной относительно функции, невидима в этой функции.

Все переменные в HomeLisp по умолчанию являются лексическими.

В противоположность лексическим переменным, динамические переменные видимы во всех вызовах функций. Чтобы сделать переменную динамической, нужно вызвать функцию DEFDYN или DEFVAR. Отличие между этими функциями состоит в том, что DEFDYN устанавливает у своего аргумента признак "динамической области видимости", не трогая значение аргумента, тогда как DEFVAR создает динамическую переменую и сразу же присваивает ей значение (подробности - в разделе, посвященном DEFVAR).

Если переменную a из предыдущих примеров сделать динамической, то результаты вычислений изменятся:

(defdyn 'a)

==> a

a

Assoc: Символ a не имеет значения (не связан).
==> ERRSTATE

(g 1)

==> 5

(let ((a 1)) (printline (+ a 7)) (f 5))
8

==> 5

Легко убедиться, что переменная a теперь видима в теле функции f. Следует отметить, что превращение переменной в динамическую вызовом DEFDYN не присваивает этой переменной никакого значения; попытка вычислить значение a завершается ошибкой. Важно отметить, что став динамической, переменная будет оставаться таковой до завершения работы Лисп-машины.

В HomeLisp в дополнение к динамическим есть также глобальные переменные. Если переменная объявлена глобальной, то она будет видима в любой точке программы (как и положено глобальной переменной). Создать глобальную переменную можно либо вызовом функции DEFGLOB, либо вызвав SET/SETQ на верхнем уровне. При этом "глобальность" и "лексичность/динамичность" не исключают друг друга. Если глобальная переменная не объявлена динамической, то она будет вести себя, как лексическая: установленное выше значение не "просочится" в вызов, а будет просто взято глобальное значение. Глобальная динамическая переменная практически не отличается от просто динамической переменной.

Вот комплексный пример:

(setq a 1)

==> 1
Создана глобальная переменная a

(defun f (x) (* x a))

==> f

(defun g (a) (f 5))

==> g

(g 111)

==> 5

(defdyn 'a)

==> a

(g 111)

==> 555

Сначала создана глобальная переменная a. Затем создается функция f относительно которой переменная a свободна. Затем создается функция g и вызывается с параметром 111. В теле g значение переменной a будет равно 111, однако, поскольку a - не динамическая переменная, в функцию f попадет значение глобальной переменной. Если же сделать a динамической переменной, то при вызове f будет передано значение 111 (и получится соответствующий результат). Отличить лексическую переменную от динамической можно с помощью функции SPECIALP.

Списки свойств атомов.

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

Каждый атом может иметь т.н. список свойств. Атомы-числа, атомы-строки, атомы-битовые константы имеют этот список в обязательном порядке. Имеют список свойств и атомы-имена функций (всех типов - от SUBR до MACRO). Нетрудно догадаться, что этот список связывается с атомом при его создании ядром Лиспа. Как устроены эти списки? Очень просто. При инициализации ядра вместе с самоопределенными атомами типа Nil, T, создаются стандартные атомы-флаги:

SUBR - признак встроенной функции на языке ядра Лиспа, вычисляющей аргументы;

FSUBR - признак встроенной функции на языке ядра Лиспа, не вычисляющей аргументы;

EXPR - признак функции, написанной на Лиспе, вычисляющей аргументы;

FEXPR - признак функции, написанной на Лиспе, не вычисляющей аргументы;

APVAL - признак наличия определяющего выражения;

FIXED - признак целого числа;

BITS - признак битовой шкалы;

STRING- признак строки;

FLOAT - признак числа с плавающей точкой;

MACRO - признак функции типа макро;

WINDOW- признак графического окна;

FILE - признак файла;

BLOB - признак большого двоичного объекта;

COM - признак COM-объекта;

DIALOG- признак диалога;

CONTROL - признак элемента управления

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

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

(proplist 7)

==> (FIXED)

(proplist 7.0)

==> (FLOAT)

(proplist &H7)

==> (BITS)

(proplist "7")

==> (STRING)

(proplist 7,0)

Символ 7,0 не имеет значения (не связан).
==> ERRSTATE

(proplist '7,0)

==> NIL

(proplist 'b)

==> NIL

(proplist nil)

==> (APVAL)

(proplist T)

==> (APVAL)

(proplist _WHITE)

==> (BITS)

Из приведенной врезки вполне очевидно, что атом 7 имеет тип FIXED, атом 7.0 имеет тип FLOAT, атом &H7 имеет тип BITS, а атом "7" - тип STRING. Как видим, список свойств числовых, битовых и строковых констант состоит из единственного элемента.

Некоторое удивление может вызвать результат вычисления (proplist 7,0). Но ничего сверхъестественного не произошло. Цепочка символов 7,0 является атомом Лиспа, но этот атом (не являющийся корректным изображением числа) не имеет значения, а функция PROPLIST принадлежит классу SUBR; попытка вычислить значение атома 7,0 вызывает ошибку. Квотирование исправляет ситуацию. Список свойств атома 7,0 пуст. Точно так же пустым оказывается и список свойств атома b. А вот списки свойств атомов Nil и T оказываются непустыми - они содержат атом APVAL. Интересно отметить, что список свойств атома _WHITE содержит атом BITS (другими словами, атом _WHITE есть битовая константа). Это нисколько не удивительно, поскольку можно убедиться, что атом _WHITE создается вызовом (csetq _WHITE &HFFFFFF)).

Рассмотрим еще несколько примеров вызова функции PROPLIST:

(proplist 'car)

==> (SUBR)

(proplist 'quote)

==> (FSUBR)

(defun f (x y) (* x y))

==> f

(proplist 'f)

==> (EXPR)

(defunf g (x y) (list x y))

==> g

(proplist 'g)

==> (FEXPR)

(proplist 'for)

==> (MACRO)

Хорошо видно, что атом CAR есть имя функции типа SUBR; атом QUOTE - имя функции типа FSUBR; атом f - имя только что созданной функции типа EXPR; атом g - имя только что созданной функции типа FEXPR, а атом for - имя функции типа MACRO.

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

(spropl 'a1 '(q w e r t y))

==> (q w e r T y)

(proplist 'a1)

==> (q w e r T y)

(spropl 2 '(a b c))

==> (a b c)

(proplist 2)

==> (FIXED a b c)

(+ 2 2)

==> 4

В первом примере у атома a1 устанавливается список свойств (q w e r t y). Последующий вызов функции PROPLIST показывает, что список успешно установлен. В следующем примере делается попытка установить список свойств у атома 2. И попытка оказывается успешной: последующий вызов PROPLIST показывает, что у атома 2 список свойств изменился. Стандартный флаг FIXED при этом никуда не делся, - как было отмечено выше, стандартные флаги неуничтожимы. Пусть читатель приведет еще какой-нибудь язык программирования, в котором у двойки могут быть еще какие-либо свойства! (Разве что Forth. Может быть, и до него доберемся...) Кстати, наличие списка свойств нисколько не мешает арифметике - последний пример это подтверждает: 2 + 2 по-прежнему 4.

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

В принципе двух описанных функций (PROPLIST и SPROPL) достаточно для любых манипуляций со списками свойств. При необходимости другие функции можно реализовать на Лиспе (см. PUTFLAG, PUTPROP, FLAG, FLAGP).

Важное замечание

Совершенно естественным применением списков свойств могло бы быть моделирование математических объектов (рациональные числа, векторы, матрицы и т.д.). К сожалению, использование списков свойств здесь не так удобно, как может показаться на первый взгляд. Проблема состоит в том, что каждый атом в Лиспе уникален, соответственно уникален и список его свойств. А вот значения атома зависят от контекста вычислений. Если некий атом используется как локальная переменная, то его значение при выходе из PROG-конструкции восстановится. Но если внутри PROG-конструкции у этого атома модифицировался список свойств, то этот список свойств не изменится при выходе из PROG. Подобная ситуация совершенно недопустима, поэтому в главе, посвященной приемам программирования на Лиспе, признаки создаваемых объектов (массивов, рациональных чисел и т.д.) хранятся в общем списке с данными, составляющими сущность моделируемого объекта.


HLisp - руководство пользователя

Введение

Эта работа посвящается памяти Алексея Жаркова, моего друга и большого энтузиаста Лиспа, безвременно ушедшего из жизни в 1994 году.

HomeLisp - простой интерпретатор стандартного Лиспа, сделанный по лекалам, описанным в великолепной книге С.C. Лаврова и Г.С. Силагадзе "Автоматическая обработка данных. Язык лисп и его реализация" (М. 1978 г.) К огромным достоинствам этой книги (кроме уникального содержания и доступности изложения) следует отнести и то обстоятельство, что многочисленные программы на Лиспе, приведенные в книге, практически не содержат опечаток!

Программа HomeLisp может использоваться на правах FREEWARE и передается в общественную собственность (PUBLIC DOMAIN).

Разработчик будет только рад, если кто-либо применит HomeLisp для обучения или других целей (включая коммерческие, но не нарушающие Конституцию Российской Федерации)! Разработчику можно написать по адресу: catstail@narod.ru или Cats_tail@mail.ru; он с благодарностью воспримет любую конструктивную критику в адрес представляемой программы. Однако, следует предупредить, что программа распространяется "как есть" , и разработчик не несет ответственности за последствия использования HomeLisp.

Что двигало автором?

Во-первых - самоутверждение.

Во-вторых - интерес к языку (в одной статье, посвященной Лиспу, написано примерно следующее: "...даже если вы и не станете применять Лисп, изучив этот язык, вы будете смотреть на программирование другими глазами..." Автор настоящего руководства полностью разделяет эту точку зрения).

И, наконец, желание сделать для наших студентов и школьников, изучающих Лисп, полностью русифицированную, свободно распространяемую, бесплатную программу с удобными средствами отладки и внятным интерфейсом. А то, полазив по Internet-у, автор обратил внимание, что Лиспу учат по системам CommonLisp, XLisp, MuLisp и т.д. Отечественные разработки практически отсутствуют, если не считать системы YLisp. По декларированным возможностям YLisp превосходит HomeLisp, однако предлагаемая авторами YLisp документация практически не содержит примеров применения, что очень затрудняет работу. Кроме того, YLisp оформлен как 32-х битовое консольное приложение с соответствующим интерфейсом. В общем, читающий эту инструкцию может самостоятельно сравнить обе системы и сделать свой выбор.

Настоящее руководство рассчитано на пользователя, знакомого с языком Лисп. Элементарные конструкции языка не объясняются (о Лиспе написано много; в т.ч. и на русском языке). Встроенные стандартные и дополнительные функции описываются в предположении, что пользователь понимает, что такое S-выражение (атом, список, точечная пара); понимает чем декларативное (функциональное) программирование отличается от процедурного (императивного); владеет понятием рекурсии. Разумеется, это руководство не годится для первоначального изучения языка. Но для тех, кто уже знаком с Лиспом, руководство будет небесполезным.

Несколько слов о реализации языка. Разработчик придерживался стандарта языка из книги Лаврова и Силагадзе (практически совпадающего с оригиналом великого Дж. МакКарти). Сделано лишь несколько незначительных дополнений (для облегчения записи программ):

- всесто (QUOTE Нечто) можно писать 'Нечто ;
- вместо PLUS, DIFFERENCE, TIMES, QUOTIENT, REMAINDER можно писать "+ - * \ и %" соответственно.
- для задания функции наряду с конструкцией SEXPR можно использовать сокращенный вариатн DEFUN

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

Установка и удаление HomeLisp

HomeLisp нормально работает в Win-98/2k/XP. Разработчик не проверял работоспособность программы в Win-95, Win-Me и в Vista (за отсутствием под рукой этих ОС). Установка HomeLisp не должна вызвать трудностей. Компьютер, на который выполняется установка, будем называть целевым, а директорию, в которую выполняется установка - целевой. Перед началом процедуры установки желательно закрыть все выполняющиеся приложения. Затем следует запустить инсталлятор iHLisp.exe (являющийся саморазворачивающимся rar-архивом) и задать директорию установки. В директории установки будут созданы поддиректории Lib, Samples, Patt и Com. Назначение поддиректорий следующее:

Директория Lib содержит автозагружаемую библиотеку дополнительных функций;

Директория Samples содержит примеры программ;

Директория Patt содержит шаблоны стандартных конструкций языка;

Директория Com содержит требуемые ActiveX-компоненты;

Поскольку программа HomeLisp написана на VB6, то ей для работы необходимы следующие дополнительные программные компоненты:

Msvbvm60.dll - виртуальна машина VB 6.0;

Mscomctl.ocx - стандартная палитра компонентов (тулбар, статусбар и т.д.);

Msflxgrd.ocx - стандартный грид Микрософт;

Richtx32.ocx - RichText-бокс Микрософт;

cntAssoList.ocx, cntObjList.ocx - служебные компоненты.

С целю предотвращения конфликта устанавливаемых компонентов с существующими, разработчик использовал следующую методику установки:

Все компоненты распаковываются в поддиректорию Com;

Сразу после распаковки автоматически запускается программа setup.exe, которая для каждого из перечисленных выше компонентов проверяет наличие на компьютере зарегистрированной копии. Проверка, разумеется, выполняется не по имени компонента, а по его GUIDу;

Если очередной компонент уже имеется на компьютере, соответствующий файл удаляется из директории Com;

Если же очередной компонент не найден в реестре, то выполняется его регистрация в директории Com, а имя компонента заносится в файл setup.log.

После завершения процесса регистрации программа setup.exe автоматически удаляется и целевой директории.

Подобная "политкорректная" методика установки практически исключает риск конфликта новых компонентов с уже существующими. Если на целевом компьютере были установлены приложения, использующие другие версии компонентов, то ничего страшного не произойдет - старые приложения будут использовать свои версии компонентов, а HomeLisp - свои. В целевой директории кроме исполяемого модуля lispik.exe устанавливается программа unsetup.exe (программа деинсталляции), а также создается файл lispik.ini, содержащий настройки программы. Весь процесс установки (включая распаковку и регистрацию компонентов) занимает 10-15 сек.

Для деинсталляции программы следует запустить unsetup.exe и подтвердить деинсталляцию. Программа unsetup.exe разрегистрирует зарегистрированные ранее компоненты (взяв их имена из файла setup.log), а затем удалит все поддиректории целевой директории вместе с содержимым.

Общие сведения о среде HomeLisp

Для начала работы запустите модуль lispik.exe. После запуска произойдет автоматическая загрузка библиотеки дополнительных функций из модуля \Lib\lib-1.lsp, после чего пользователь увидит главное окно программы:

Рис.1 Главное окно программы

Интерфейс программы достаточно прозрачен. Имеется стандартное меню и тулбар. Можно пользоваться тем и другим по желанию пользователя - все функции тулбара продублированы в главном меню. Часть функций дополнительно реализована в контекстном меню (о чем будет сказано ниже).

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

Основную часть главной формы занимают две области: область ввода внизу (белый фон) и область ответа вверху (желтый фон). Между областями ввода и ответа располагается разделительная линия. Эту линию можно перемещать по вертикали мышью (при нажатой левой клавише). Это дает возможность менять по желанию относительные размеры областей ввода и ответа:

Рис.2 Изменение размеров области ввода

В нижней части формы располагается статус-бар с шестью панелями. В первой панели отобразается текущее количество занятых ячеек списочной памяти. Во второй - текущее количество атомов (длина списка объектов). В третьей панели отображается текущее число переменных (атомов, которым присвоены значения конструкцией SET/SETQ). Четвертая панель показывает суммарное время выполнения последней команды (в сек.). Во время выполнения команд загрузки из файлов в этой панели располагается прогресс-бар. В пятой панели размещается индикатор контроля скобочной структуры вводимых S-выражений. В последней, шестой панели отображаются индикаторы управления статистикой, эхо и дампом.

Назначение командных кнопок тулбара:

1. | Открыть и загрузить файл.При нажатии на эту кнопку появляется стандартный диалог выбора файла с расширением ".lsp". Если пользователь нажимает кнопку "принять" - файл загружается (т.е. последовательно исполняются все S-выражения, содержащиеся в файле). При этом, если в файле содержится определяющее выражение уже заданной ранее функции, то оно заменяет старое определение. То же относится и к значениям переменных: если атом A имел значение 12, а в файле содержится команда (SETQ A 20), то после загрузки файла атом A будет иметь значение 20. Если загрузка завершена без ошибок, то в область ввода выдается итоговое сообщение об успешной загрузке. Эту команду можно выполнить, вызвав встроенную функцию HLisp-а: (RDS ["имя-файла"]). Если имя файла опущено - будет вызвано окно диалога.
2. | Сохранить состояние в файле. При нажатии на эту кнопку появится окно стандартного диалога "сохранить как". Пользователь задает имя файла, в который будут сохранены определяющие выражения всех функций, а также значения всех переменных (из ассоциативного списка).Эту команду можно выполнить, вызвав встроенную функцию HLisp-а: (WRS ["имя-файла"]). Если имя файла опущено - будет вызвано окно диалога.
3. | Очистить область ввода. Позволяет очистить область ввода (ниже разделительной линии). Следует отметить, что при вводе S-выражения область ввода очищается автоматически.
4. | Очистить область вывода. Позволяет очистить область вывода (выше разделительной линии). В процессе работы с системой область вывода скроллируется. Размер сохраняемой части (в Кб) задается в ini-файле. Значение по умолчанию - 10Кб. Для очистки области вывода можно использовать контекстное меню (см. ниже), а также встроенную функцию (CLS).
5. | Включить/выключить режим эха. При включенном режиме эха вводимые S-выражения дублируются в области вывода, после чего вычисляются. Результат вычислений выводится после строки ===>. По умолчанию режим эха включен. При нажатии этой кнопки режим эха выключается. Последующее нажатие вновь включает эхо. Статус эха отображается в шестой панели статус-бара (Эхо=Вкл/Выкл). Включение и выключение режима может осуществляется встроенной функцией (ECHO [выражение]). Если результат вычисления выражения равен Nil, режим эха выключается. Если же результат вычисления выражения не равен Nil, режим эха включается.
6. | Включить/выключить режим статистики. При включенном режиме статистики после вычисления S-выражения в область вывода выводится статистика вызовов функций Лиспа. По умолчанию режим статистики включен. При нажатии этой кнопки режим статистики выключается. Последующее нажатие вновь включает статистику. Статус режима отображается в шестой панели статус-бара (Стат=Вкл/Выкл). Включить статистику можно, введя S-выражение (STAT); выключается статистика вводом S-выражения (UNSTAT).
7. | Включить/выключить формирование дампа. При включенном режиме дампа во внешний файл выводится подробная информация, характеризующая процесс вычисления S-выражения "изнутри". Этот режим может помочь при отладке, но сильно замедляет процесс вычисления. По умолчанию режим выключен. При нажатии кнопки выводится диалог выбора файла для дампа (расширение ".dmp"). Если пользователь отказывается от выбора - режим не включается. Если же файл выбран, то информация о вычислении каждого S-выражения будет дописываться в конец файла. Статус режима отображается в шестой панели статус-бара (Дамп=Вкл/Выкл). Если дамп включен, то в статус-баре отображается имя файла. Как и для режима статистики, существует пара встроенных функций, позволяющая управлять режимом дампирования из программного кода: (DUMP ["имя_файла"]) - включить и (UNDUMP) - выключить дампирование соответственно.
8. | Вставка стандартного шаблона. Для облегчения работы начинающих изучать Lisp, поддерживается возможность вставки в текущее положение области ввода шаблонов стандартных конструкций. В поставку включены следующие шаблоны: COND, LAMBDA, DEFUN, SEXPR, SFEXPR, SMACRO, PROG. Шаблоны как текстовые файлы с расширением .patt хранятся в поддиректории \Patt. При необходимости пользователь может поместить в эту директорию и другие шаблоны. При нажатии на кнопку появится окно выбора шаблона. Пользователь должен выбрать нужный шаблон и нажать кнопку "Принять". Этот режим ниже будет описан более подробно.
9. | Показать/скрыть список объектов. Однократное нажатие на эту кнопку покажет пользователю список всех атомов (список объектов). У каждого атома кроме наименования показываются стандартные индикаторы (APVAL, FIXED, FLOAT, BITS, EXPR, FEXPR, SUBR, FSUBR, MACRO), а также весь список дополнительных свойств. Для атомов типа EXPR, FEXPR, MACRO можно увидеть определяющее выражение. Все, что показывается пользователю, может быть также получено с помощью встроенных и дополнительных функций. Обращаем внимание, что значения атомов в списке объектов не содержатся. Для их просмотра служит режим "показать/скрыть ассоциативный список". Повторное нажатие на кнопку убирает список объектов с главной формы.
10. | Показать/скрыть ассоциативный список. Однократное нажатие на эту кнопку покажет пользователю список всех атомов, имеющих значения. Значение атома можно увидеть, просто введя S-выражение, состоящее из имени атома. Повторное нажатие на кнопку убирает ассоциативный список с главной формы.
11. | Просмотр дампов. Этот режим позволяет просматривать дампы без выхода из среды разработки. Будет отображено диалоговое окно, и пользователь должен будет выбрать имя файла для просмотра.
12. | Уборка мусора. Нажатие этой кнопки запускает подпрограмму сборки мусора. В данной версии убирается мусор в списке объектов и в области списочных ячеек. Особенность реализации интерпретатора такова, что в области списочных ячеек мусор образуется чрезвычайно редко (разве что при работе с циклическими структурами; для линейных структур ненужные далее списочные ячейки утилизируются автоматически). Нажатие кнопки "Убрать мусор" полностью эквивалентно вызову встроенной функции (GCD). В область ответа выводится итоговое сообщение "Утилизировано столько-то списочных ячеек и столько-то атомов".
13. | Конфигурация. Нажатие этой кнопки вызывает форму конфигурации. Пользователь может задать некоторые числовые параметры распределения памяти, а также указать путь к файлу автозагружаемой библиотеки стандартных функций. Вся информация, введенная пользователем, сохраняется в ini-файле lispik.ini. Старое содержимое файла сохраняется в файле lispik.bak.
14. | Помощь. Это то, что вы сейчас читаете.
15. | О программе и авторе. Будет показана форма, на которой представлен номер версии программы (впрочем, он продублирован в заголовке главной формы), а также даны ссылки на сайт разработчика и его почтовый адрес.
16. | Выход. Нажатие на эту кнопку завершает работу. Завершиться можно также, введя команду (EXIT ["Прощальное сообщение"]). Если прощальное сообщение задано, оно будет выведено перед окончательным завершением.
Ввод S-выражений

Для ввода S-выражения следует установить курсор в область ввода, ввести (и, при необходимости, отредактировать) выражение, установить курсор после последней скобки S-выражения и нажать Enter. Текст введенного выражения будет передан интерпретатору только в случае, если скобки сбалансированы. Если же скобки несбалансированы или стоят в неправильном порядке, то выдается звуковой сигнал, а в шестой панели статус-бара появляется предупреждение:

Рис.3 Сообщение о несбалансированности скобок

Для контроля баланса скобок и показа их вложенности существует встроенное средство. Если нажать клавишу F12 (или щелкнуть мышью в пятой панели статус-бара), то включается режим контроля. Суть его состоит в том, что при вводе закрывающей скобки (или просто при прохождении курсора через правую скобку) происходит выделение части S-выражения от закрывающей скобки до открывающей. Время, в течение которого выделение активно, можно задать в ini-файле. Признаком включения контроля скобок является отображения комбинации "(!)" в пятой панели статус-бара. Это выглядит так:

Рис.4 Автоматическое выделение скобочной пары (при включенном режиме контроля)

Корректное (в смысле скобочной структуры) выражение передается интерпретатору. Если включен режим эха, введенное выражение дублируется в области вывода, а вслед за этим, после комбинации "==>" выводится результат вычисления:

Рис.5 Результат обработки S-выражения при включенном эхо.

Команды, введенные пользователем, запоминаются в специальном буфере. Размером этого буфера можно управлять - количество запоминаемых команд задается в конфигурационном файле lispik.ini. Для вызова введенной ранее команды в область ввода, следует нажать сочетание клавиш Ctrl+ВасkSpace. Повторное нажатие этого сочетания клавиш вызовет команду, введенную до предыдущей, и т.д. После вызова самой ранней команды из буфера будет вызвана самая поздняя (предыдущая) команда. Если пользователь вводит команду (S-выражение), которое уже есть в буфере, то введенное выражение не дублируется. В противном случае последняя введенная команда запоминается, а самая старая удаляется из буфера.

В процессе вычисления S-выражений могут произойти ошибки. Сообщения об ошибках выводятся независимо от состояния режима эха. Разработчик обращает внимание на следующее важное обстоятельство. Не следует вводить несколько независимых S-выражений за раз. Точнее, в этом случае обработается только первое S-выражение; остальные будут проигнорированы. Игнорируются символы, стоящие правее правой скобки, закрывающей первое S-выражение. Об этом пользователь будет предупрежден. Так, если введено:

Рис.6 Ввод S-выражения с "хвостом"

То пользователь увидит в области ответа:

Рис.7 Интерпретатор игнорирует "хвост" S-выражения

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

Использование шаблонов стандартных конструкций Лиспа

Для облегчения ввода ряда стандартных конструкций Лиспа поддерживается аппарат шаблонов. Шаблон - это обычный текстовый файл в директории \Patt, имеющий расширение ".patt" В поставку включаются шаблоны конструкций COND, EXPR, FEXPR, MACRO, DEFUN, LAMBDA . Пользователь может добавить свои шаблоны (в частности, для функций дополительной библиотеки). Нажав кнопку 8 (см. описание командных кнопок) пользователь увидит следущее окно:

Рис.8 Выбор шаблона

Небходимо выбрать нужный шаблон и нажать кнопку OK (или дважды щелкнуть мышью по названию нужного шаблона). Шаблон выбранной конструкции вставится в область ввода с текущего положения курсора. При этом происходит автоматическое выравнивание вставляемого шаблона по положению курсора в области ввода. В случае выбора шаблона DEFUN при пустой области ввода, пользователь увидит:

Рис.9 Шаблон DEFUN, вставленный в область ввода

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

Рис.10 Задание имени функции и списка параметров

Очевидно, что тело функции должно представлять конструкцию COND. Установив курсор перед строкой "...Тело функци..." (как на рисунке) следует стереть надпись "...Тело функци..." и вставить описанным выше образом шаблон COND. Пользователь увидит следующее:

Рис.11 Вставка констукции COND на место тела функции

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

Рис.12 Окончательный вид функции ЧетНечет в области ввода

Пользователь должен установить курсор после последней скобки и нажать Enter. Функция будет обработана ядром интерпретатора. В области ответа пользователь должен увидеть:

Рис.13 Функция ЧетНечет успешно обработана интерпретатором

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

Рис.14 Функция работает

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

Просмотр списка объектов и ассоциативного списка

Список объектов содержит все атомы, присутствующие в сеансе работы. Атомы создаются автоматически по мере необходимости, а также в процессе инициализации ядра Лиспа. Каждый атом имеет список свойств, состоящий из индикаторов. Ряд индикаторов являются стандартными (встроенными). Это APVAL, FIXED, FLOAT, BITS, EXPR, FEXPR, MACRO, SUBR, FSUBR Другие индикаторы может задавать пользователь (см. ниже). Вся эта информация наглядно представляется в виде трехколоночного списка. В первой колонке стоят имена атомов, во второй - стандартные свойства, а в третьей - дополнительные свойства. Для вызова списка объектов следует нажать 9-ю кнопку тулбара (или выбрать соответствующий пункт меню). Пользователь увидит следующую картину:

Рис.15 Просмотр списка объектов.

Из рисунка видно, что у атома VER установлены два стандартных индикатора (APVAL и STRING). Индикатор APVAL означает, что атом является константой. Значением такой константы может быть либо другой атом, либо этот же атом. У атома VER значением является другой атом - строковая константа.
При необходимости список объектов можно отсортировать по именам атомов. Для этого нужно щелкнуть мышью по заголовку колонки "имя атома". Если щелкнуть по заголовку "стандартные свойства", то содержание списка будет отсортировано по второй колонке. Этими возможностями удобно пользоваться для поиска в списке нужной информации. У атомо-имен функций заданных пользователем будет установлен один из индикаторов EXPR, FEXPR или MACRO. У таких атомов можно увидеть определяющее выражение функции. Для этого достаточно щелкнуть по имени атома в первой колонке списка:

Рис.16 У атома с индикаторами EXPR, FEXPR или MACRO можно увидеть определяющее выражение.

Определяющее выражение функции можно увидеть, вызвав функцию (GETD Имя_Атома). Для просмотра свойств атомов существуют также соответствующие функции, описываемые ниже. Чтобы вернуться к вводу S-выражений следует повторно нажать 9-ю кнопку тулбара.
Значения, присваиваемые атомам с помощью конструкций SET/SETQ хранятся в ассоциативном списке. Для отображения ассоциативного списка служит 10-я кнопка тулбара. Ассоциативный список представляется в виде двухколоночной сеточной формы. В первой колонке находится имя атома, во второй - значение. Значением атома может быть произвольное S-выражение. Первоначально ассоциативный список пуст. Поэтому, при нажатии 10-й кнопки появится соответствующее сообщение. Однако, если пользователь введет команды:

(SETQ A1 123)

(SETQ A2 '(1 2 3))

(SETQ A3 "Проба")

(SETQ A4 '(A . B))

и затем нажмет 10-ю кнопку, то увидит следующую картину:

Рис.17 Просмотр ассоциативного списка.

Чтобы вернуться к вводу S-выражений следует повтороно нажать 10-ю кнопку тулбара.

Работа с дампами

Как уже было сказано выше, пользователь может запросить дамп вычисления. Дампы - это обычные текстовые файлы, имеющие расширение ".dmp". Чтобы запросить формирование дампа, следует нажать 7-ю кнопку тулбара. Появится диалоговое окно выбора файла. Пользователь должен задать имя нового или существующего файла. Новый файл будет создан, а в существующий информация будет дописываться в хвост файла. Если пользователь задал имя файла, оно будет отображаться в статус-баре. Теперь полный протокол вычисления всех S-выражений будет записываться в dmp-файл.
Проиллюстрируем все сказанное примером. В стандартную поставку входит рекурсивная функция fact, которая вычисляет факториал целого числа. Если запустить интерпретатор, включить режим дампирования, задать имя файла 444.dmp и вычислить S-выражение (fact 5), то в dump-файл попадет вся служебная информация о работе функции.

Рис.18 Вычисление 5! с дампированием.

Обратите внимание, что на вычисление ушло 4.5 сек, тогда как без дампирования это же S-выражение вычисляется 0,02 сек (т.е. более чем в 200 раз быстрее). Теперь можно посмотреть, какая информация попала дамп. Для просмотра дампа выполнения следует нажать 11-ю кнопку тулбара. Пользователю будет предложено выбрать дамп для просмотра. Если выбрать файл 444.dmp, то пользователь увидит:

Рис.19 Содержание дамп-файла

По дампу легко проследить все этапы вычисления. В частности, отчетливо видно, как меняется ассоциативный список и как обеспечивается рекурсия. Для возврата к вводу S-выражений следует повторно нажать 11-ю кнопку тулбара.

Общие сведения о входном языке HomeLisp

Алфавит HomeLisp включает латинские и русские буквы (заглавные и строчные), цифры и все спецзнаки, которые есть на стандартной клавиатуре. Имена атомов могут включать любые знаки (но числа должны начинаться с цифры и иметь "правильную" структуру). Так 1A - просто атом, 123 - число типа FIXED, 1e1 - число типа FLOAT (в научной нотации). Арифметика HomeLisp будет рассмотрена ниже.

Заглавные и строчные буквы в именах атомов не различаются: CAR, car и Car - одно и то же. (А занятно звучит эта фраза!) Автор обращает особое внимание на то, что русские буквы могут входить в имена атомов (правда, это, на взгляд автора, порождает больше проблем, чем удобства: приходится часто переключать регистры; кроме того нечаянное употребление букв, совпадающих по начертанию, порождает крайне неприятные ошибки). В строках, заключенных в двойные кавычки, строчные и заглавные буквы различаются: "Car" и "CAR" - разные атомы.

Программы записываются в текстовых файлах в свободном формате. Комментарием является все содержимое строки после символов "//".

Типы данных и особенности арифметики HomeLisp-а

В HomeLisp имеются данные следующих типов: FIXED, FLOAT, BITS и STRING.

Данные типа FIXED - это целые числа практически неограниченной разрядности. Описываемые ниже функции PLUS, DIFFERENCE, TIMES, QUOTIENT, REMAINDER позволяют вычислить, например, величину "100 факториал" или "2 в сотой степени".

Данные типа FLOAT соответствуют числам с плавающей точкой двойной точности (DOUBLE). Для работы с данными типа FLOAT можно использовать функции PLUS, DIFFERENCE, TIMES, ADD1, SUB1. Для деления чисел типа FLOAT следует использовать функцию DIVIDE (отсутствующую в реализации Лаврова и Силагадзе). Попытка использовать для деления чисел с плавающей точкой функции QUOTIENT и REMAINDER приведет к ошибке.

Данные типа BITS представляют собой 32-х битовые шкалы. Над ними можно выполнять битовые поразрядные операции LOGAND, LOGOR, LOGXOR, а также сдвигать в обе стороны, используюя функцию с двумя аргументами LEFTSHIFT. Второй аргумент задает количество сдвигов (число типа FIXED). Если оно отрицательно - сдвиг выполняется вправо.

Арифметика HomeLisp частично полиморфна. У функций PLUS, DIFFERENCE, TIMES допустимы аргументы типов FIXED и/или FLOAT. При вычислении происходит "поглощение" типом FLOAT типа FIXED. Так, при вычислении выражения:

(+ 555 1.0)

будет получен результат 556.0, имеющий тип FLOAT. Тогда как при вычислении выражения:

(+ 555 1)

будет получен результат 556, имеющий тип FIXED.

Функция EXPT (возведение в степень) возвращает результат типа FIXED только если оба ее аргумента имеют тип FIXED. В противном случае возвращается результат типа FLOAT. Дополнительно следует отметить, что попытка возведения отрицательного числа (любого типа) в вещественную степень (даже с нулевой дробной частью) вызовет ошибку. Тогда как возведение отрицательного числа в целую степень выполняется без проблем. Нуль в любой положительной степени равен нулю. Попытка возвести нуль в отрицательную степень вызовет ошибку. Возведение числа в нулевую степень дает единицу.

Функции инкремента и декремента ADD1, SUB1 возвращают результат того же типа, что и аргумент.


Описание дополнительных математических функций

Описание дополнительных математических функций HomeLisp

В этом разделе описывается скромная библиотека математических функций, встроенных в HomeLisp. Математические функции, относящиеся к "классическому" Лиспу описаны в соответствующем разделе документации. Основное назначение приводимой библиотеки - обеспечение графики и численных методов.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
ACS | 1 | FIXED/FLOAT | Вычисляет значение арккосинуса от значения аргумента. Если значение аргумента превышает по модулю единицу, выдается сообщение.
ASN | 1 | FIXED/FLOAT | Вычисляет значение арксинуса от значения аргумента. Если значение аргумента превышает по модулю единицу, выдается сообщение.
ATN | 1 | FIXED/FLOAT | Вычисляет значение арктангенса от значения аргумента.
CH | 1 | FIXED/FLOAT | Вычисляет значение гиперболического косинуса от значения аргумента
COS | 1 | FIXED/FLOAT | Возвращает косинус аргумента. Аргумент должен быть задан, естественно, в радианах
EXP | 1 | FIXED/FLOAT | Вычисляет значение экспоненты от значения аргумента.
LOG | 1 | FIXED/FLOAT | Вычисляет значение натурального логарифма (по основанию E) от значения аргумента. Если аргумент неположителен - выдается сообщение об ошибке.
RND | 1 | FIXED/FLOAT | Генерирует случайное число из дианазона [0,A], где A-значение аргумента. Если тип аргумента FIXED, то случайное число тоже будет иметь тип FIXED
ROUND | 2 | 1-FLOAT; 2-FIXED | Выполняет округление значения первого аргумента до количества десятичных знаков после точки, заданного значением второго аргумента.
SH | 1 | FIXED/FLOAT | Вычисляет значение гиперболического синуса от значения аргумента
SIGN | 1 | FIXED/FLOAT | Вычисляет знак значения аргумента. Тип результата совпадает с типом аргумента.
SIN | 1 | FIXED/FLOAT | Возвращает синус аргумента. Аргумент должен быть задан, естественно, в радианах
SQR | 1 | FIXED/FLOAT | Извлекает квадратный корень из значения аргумента. Если значение аргумента отрицательно - выдается сообщение об ошибке
ACS

Функция ACS принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение арккосинуса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Если значение аргумента превышает по модулю единицу, выдается сообщение, а результат устанавливается равным атому ERRSTATE Примеры:

(acs -1)

==>  3.14159265358979

(acs 1)

==>  0.0

(acs 0.5)

==>  1.0471975511966

(acs 1.5)

Модуль аргумента ACS превышает 1
==>  ERRSTATE

(* 4 5 (acs 3))

Модуль аргумента ACS превышает 1
==>  ERRSTATE

В последнем примере производится попытка вычислить арккосинус от 3. Это приводит к ошибке. Возникший в качестве промежуточного результата атом ERRSTATE "поглощает" результаты всех предыдущих вычислений. Конечный результат равен ERRSTATE.

ASN

Функция ASN принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение арксинуса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Если значение аргумента превышает по модулю единицу, выдается сообщение, а результат устанавливается равным атому ERRSTATE Примеры:

(asn 1)

==>  1.5707963267949

(asn -1)

==>  -1.5707963267949

(asn 0)

==>  0.0

(asn 1.5)

Модуль аргумента ASN превышает 1
==>  ERRSTATE

(* 5 6 (asn 6) 5)

Модуль аргумента ASN превышает 1
==>  ERRSTATE

В последнем примере производится попытка вычислить арксинус от 6. Это приводит к ошибке. Возникший в качестве промежуточного результата атом ERRSTATE "поглощает" результаты всех предыдущих вычислений. Конечный результат равен ERRSTATE.

ATN

Функция ATN принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение арктангенса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Примеры:

(atn 1)

==>  0.785398163397448

(asn -1)

==>  -0.785398163397448

(atn 0)

==>  0.0
CH

Функция CH принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение гиперболического косинуса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Используется прямое выражение гиперболического косинуса через экспоненту:

ch(x)=0.5(ex+e-x)

Если значение аргумента превышает 300, возникает переполнение. Примеры:

(ch 1)

==>  1.54308063481524

(ch -1)

==>  1.54308063481524

(ch 100)

==>  1.34405857090807E+43

(ch 400)

Переполнение при вычислении CH
==>  ERRSTATE

В приведенном выше примере хорошо видно, что гиперболический косинус (как и его тригонометрический брат) явлется четной функцией.

COS

Функция COS принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение косинуса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Аргумент должен выражаться в радианах. Примеры:

(cos 0)

==>  1.0

(cos _Pi)

==>  -1.0

(cos -_Pi)

==>  -1.0

(cos (* 0.5 _Pi))

==>  -3.49148336110938E-15

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

EXP

Функция EXP принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение экспоненты от значения аргумента. Тип значения, разумеется, всегда FLOAT. Аргумент не должен превышать 300. Примеры:

(exp 0)

==>  1.0

(exp 1)

==>  2.71828182845905

(exp -1)

==>  0.367879441171442

(exp 400)

Переполнение при вычислении EXP
==>  ERRSTATE

Следует обратить внимание на то, что в числе встроенных функций HomeLisp имеется функция EXPT, позволяющая вычислить Xy (при разумных значениях X и y). В принципе можно вычислить ex с использованием EXPT, введя (EXPT _E x). Функция EXP сделает это же действие быстрее.

LOG

Функция LOG принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение натурального логарифма от значения аргумента. Тип значения, разумеется, всегда FLOAT. Аргумент должен быть положительным числом. Примеры:

(log 1)

==>  0.0

(log 5)

==>  1.6094379124341

(log 50)

==>  3.91202300542815

(log _E)

==>  1.0

(log 0)

Попытка вычислить логарифм неположительного числа
==>  ERRSTATE

(log -100)

Попытка вычислить логарифм неположительного числа
==>  ERRSTATE

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

logaX = (logbX)/(logba)

В частности, для вычисления десятичного логарифма подойдет функция lg, определение которой вместе с примерами вызова приводится ниже.

(defun lg (x) (/ (log x) (log 10)))

==>  lg

(lg 1000)

==>  3.0

(lg 10)

==>  1.0

(lg 0.01)

==>  -2.0
RND

Функция RND принимает единственный аргумент числового типа (FIXED или FLOAT). Функция выдает случайное число из диапазона [0,X], где X - значение аргумента. Если аргумент имеет тип FIXED, то и результат будет иметь тип FIXED. Соответственно, при вызове функции с аргументом типа FLOAT, результат будет иметь тип FLOAT. Примеры:

(rnd 1)

==>  0

(rnd 1)

==>  1

(rnd 1)

==>  1

(rnd 1)

==>  0

(rnd 1.0)

==>  0.423560559749603

(rnd 1.0)

==>  0.441152453422546

(rnd 1.0)

==>  0.921030580997467

(rnd 1.0)

==>  0.320934772491455

(rnd 1.0)

==>  0.290106475353241

(rnd 2.0)

==>  1.62687706947327

(rnd 100.0)

==>  13.8400852680206

(rnd 100)

==>  92

Функция RND вызывает соответствующую функцию Visual Basic. Предварительной рандомизации не требуется - она выполняется автоматически при инициализации среды HomeLisp.

ROUND

Функция ROUND принимает два аргумента: первый должен иметь тип FLOAT, а второй - FIXED. Функция выполняет округление значения первого аргумента, оставляя количество десятичных знаков, заданное значением второго аргумента. Примеры:

(round 567.56 1)

==>  567.6

(round 567.56 0)

==>  568.0
SH

Функция SH принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение гиперболического синуса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Используется прямое выражение гиперболического синуса через экспоненту:

sh(x)=0.5(ex-e-x)

Если значение аргумента превышает 300, возникает переполнение. Примеры:

(sh 1)

==>  1.1752011936438

(sh -1)

==>  -1.1752011936438

(sh 100)

==>  1.34405857090807E+43

(sh 400)

Переполнение при вычислении SH
==>  ERRSTATE

В приведенном выше примере хорошо видно, что гиперболический синус (как и его тригонометрический брат) явлется нечетной функцией.

SIGN

Функция SIGN принимает один аргумент типа FIXED или FLOAT. Для положительных значений аргумента функция возвращает 1, для отрицательных -1, а для нулевых - 0. Тип результата совпадает с типом аргумента. Примеры:

(sign 5)

==>  1

(sign -5)

==>  -1

(sign 0)

==>  0

(sign 1.0)

==>  1.0

(sign -1.0)

==>  -1.0

(sign -0.0)

==>  0.0
SIN

Функция SIN принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение косинуса от значения аргумента. Тип значения, разумеется, всегда FLOAT. Аргумент должен выражаться в радианах. Примеры:

(sin 0)

==>  0.0

(sin (* 0.5 _pi))

==>  1.0

(sin _pi)

==>  1.22460635382238E-16

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

SQR

Функция SQR принимает единственный аргумент числового типа (FIXED или FLOAT). Она вычисляет значение квадратного корня от значения аргумента. Тип значения, разумеется, всегда FLOAT. Примеры:

(sqr 25)

==>  5.0

(sqr 625)

==>  25.0

(sqr 2)

==>  1.4142135623731

(sqr 3)

==>  1.73205080756888

(sqr -9)

Попытка извлечь квадратный корень из отрицательного числа
==> ERRSTATE

Следует обратить внимание на то, что в числе встроенных функций HomeLisp имеется функция EXPT, позволяющая вычислить Xy (при разумных значениях X и y). В принципе можно вычислить квадратный корень от x с использованием EXPT, введя (EXPT x 0.5). Функция SQR сделает это же действие быстрее.


HLisp - руководство пользователя

Прощения прошу, пока не готово...


Использование шаблонов стандартных конструкций Лиспа

Для облегчения ввода конструкций Лиспа поддерживается аппарат шаблонов. Шаблон - это обычный текстовый файл в директории \Patt, с именем "pattern.patt". Этот текстовый файл содержит шаблоны всех конструкций (функций) HomeLisp-a, описанных в этом руководстве. Функции сгруппированы по типам, а в пределах типа отсортированы по алфавиту. Пользователь HomeLisp-а может добавлять свои шаблоны или менять существующие. Вставку шаблонов можно использовать как при работе в среде разработки, так и при написании функций-обработчиков событий в экранном дизайнере.

О г л а в л е н и е
Вставка шаблонов функции при вводе S-выражений
Создание своих шаблонов функций
Вставка шаблонов функции при вводе S-выражений

Для вставки шаблона следует нажать кнопку 11 (для среды разработки) или кнопку 6 (для экранного дизайнера). В обоих случаях режим активизируется кнопкой с одной и той же иконкой:

Пользователю будет показана форма выбора шаблона:

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

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

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

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

Поскольку мы конструируем обычную функцию (типа EXPR), можно воспользоваться функцией DEFUN. Входим в режим Вставка шаблна, выбираем группу Функции классического ЛИСПа и находим функцию DEFUN (см. приведенный выше рисунок). Найдя функцию DEFUN, следует нажать кнопку OK или дважды щелкнуть мышью по названию нужного шаблона. Шаблон выбранной конструкции вставится в область ввода с текущего положения курсора. При этом происходит автоматическое выравнивание вставляемого шаблона по положению курсора в области ввода. В случае выбора шаблона DEFUN при пустой области ввода, пользователь увидит:

Назовем нашу функцию ЧетНечет. Это имя вносим вместо слова "Атом" в конструкции DEFUN на приведенном выше рисунке. Поскольку список параметров нашей функции состоит из единственного аргумента, меняем первое слово "Список" на (x). Тело функции должно быть написано нами. Очевидно, что оно должно представлять собой вызов COND. Убираем второе слово "Список" и оставляем курсор под списком (x):

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

Первое условие предложения COND будет фиксировать четное значение аргумента, а устовие "по умолчанию" будет фиксировать нечетное значение аргумента. Заготовки остальных условий просто стираются. Все это на Лиспе реализуется так, как показано на нижеследующем рисунке:

Пользователь должен установить курсор после последней скобки и нажать Enter. Функция будет обработана ядром интерпретатора. В области ответа пользователь должен увидеть:

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

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

Создание собственных шаблонов функций

Для добавления своих функций в файл "pattern.patt" нужно знать структуру этого файла. Структура проста: включаемые шаблоны группируются по типам функций (классический Лисп, работа с BLOB-ами, работа с файлами и т.д.).

Заголовок группы начинается двумя символами двоеточия. Остальная часть строки служит заголовком. Строки заголовка помещаются в выпадающий список групп функций. После заголовка группы в файл помещаются описания функций.

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

После заголовка описания идет шаблон вызова, который может занимать несколько строк. Далее идет описание следующей функции и т.д. Все это проще показать, чем описывать. Ниже приводится фрагмент файла "pattern.patt", описывающий функции работы с BLOB-ами.

:#############################################################
::Работа с BLOB-объектами
:#############################################################
:BIT2BLO Занести в BLOB битовую шкалу
(bit2blo  BLOB    // BLOB
          BITS    // битовая шкала
          FIXED   // с какой позиции заносить
          FIXED   // длина шкалы (1, 2, 3, 4 байтов)
)
:BLO2BIT Извлечь битовую шкалу из BLOBа
(blo2bit  BLOB    // BLOB
          FIXED   // с какой позиции извлекать
          FIXED   // длина шкалы (1, 2, 3, 4 байтов)
)
:BLO2FIX Извлечь из BLOBа число типа FIXED
(blo2fix    BLOB    // BLOB
            FIXED   // с какой позиции извлекать
            FIXED   // длина числа (1, 2, 3, 4 байтов)
)
:BLO2FLO Извлечь из BLOBа число типа FLOAT
(blo2flo    BLOB    // BLOB
            FIXED   // с какой позиции извлекать
            FIXED   // длина числа (4 или 8 байтов)
)

Из врезки видно, как устроен описываемый файл. Редактировать его можно любым редактором (кроме, естественно, Word-а!). Рекомендуется перед редактированием сделать резервную копию файла.


HomeLisp

HomeLisp - простой, свободно распространяемый Лисп на платформе Windows!

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

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

Лисп - абсолютно не попсовый, элитарный язык программирования. И не удивительно, что хотя с момента его изобретения Дж. МакКарти прошло уже более 50 лет, Лисп жив по сей день! Далеко не каждому языку суждена столь долгая жизнь, - где сейчас Алгол-60 (которому пророчили большое будущее)? А по Лиспу издаются книги (к сожалению, не на русском языке…); появляются новые и новые реализации Лиспа на разных аппаратных платформах.

Одну из таких простых реализаций Лиспа и предлагает автор этих строк.

HomeLisp (домашний Лисп) - это 32-х битная реализация Лиспа в среде Windows. За основу была взята реализация Лиспа, описанная в книге С.С. Лавров, Г.С. Силагадзе "Автоматическая обработка данных. Язык Лисп и его реализация" М. 1978

HomeLisp является свободно распространяемым программым обеспечением (FreeWare). Допускается свободное копирование как программного обеспечения, так и документации. Изменение программного кода компонентов HomeLisp не допускается. Цитирование документации допускается только с указанием автора. Авторские права на программный продукт и прилагаемую документацию принадлежат разработчику - Файфелю Борису Леонидовичу.

HomeLisp включает в себя следующие независимые компоненты:

Среду разработки (IDE), содержащую ядро языка, текстовый редактор, конструктор диалогов (экранный дизайнер), построитель EXE-файлов и скромные средства отладки;

COM-библиотеку, позволяющую вызывать Лисп из любой программной среды, поддерживающей COM-автоматизацию (например, из Microsoft Excel), а также два скриптовых "движка", позволяющих писать скрипты на языке Лисп;

WEB-компоненту для работы на WEB-сервере IIS, позволяющую построить учебный класс для изучения Лиспа (при этом WEB-компонента инсталлируется только на сервере).

HomeLisp обрабатывает следующие типы данных:

целые числа практически неограниченной разрядности;
стандартные числа с плавающей точкой;
битовые шкалы;
строки.
К числу объектов, встроенных в ядро HomeLisp относятся:

COM-объекты, позволяющие вызывать из среды Лиспа объекты, поддерживающие COM-автоматизацию;
Файлы и BLOB-объекты (большие двоичные объекты), позволяющие обрабатывать произвольную двоичную информацию;
Графические окна , позволяющие использовать простую графику;
Диалоги, позволяющие строить программы с графическим пользовательским интерфейсом;

HomeLisp снабжен подробной документацией, содержащей не только описание всех функций, описание языка "с нуля", а также приемов программирования на Лиспе (что может оказаться полезным для тех, кто впервые знакомится с языком). Имеется список литературы на руccком языке, включающий и ряд Интернет-ресурсов, посвященных Лиспу.
Скачать HomeLisp


Cписок литературы

Список литературы

Приводимый далее список печатных изданий и Интернет-ресурсов по Лиспу не претендует на полноту. Предпочтение отдается материалам на русском языке.

1) С.С. Лавров, Г.С. Силагадзе "Автоматическая обработка данных. Язык Лисп и его реализация" М. 1978 - замечательная книга, содержание которой и подвигло автора на разработку HomeLisp. К достоинствам этой книги (четкости изложения и его методической продуманности) можно также отнести то, что Лисп-программы, приведенные в книге, не содержат опечаток!

2) Д. Баррон "Рекурсивные методы в программировании" М. 1974 - достаточно элементарное введение в рекурсивное программирование. Лисп в книге только упоминается, поскольку книга ориентирована не на конкретный язык, а на методологию.

3) Дж. Фостер "Обработка списков" М. 1974 - книга посвящена обработке списков в разных языках программирования. Есть небольшой обзор возможностей Лиспа.

4) Т. Пратт "Языки программирования: разработка и реализация" М. 1979 - серьезная монография, которая содержит главу о Лиспе. В ней не только объясняются конструкции языка, но и описываются детали типичной реализации.

5) У. Маурер "Введение в программирование на языке Лисп" М. 1976 - очень краткое пособие, к тому же с большим числом опечаток. Тем не менее, по имеющимся у автора сведениям, это - первая книга целиком посвященная Лиспу на русском языке.

6) Э.Хювенен, Й.Сеппянен "Мир Лиспа" т.1 и т.2 М. 1990 - отличный двухтомник, давно ставший библиографической редкостью. Его можно с полным правом назвать "Энциклопедией Лиспа".

7) Л.В. Городняя "Основы функционального программирования" Новосибирск 2004 - современный курс функционального программирования, в котором изложение ведется применительно к Лиспу. По имеющимся сведениям, Л.В. Городняя (вместе с С.С. Лавровым) является пионером Российского Лиспа.

8) Н.Н. Дацун, А.П. Хован "Интернет-учебный курс для обучения языку Лисп" в материалах конф. "Вирт-2001" - Этот материал подвигнул автора на разработку Web-компоненты HomeLisp.

9) М.Ю. Семенов. "Язык ЛИСП для персональных ЭВМ" - М. 1989, Издательство МГУ - элементарное учебное пособие по Лиспу.

10) И.В. Тужилов "Язык программирования XLISP" - Пенза 1994. - описывается работа с бесплатной системой XLISP, являющейся объектным расширением стандартного Лиспа. XLISP - очень эффективная, производительная реализация Лиспа с целым рядом дополнительных возможностей.

11) А.П. Крюков, А.Я. Родионов, А.Ю. Таранов, М.Е. Шаблыгин "Программирование на языке R-Лисп" М. "Радио и связь", 1991 - Приведено описание стандартного Лиспа, а также описание алгебраического языка R-Лисп, основанного на стандартном Лиспе. Изложение ведется применительно к популярной в те годы реализации Лиспа на мэйнфрейме IBM-370 (ЕС ЭВМ). По имеющимся сведениям, это последняя книга по Лиспу, изданная в СССР...

Интеренет-ресурсы, посвященные Лиспу

12) Интернет-университет информационных технологий - Ценнейший образовательный портал, содержащий, в числе прочего, много материалов по функциональному программированию (в частности - 12 лекций по Лиспу). Материалы очень хорошо оформлены и снабжены библиографическими ссылками.

13) Курс лекций М.Н. Морозова (Марийский гос. тех. университет) - Очень наглядное, живое, с большим количеством иллюстраций изложение основ Лиспа.

14) Сайт информационных технологий Курганского университета - Объемный курс лекций по Лиспу. Лекции очень хорошо организованы с чисто методической точки зрения - изложение разбивается на отдельные "шаги", что позволяет при непонимании вернуться к нужному шагу, а затем продолжить с прерванного места.

15) Цикл статей по Лиспу В.Водолазкого - Очень доступное изложение основ Лиспа (применительно к GNU Common-Лиспу). Автор этих статей - признанный компьютерный писатель.

16) SoftCraft - разноликое программирование (И.А. Дехтяренко "Введение в Лисп") - Интересный сайт, в содержании которого философия программирования удачно уживается с конкретными знаниями.

17) lisper.ru - Сайт, посвященный Common Lisp, написанный на Common Lisp и акцентирующий внимание на практическом применении Common Lisp. На этом же сайте располагается перевод книги П.Зибеля "Практрический Common Lisp" на русский язык, выполняемый энтузиастами.

18) lisp.ru - Центральный русскоязычный ресурс, посвященный Лиспу.

19) Лисп у Истоков - Еще один русскоязычный ресурс, посвященный Лиспу (главным образом - системе LispWorks).

Разное

20) И.Болдырев "Переменные в Common Lisp" - Полезная "идеологическая" статья о переменных в Common Lisp (не лишенная, впрочем, методических недостатков).

21) А.Г.Фенстер "Замыкания в Common Lisp" - Очень четкое и сжатое изложение понятия замыкания.


Описание функций работы с реестром

Описание функций работы с реестром Windows

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

Информация в реестре хранится в иерархическом виде - она включает разделы (ключи) и параметры. Проводя аналогию с файловой системой, можно сказать, что разделы - это папки, а параметры - это файлы. На самом верхнем уровне иерархии находятся корневые разделы: HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_CONFIG, а в системах Win-9x/Me еще и HKEY_DYN_DATA. За каждым из корневых разделов реестра закрепляется битовая константа в соответствии со следующей таблицей:

Имя корневого раздела | Битовая константа-обозначение
HKEY_CLASSES_ROOT | &H80000000
HKEY_CURRENT_USER | &H80000001
HKEY_LOCAL_MACHINE | &H80000002
HKEY_USERS | &H80000003
HKEY_CURRENT_CONFIG | &H80000005
HKEY_DYN_DATA | &H80000006

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

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

Битовая константа | Права доступа
KEY_ALL_ACCESS = &HF003F | Полный доступ
KEY_CREATE_SUB_KEY = &H4 | Разрешено создавать подразделы
KEY_ENUMERATE_SUB_KEYS = &H8 | Разрешено перечисление подразделов
KEY_READ = &H20019 | Общее разрешение операций чтения
KEY_QUERY_VALUE = &H1 | Разрешено запрашивать данные подразделов
KEY_SET_VALUE = &H2 | Разрешено устанавливать данные подразделов
KEY_WRITE = &H20006 | Общее разрешение операций записи

Функция REGENUMKEY позволяет перечислить подразделы ("папки") открытого раздела. Функция REGENUMVALUE позволяет перечислить параметры ("файлы") открытого раздела. Перечисление означает получение имени подраздела (параметра) по его номеру. Объекты нумеруются с нуля. Нет возможности узнать сколько подразделов (параметров) содержит раздел. Поэтому единственным способом перечисления всех разделов (параметров) является итерационный цикл с получением значений объектов с номерами 0, 1, 2 и т.д., до тех пор, пока функция не вернет код ошибки.

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

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

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

Системный реестр легко испортить, что может серьезно нарушить работоспособность Windows. Автор не возлагает на себя ответственность за возможную порчу реестра пользователями HomeLisp, и усиленно рекомендует сделать предварительную копию реестра - запустить regedit.exe и далее в меню: Реестр -> Экспорт файла реестра (нажать радиокнопку "весь реестр"); задать имя файла и директорию; нажать "OK".

Впрочем, настоящие программисты ничего не боятся...

Имена всех функций, обслуживающих реестр, для удобства начинаются с префикса REG. Функции работы с реестром принадлежат к классу SUBR - их аргументы вычисляются. Функции доступны при работе в среде разработки, в составе EXE-файла, а также из библиотеки HomeLispLib.Exe. При работе WEB-компоненты функции недоступны.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
REGCLOSEKEY | 1 | FIXED | Закрывает манипулятор раздела
REGCREATEKEY | 4 | 1-BITS, 2-STRING, 3-FIXED, 4-BITS | Открывает существующий ключ(раздел) или создает новый.
REGDELETEKEY | 2 | 1-BITS, 2-STRING | Удаляет раздел вместе со всеми подразделами и параметрами.
REGDELETEVALUE | 2 | 1-FIXED, 2-STRING | Удаляет параметр из предварительно открытого раздела
REGENUMKEY | 2 | FIXED | Дает значение ключа из предварительно открытого раздела
REGENUMVALUE | 2 | FIXED | Дает значение параметра из предварительно открытого раздела
REGOPENKEY | 3 | 1-BITS, 2-STRING, 3-BITS | Открывает существующий раздел
REGQUERYVALUE | 2 | 1-FIXED, 2-STRING | Получение значения параметра
REGSETVALUE | 2 | 1-FIXED, 2-STRING, 3-FIXED, 4-ANY | Установление значения параметра
REGCLOSEKEY

Функция REGCLOSEKEY закрывает открытый ранее раздел реестра. Функция требует единственный параметр типа FIXED - манипулятор открытого раздела. При успешном завершении функция возвращает T, в противном случае возвращается список из двух целых: -1 и целочисленный код ошибки. Вот примеры закрытия разделов:

(setq h (car (regCreateKey HKEY_CLASSES_ROOT "HomeLisp" 0 KEY_ALL_ACCESS)))

==> -1030057424

(regCloseKey h)

==> T

(regCloseKey -999)

==> (-1 1114)

Здесь создается и открывается раздел HKEY_CLASSES_ROOT\HomeLisp, и сразу же закрывается. Попытка закрыть несуществующий манипулятор раздела вызывает ошибку с кодом 1114.

REGCREATEKEY

Функция REGCREATEKEY позволяет создать несуществующий раздел реестра и открыть его. Если раздел уже существует, то функция открывает его.

Первым параметром функции должна быть битовая константа, идентифицирующая одну из корневых ветвей реестра. Вторым параметром должна быть строка - имя раздела. Имя раздела может содержать разделить уровней - обратный слэш. В этом случае функция создаст необходимую цепочку вложенных разделов. Третьим параметром функция требует целое число (FIXED), задающее статус создаваемого ключа. Если задан нуль, то ключ будет сохранен в реестре, если задана единица - то ключ сохранится только до перезагрузки Windows. Последний четвертый параметр задает права доступа к заданному манипулятору. Права доступа - это одна из битовых констант.

Функция всегда возвращает список из двух целых. Признаком успешного завершения является отличие головы списка от -1. В этом случае голова списка является манипулятором открытого раздела. Второй элемент списка может примать значения 1 или 2. Единица означает, что требуемый раздел создан в процессе вызова. Двойка означает, что раздел существовал ранее.

(regCreateKey &H12 "HomeLisp" 0 KEY_ALL_ACCESS)

==> (-1 1010)

(regCreateKey HKEY_CLASSES_ROOT "HomeLisp" 0 KEY_ALL_ACCESS)

==> (-1029447712 2)

(regCloseKey -1029447712)

==> T

(regCreateKey HKEY_CLASSES_ROOT "HomeLisp" 0 KEY_ALL_ACCESS)

==> (-1029562304 1)

(regCreateKey &H666 "HomeLisp" 0 KEY_ALL_ACCESS)

==> (-1 1010)

(regCloseKey -1029562304)

==> T

Здесь создается раздел HKEY_CLASSES_ROOT\HomeLisp. При первом вызове второй элемент списка равен 1, что говорит о том, что раздел до вызова не существовал. При втором вызове второй элемент списка равен 2, что свидетельствует о существовании раздела до вызова. Попытка создать раздел в несуществующем корне (&H666) завершается ошибкой с кодом 1010.

REGDELETEKEY

Функция REGDELETEKEY позволяет удалить раздел(ключ) реестра со всеми его подразделами. Функция примает два параметра: обозначение корневого раздела (BITS) и полного "пути" к разделу (STRING). Разделителем уровней является обратный слэш. Важно отметить, что открывать раздел не нужно.

Функция при успешном завершении возвращает T, а в случае ошибки - список из двух элементов. Первый равен -1, второй представляет собой код завершения.

Вот пример удаления раздела реестра.

(regCreateKey HKEY_CLASSES_ROOT "HomeLisp" 0 KEY_ALL_ACCESS)

==> (-1029599008 1)

(regCloseKey -1029599008)

==> T

(regDeleteKey HKEY_CLASSES_ROOT "HomeLisp")

==> T

(regDeleteKey HKEY_CLASSES_ROOT "HomeLisp")

==> (-1 2)

Здесь создается раздел HKEY_CLASSES_ROOT\HomeLisp, после чего его манипулятор закрывается. Затем вызовом RegDeleteKey раздел удаляется. Заглянув в реестр с помощью regedit.exe можно убедиться в том, что раздел удален. Последующая попытка удалить несуществующий раздел вызывает ошибку.

REGDELETEVALUE

Функция RegDeleteValue позволяет удалить параметр из предварительно открытого раздела. Функция принимает два параметра: манипулятор раздела (FIXED) и имя параметра (STRING). При успехе функция возвращает T, а при ошибке - список из двух элементов: первый равен -1, второй равен коду завершения.

Вот пример вызова функции RegDeleteValue:

(setq h (car (regCreateKey HKEY_CLASSES_ROOT "HomeLisp" 0 KEY_ALL_ACCESS)))

==> -1029535696

(regSetValue h "bob" REG_SZ "test")

==> T

(regDeleteValue h "bob")

==> T

(regDeleteValue h "bob")

==> (-1 2)

Здесь создается ключ "HomeLisp", под этим ключом создается параметр "bob" и ему присваивается текстовое значение. После этого параметр удаляется. Удаление завершение успешно. А попытка удалить несуществующий параметр вызывает ошибку с кодом завершения 2.

REGENUMKEY

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

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

Вот пример перечисления подразделов:

(setq h (car (regCreateKey
                    HKEY_LOCAL_MACHINE
                    "Software\Microsoft\Windows\CurrentVersion"
                    0
                    KEY_ALL_ACCESS)))

==> -1029527856

(regEnumKey h 0)

==> (0 "Setup" "")

(regEnumKey h 1)

==> (0 "RunOnce" "")

(regEnumKey h 1000)

==> (-1 259)

Можно убедиться, что раздел HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion содержит не менее двух подразделов, но не более тысячи. А точное перечисление подразделов с выводом имени каждого подраздела и общего количества приводится ниже:

(defun EnumKeys  (Branch Path)

       (prog (i sk h s)

             (setq s (regOpenKey Branch Path KEY_ALL_ACCESS))

             (cond ((eq (car s) -1) (return s)))

             (setq h (cadr s))

             (setq i 0)

       @1    (setq sk (regEnumKey h i))

             (cond ((eq (car sk) -1) (go @2)))

             (print i) (prints " ") (printline (cadr sk))

             (setq i (+ i 1))

             (go @1)

       @2    (regCloseKey h) (return i)

       )
)

 ==> EnumKeys

(EnumKeys HKEY_LOCAL_MACHINE "Software\Microsoft\Windows")

0 "CurrentVersion"
1 "ITStorage"
2 "Help"
3 "HTML Help"

==> 4

(EnumKeys HKEY_LOCAL_MACHINE "Software\Microsoft\Window")

==> (-1 2)

(EnumKeys &H777 "Software\Microsoft\Windows")

==> (-1 1114)

Следует обратить внимание на то, что при обращении к несуществующему разделу ("Software\Microsoft\Window") функция возвращает код завершения 2. А при обращению к несуществующей ветви (&H777) код завершения равен 1114.

REGENUMVALUE

Функция REGENUMVALUE позволяет перечислить параметры предварительно открытого ключа. Функция требует два параметра: манипулятор раздела (FIXED) и номер параметра (FIXED). Параметры нумеруются с нуля.

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

Тип параметра задается константой типа FIXED из нижеследующей таблицы:

Символическая константа | Значение | Описание типа
0 | REG_NONE | (?)
1 | REG_SZ | Строка символов, закрытая нулем.
2 | REG_EXPAND_SZ | Расширенный строковый параметр (?)
3 | REG_BINARY | Последовательность байтов (в т.ч. и нулей).
4 | REG_DWORD | Двойное слова с Интеловским порядком байтов: младшие байты слева.
5 | REG_DWORD_BIG_ENDIAN | Двойное слова с неинтеловским порядком байтов: младшие байты справа.
6 | REG_LINK | Ссылка (?)
7 | REG_MULTI_SZ | Последовательность строк, закрытых нулями. Признак конца агрегата - два нуля, стоящих подряд.
8 | REG_RESOURCE_LIST | (?)

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

Вот пример перечисления параметров раздела реестра:

(defun EnumValues  (Branch Path)

       (prog (i sk h s)

             (setq s (regOpenKey Branch Path KEY_ALL_ACCESS))

             (cond ((eq (car s) -1) (return s)))

             (setq h (cadr s))

             (setq i 0)

       @1    (setq sk (regEnumValue h i))

             (cond ((eq (car sk) -1) (go @2)))

             (print i) (prints " ") (printline (cdr sk))

             (setq i (+ i 1))

             (go @1)

       @2    (regCloseKey h) (return i)

       )
)

 ==> EnumValues

(EnumValues HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Run")

0 ("FLMOFFICE4DMOUSE" 1 "C:\Program Files\Browser MOUSE\mouse32a.exe")
1 ("internat.exe" 1 "internat.exe")
2 ("ScanRegistry" 1 "C:\WINDOWS\scanregw.exe /autorun")
3 ("TaskMonitor" 1 "C:\WINDOWS\taskmon.exe")
4 ("SystemTray" 1 "SysTray.Exe")
5 ("mdac_runonce" 1 "C:\WINDOWS\SYSTEM\runonce.exe")
6 ("Machine Debug Manager" 1 "C:\WINDOWS\SYSTEM\MDM.EXE")
7 ("MyDirScan" 1 "C:\!AVSCAN\DIRSCAN.EXE -T")
8 ("PWSTray" 1 "PwsTray.exe")
9 ("Outpost Firewall" 1 "C:\PROGRAM FILES\AGNITUM\OUTPOST FIREWALL 1.0\outpost.exe
   /waitservice")

==> 10

Здесь видно, что все параметры раздела автозапуска имеют тип REG_SZ.

REGOPENKEY

Функция REGOPENKEY открывает существующий раздел реестра. Функция требует три параметра: ветвь реестра (BITS), путь к нужному разделу (STRING) и права доступа (BITS).

При успешном завершении функция возвращает список из двух элементов: нуля (признак успеха) и манипулятора открытого раздела.

При ошибке функция также возвращает список из двух элементов: -1 (признак ошибки) и расширенного кода ошибки.

Вот три примера вызова REGOPENKEY:

(regOpenKey HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Run" KEY_ALL_ACCESS)

==> (0 -1029700800)

(regOpenKey HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Ru" KEY_ALL_ACCESS)

==> (-1 2)

(regOpenKey &H888 "Software\Microsoft\Windows\CurrentVersion\Run" KEY_ALL_ACCESS)

==> (-1 1010)

Первый пример демонстрирует удачное открытие. Два последующих - неудачные, но по разным причинам. Во втором случае задан несуществующий раздел "Software\Microsoft\Windows\CurrentVersion\Ru", что порождает код завершения 2. В третьем случае задан код несуществующей ветви &H888, что дает код завершения 1010.

REGQUERYVALUE

Функция REGQUERYVALUE позволяет получить значение параметра заранее открытого раздела по имени параметра. Функция требует два аргумента: манипулятор открытого раздела (FIXED) и имя запрашиваемого параметра (STRING).

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

Типы значений хранящихся в реестре приведены выше при описании функции REGENUMVALUE.

(setq h (cadr (regOpenKey HKEY_CLASSES_ROOT "homelisp" KEY_ALL_ACCESS)))

 ==> -1029765920

(regQueryValue h "Bob")

 ==> (0 1 "1954")

(regQueryValue h "buska")

 ==> (0 3 11 22 33 44 55 66)

(regQueryValue h "plushka")

==> (0 7 "bbb" "ccc" "ddd" "eee")

(regQueryValue h "zzz")

==> (-1 2)

В последнем случае запрос несуществующего параметра вызывает завершение с кодом 2.

REGSETVALUE

Функция REGSETVALUE позволяет создать параметр у предварительно открытого раздела и привоить ему значение. Функция требует четыре обязательных аргумента. Первый, как обычно, задает манипулятор раздела (FIXED). Второй (типа STRING) задает имя параметра. Третий (типа FIXED) задает тип параметра. Четвертый параметр задает устанавливаемое значение. В зависимости от третьего параметра, четвертый пераметр может быть атомом или списком.

При успешном завершении функция возвращает T.

Вот примеры вызова функции REGSETVALUE:

(regSetValue h "leo" REG_BINARY '(0 1 2 3 4))

==> T

(regQueryValue h "leo")

==> (0 3 0 1 2 3 4)

(regSetValue h "pushok" REG_SZ "Пушок")

==> T

(regQueryValue h "pushok")

==> (0 1 "Пушок")

(regSetValue h "a1" -9 "q")

REGSETVALUE: Третий аргумент имеет недопустимое значение
==> ERRSTATE

В последнем случае задан неверный тип параметра (-9).

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


Приемы программирования и избранные задачи
Введение

Предлагаемый в этом разделе материал можно условно разделить на две части.

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

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

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

Некоторые примеры рекурсивных программ
Построение списка целых

Наверное, одной из первых задач, которую предлагают студенту или школьнику при изучении Паскаля, является задача заполнения массива последовательными целыми числами от единицы до заданного N. Напомним решение этой задачи:

- заводим массив M;

- заводим целую переменную i;

- в цикле по i от единицы до N i-му элементу массива величину i (M[i]:=i).

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

Будем рассуждать так:

- Очевидно, что если N=0, то результирующий список должен быть пустым списком (список из нуля элементов);

- Пусть у нас уже построен список из N-1-го числа (обозначим его Ln-1). Тогда очевидно, что список из N чисел получается из списка Ln-1 простым добавлением в хвост числа N: Ln = Ln-1 + ( N ).

Как можно убедиться, приведенные выше рассуждения в точности повторяют выкладки метода математической индукции. Теперь осталось воплотить эти выкладки в программу на Лиспе. Объединение списков выполняет функция append, а построить список из одного элемента можно с помощью функции list. Собирая все вместе, получаем:

(defun alist (n) (cond ((= n 0) Nil)
                        (T (append (alist (- N 1)) (list N)))
                 )
)

==> alist

(alist 100)

==> (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
     23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
     42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
     61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
     80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
     99 100)

Функция вполне работоспособна. Теперь немного усложним задачу. Предположим, что требуется построить список не всех чисел от единицы до заданного N, а список всех чисел из интервала N1 - N2. Повторим наши рассуждения, применительно к новой задаче:

- Очевидно, что если N1 = N2, то результирующий список должен быть списком из одного элемента (неважно, какого N1 или N2);

- Пусть у нас уже построен список чисел из диапазона N1 - (N2-1). Тогда очевидно, что искомый список получается из последнего просто добавлением в хвост числа N2.

Этот подход реализуется на Лиспе вполне очевидным образом:

(defun clist (n1 n2) (cond ((= n1 n2) (list n1))
                        (T (append (clist N1 (- N2 1)) (list N2)))
                 )
)

==> clist

(clist 5 55)

==> (5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
      27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
      47 48 49 50 51 52 53 54 55)

Решение получилось изящным и лаконичным.

Чередование элементов двух списков ("два гребня")

Пусть даны два произвольных списка. Обозначим элементы этих списков Ai и Bi соответственно. Длины списков могут различаться. Требуется построить новый список в котором элементы исходных списков чередовались бы следующим образом: A1 , B1 , A2 , B2 ... и т.д. до исчерпания более короткого списка. Допуская вольность, можно представить себе результат, как два два гребня, вставленных один в другой.

Рассмотрим лисповское (функциональное) решение этой задачи. Назовем функцию, которая будет строить список-результат grb. У этой функции будет два параметра (исходные списки); обозначим их a и b. Представляется очевидным, что результирующий список будет совпадать со списком a, если список b пуст. Соответственно, если пуст список a, то результат будет совпадать со списком b. Если же оба исходных списка не пусты, то результат можно представить как объединение следующих двух списков:

- двухэлементного списка, состоящего из первых элементов списков a и b;

- результата применения функции grb к хвостам списков a и b.

В Лиспе это выглядит следующим образом:

(defun grb (a b) (cond ((null a) b)
                       ((null b) a)
                       (T (append (list (car a) (car b)) (grb (cdr a) (cdr b))))
                 )
)

==> grb

(grb '(1 2 3 4 5 6) '(a b c d e f g h))

==> (1 a 2 b 3 c 4 d 5 e 6 f g h)

Определяющее выражение функции получается дословным переводом на Лисп приведенных выше математических рассуждений (что является большим преимуществом рекурсивного функционального подхода).

Получение n-го элемента списка

Встроенные в Лисп функции CAR и CDR позволяют "разобрать" произвольный список на составные части. Однако, в стандартный набор функций Лиспа не входит функция, позволяющая получить элемент списка по его номеру. Попробуем заполнить этот пробел.

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

Воплощаем все сказанное в Лиспе:

(defun n-th (x n) (cond ((null x) (raiseError "n-th: список пуст(исчерпан)"))
                        ((= n 1) (car x))
                        (T (n-th (cdr x) (- n 1)))
                  )
)

==> n-th

(n-th '(1 2 3) 5)

n-th: список пуст(исчерпан)
==> ERRSTATE

(n-th '(q w e r t y) 4)

==> r

(n-th '(q w e r t y) -4)

n-th: список пуст(исчерпан)
==> ERRSTATE

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

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

Тем не менее, чтобы функция была более дружественной, вероятно стоит добавить в COND-конструкцию ветвь для анализа положительности аргумента:

(defun n-th (x n) (cond ((<= n 0) (raiseError "n-th: аргумент неположителен"))
                        ((null x) (raiseError "n-th: список пуст(исчерпан)"))
                        ((= n 1) (car x))
                        (T (n-th (cdr x) (- n 1)))
                  )
)

==> n-th

(n-th '(q w e r t y) 4)

==> r

(n-th '(q w e r t y) -4)

n-th: аргумент неположителен
==> ERRSTATE

Теперь наша функция приобрела необходимую завершенность.

Арифметика неограниченной разрядности

Действия с целыми числами высокой разрядности всегда доставляли определенные трудности. Дело в том, что стандартные целые, которые обрабатывает компьютер, имеют достаточно большую, но все же ограниченную разрядность. Так, для популярных интеловских 32-х битных процессоров максимальная величина целого составляет 231 = 2 147 483 648. Если попробовать вычислить, например, величину 50!=1*2*3*...*49*50, то переполнение неминуемо.

Хорошим тоном при реализации Лиспа считается обеспечение арифметики неограниченной разрядности. Долгое время (примерно, до середины 90-х годов прошлого века) только Лисп и позволял вычислить, например, все значащие цифры 100!. Сейчас с появлением языков новых поколений (таких, как Haskell или Python) ситуация изменилась. Тем не менее, арифметика неограниченной разрядности остается привлекательной строной Лиспа. В данном разделе будет разобрано две задачи, в которых арифметика высокой разрядности играет главную роль.

Проблема 3n+1

Эта математическая проблема, суть которой сообщил автору П.Л. Бахрах, состоит в следующем:

Берется произвольное целое число N. Из него получается число M по следующему алгоритму:

Если N - четное, то M полагается равным N\2;

Если N - нечетное, то M полагается равным 3*N+1;

Далее полагается N = M и процесс повторяется. При этом оказывается, что какое бы исходное число N ни было бы взято, процесс рано или поздно сойдется к единице. Строгого математического доказательства этого факта (на момент написания этих строк) пока не найдено. Удивительно здесь то, что число или уменьшается вдвое или увеличивается втрое. Казалось бы, увеличение должно "перевесить", и число должно бесконечно возрастать. Но этого не происходит!

Вот простая Лисповская программа, реализующая приведенный выше алгоритм:

(defun 3n+1 (n) (prog nil
                      (printline n)
                      (cond ((= n 1) (return 'OK))
                            ((= (% n 2) 0) (return (3n+1 (\ n 2))))
                            (T (return (3n+1 (+ (* n 3) 1))))
                      )
                )
)

==> 3n+1

Функция снова получена простым переводом на Лисп алгоритма "3n+1". Единственное отличие заключается в том, что тело функции оформлена как PROG-конструкция. При входе в функцию сначала выполняется печать значения аргумента. Это позволяет проследить, какие числа получаются в процессе работы программы.

А вот и результат вызова:

(3n+1 10000)

10000
5000
2500
1250
625
1876
938
469
1408
704
352
176
88
44
22
11
34
17
52
26
13
40
20
10
5
16
8
4
2
1

==> OK

А если не требуется видеть все промежуточные результаты, а хочется получить только количество циклов до сходимости?.. В этом случае функция будет выглядеть еще проще:

(defun 3n+1 (n k) (cond ((= n 1) k)
                        ((= (% n 2) 0) (3n+1 (\ n 2) (+ k 1)))
                        (T  (3n+1 (+ (* n 3) 1) (+ k 1)))
                  )
)

==> 3n+1

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

Единственным неудобством новой редакции функции 3n+1 является то, что вызывать ее нужно с двумя параметрами - исследуемым числом и нулем:

(3n+1 10000 0)

==> 29

(3n+1 (fact 50) 0)

==> 1295

Впрочем, это неудобство легко преодолеть: достаточно погрузить функцию 3n+1 в функцию-облочку:

(defun *3n+1 (n) (3n+1 n 0))

==> *3n+1

(*3n+1 10000)

==> 29

Накапливающий параметр теперь скрыт в оболочке.

Численный перебор

В книге И. Сафронова "Visual Basic в задачах и примерах" Санкт-Петербург, 2006. автор этих строк нашел замечательную задачу. Вот ее формулировка:

Сколько существует натуральных чисел n, меньших 1000, для которых 2n-n делится на 7?

Решение ее кажется простым: завести цикл по i от единицы до 1000 и для каждого i проверить остаток от деления 2i-i на 7. Да вот незадача: Visual Basic не позволит вычислить 2i при i большем 31... А в Лиспе такой проблемы нет! Вот простая программа решения задачи И. Сафронова "в лоб":

(prog (i n)
      (setq n 0)
      (setq i 1)
   @1 (cond ((= (% (- (^ 2 i) i) 7) 0) (setq n (+ 1 n))))
      (setq i (+ 1 i))
      (cond ((<= i 1000) (go @1)))
      (return n)
)

==> 142

PROG-конструкция использует две локальные переменные i и n. Для очередного i вычисляется остаток от деления на 7 величины 2i-i. Если остаток оказывается нулевым, значение переменной n увеличивается на единицу. Далее величина i увеличивается на единицу и проверяется, не превысила ли величина i граничного значения (1000). Если нет - вычисления повторяются. Когда i получает значение, большее 1000, цикл разрывается и оператор return возвращает количество чисел, удовлетворяющих заданному условию.

Используя макро for этот же алгоритм можно реализовать короче:

(prog (i n)
      (setq n 0)
      (for i 1 1000
         ( (cond ((= (% (- (^ 2 i) i) 7) 0) (setq n (+ 1 n)))) )
      )
      (return n)
)

==> 142

Обе программы дают верный результат, но работают ощутимо долго - около минуты. Производительность программы может быть повышена почти в 10 раз, если оптимизировать приведенный код. Легко видеть, что "узким местом" является вычисление степени двойки. Предположим, на j-м витке цикла вычислена величина 2j. На следующем витке будет вычислятся величина 2j+1 с помощью встроенной функции EXPT(^), хотя для вычисления 2j+1 достаточно умножить уже вычисленную ранее величину 2j на 2! Если не перевычислять степень двойки на каждом витке, а завести рабочую переменную, значение которой просто умножать на 2, тот производительность программы возрастет более, чем в 9 раз:

(prog (i n m)
      (setq n 0)
      (setq m 2)
      (for i 1 1000
         ( (cond ((= (% (- m i) 7) 0) (setq n (+ 1 n)))) (setq m (* 2 m)) )
      )
      (return n)
)

==> 142

Результат получается не за минуту, а за 7.5 сек! А если мы хотим не просто подсчитать, а увидеть числа n, для которых 2n-n делится на 7 без остатка? Нет проблем:

(prog (i n m)
      (setq n 0)
      (setq m 2)
      (for i 1 1000
         ( (cond ((= (% (- m i) 7) 0) (prog nil (printline i) (setq n (+ 1 n)))))
           (setq m (* 2 m))
         )
      )
      (return n)
)

11
15
16
32
36
37
53
57
58
74
78
79
95
99
...
960
961
977
981
982
998

==> 142

Здесь просто добавлена печать очередного числа, удовлетворяющего условиям задачи. Желающие могут убедиться, что все приведенне числа условиям задачи удовлетворяют.

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

Поскольку верно равенство 7=23-1, то 2n-n=7*2n-3+2n-3-n. Поэтому, остаток от деления (2n-n) на 7 равен остатку от деления на 7 величины (2n-3-n). Далее: (2n-3-n) = 7*2n-6+2n-6-n. Остаток от деления (2n-3-n) на 7 равен остатку от деления на 7 величины (2n-6-n). Таким образом, чтобы оценить остаток от деления (2n-n) на 7 нужно вычитать из показателя степени по тройке до тех пор, пока показатель не станет меньше 32 (когда степень можно вычислить явно).

Вот программа на Лиспе, реализующая эту идею:

(prog (m n i k)
      (setq n 0)
      (for i 1 1000
         (
          (setq m i)
          (setq k (+ 1 (\ (- m 31) 3)))
          (setq m (- m (* 3 k)))
          (cond ((= (% (- (^ 2 m) i) 7) 0) (setq n (+ 1 n))  ))
         )
      )
      (return n)
)

==> 142

Следует заметить, что время выполнения этой программы составляет чуть меньше 2 сек. Математика себя оправдывает!

Различные математические объекты

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

Рациональная арифметика

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

Рассмотрим множество упорядоченных пар целых взаимно простых чисел (n , d). Первое число пары будем называть числителем, второе - знаменателем. Предполагаем, что d отлично от нуля. Взаимная простота чисел n и d означает, что числа n и d не имеют общих делителей, кроме единицы. Будем считать, что пары (n1 , d1) и (n2 , d2) равны тогда и только тогда, когда n1 = n2 и d1 = d2.

Следует особо подчеркнуть, что в наше множество пар целых включаются не всякие пары, а только пары взаимно-простых чисел. В частности, пара (0 , d) входит в наше множество только при d=1. Соответственно, пара (n , n) входит в наше множество только при n=1. Пары (0 , 1) и (1 , 1) играют в нашем множестве особую роль. Об этом - чуть ниже.

Теперь определим алгебраические операции на множестве пар.

Суммой пар (n1 , d1) и (n2 , d2) будем называть пару (n , d) , числитель которой равен:

n = (n1 * d2 + n2 * d1)/НОД((n1 * d2 + n2 * d1) , (d1 * d2))

а знаменатель:

d = (d1 * d2)/НОД((n1 * d2 + n2 * d1) , (d1 * d2))

Разностью пар (n1 , d1) и (n2 , d2) будем называть пару (n , d) , числитель которой равен:

n = (n1 * d2 - n2 * d1)/НОД((n1 * d2 + n2 * d1) , (d1 * d2))

а знаменатель:

d = (d1 * d2)/НОД((n1 * d2 + n2 * d1) , (d1 * d2))

Произведением пар (n1 , d1) и (n2 , d2) будем называть пару (n , d) , числитель которой равен:

n = (n1 * n2)/НОД((n1 * n2) , (d1 * d2))

а знаменатель:

d = (d1 * d2)/НОД((n1 * n2) , (d1 * d2))

В приведенных выше формулах символ НОД(x , y) означает наибольший общий делитель целых x и y. Использование НОД необходимо, поскольку при вычислении числителя и знаменателя может нарушиться взаимная простота.

Обратной паре (n , d) при n отличном от нуля, будем называть пару (d , n). Легко видеть, что произведение пар (n , d) и (d , n) дает пару (1 , 1).

Противоположной паре (n , d) будем называть пару (-n , d). Легко видеть, что сумма пар (n , d) и (-n , d) дает пару (0 , d).

Можно убедиться, что для определенных выше алгебраических операций будут справедливы сочетательный, распределительный и переместительный законы арифметики. Роль нуля будет играть пара (0 , 1), а роль единицы - пара (1 , 1). Осталось сказать, что пара (n , d) традиционно записывается в виде n/d и называется дробью. Множество целых чисел естественным образом вкладывается во множество дробей, если отожествить целое p с дробью p/1.

Множество дробей с двумя операциями, определенными выше, называется полем рациональных чисел и обозначается Q.

Переходим к моделированию рациональной арифметики на Лиспе. Целочисленная арифметика в нашем распоряжении уже имеется - она встроена в ядро Лиспа. Как следует из приведенных выше определений, нам прежде всего понадобится функция, вычисляющая НОД. Для этого существует классический алгоритм Евклида. Пусть требуется найти НОД двух целых X и Y. Очевидно, что если одно из чисел равно нулю, то НОД равен другому (ненулевому) числу. Если оба числа X и Y равны нулю, то НОД этих чисел неопределен. А при ненулевых X и Y следует поступать так:

1) Вычислить R - остаток от деления X на Y.

2) Если R=0, то НОД(X , Y)=Y

3) Если R <> 0, то положить X=Y; Y=R и перейти к шагу 1).

Алгоритм гарантированно сходится. Ниже приведен нерекурсивный вариант программы вычисления НОД двух чисел.

(defun gcd (x y) (cond
                      ((AND (eq y 0) (eq x 0)) nil)
                      ((eq x 0) y)
                      ((eq y 0) x)
                      (T
                         (prog (xx yy r)
                               (setq xx x)
                               (setq yy y)
                          loop
                               (setq r (remainder xx yy))
                               (cond ((eq r 0) (return yy)))
                               (setq xx yy)
                               (setq yy r)
                               (go loop)
                         )
                      )
                  )
)

==> gcd

(gcd 1212 6543)

==> 3

Название функции происходит от английского "the great common divisor", что по-русски как раз и означает "наибольший общий делитель".

Как представлять дробь в Лиспе? Наиболее естественное представление дроби - в виде двухэлементного списка. Первый элемент будет числителем, второй - знаменателем. А чтобы Лисп мог отличить дробь от произвольного двухэлементного списка, добавим в начало списка атом RATIONAL. Окончательно дробь будет представляться списком из трех элементов: атом RATIONAL, числитель и знаменатель.

В процессе работы нам понадобится программа "сокращения" дроби. Суть сокращения проста - деление числителя и знаменателя на их НОД. Вот соответствующая программа на Лиспе:

(defun  simplify (x) (prog (num denom g sgn)
                      (setq denom (cadr x))
                      (cond ((= denom 0) (raiseerror "Знаменатель равен нулю!")))
                      (setq num (car x))
                      (setq sgn 1)
                      (cond ((And (< num 0) (> denom 0)) (setq sgn -1))
                            ((And (> num 0) (< denom 0)) (setq sgn -1))
                            ((= num 0) (setq denom 1))
                      )
                      (setq num (abs num))
                      (setq denom (abs denom))
                      (setq g (gcd num denom))
                      (cond ((eq g 1) (return (list (* num sgn) denom)))
                            (T (return (list (\ (* num sgn) g) (\ denom g))))
                      )
                     )
)

==> simplify

(simplify '(6 3))

==> (2 1)

(simplify '(3 6))

==> (1 2)

(simplify '(0 6))

==> (0 1)

(simplify '(-6 6))

==> (-1 1)

(simplify '(6 -6))

==> (-1 1)

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

Теперь можно приступать к созданию функций для работы с дробями. Имена всех этих функций будут иметь префикс rat (рациональный). Начнем с функции создания дроби. Она оказывается очень простой:

(defun ratCreate (num denom)
                  (cond ((null (flagp num 'FIXED)) (raiseerror "Числитель - не целое"))
                        ((null (flagp denom 'FIXED)) (raiseerror "Знаменатель - не целое"))
                        (T (cons 'RATIONAL (simplify (list num denom))))
                  )
)

==> ratCreate

(ratCreate -1 2)

==> (RATIONAL -1 2)

(ratCreate -6 3)

==> (RATIONAL -2 1)

(ratCreate 6 -3)

==> (RATIONAL -2 1)

Функция ожидает два параметра - числитель и знаменатель. Возвращает функция трехэлементный список: атом RATIONAL (признак дроби), числитель и знаменатель. К списку (числитель знаменатель) применяется функция simplify (что обеспечивает несократимость дроби и отнесение знака к числителю). Перед созданием списка функция проверяет наличие флага FIXED у значений числителя и знаменателя. Если числитель или знаменатель не являются числом типа FIXED - возбуждается состояние ошибки.

Теперь написать функции манипулирования рациональными оказывается совсем просто. Как ни странно, самой сложной функцией будет функция печати рационального числа. При печати желательно предусмотреть все возможности: чтобы рациональное со знаменателем 1 печаталось как целое, чтобы дробь с числителем 0 печаталась как 0, чтобы из неправильной дроби выделялась целая часть и печаталась отдельно.

Ниже приводится функция ratPrint, которая печатает рациональные числа в привычном для человека виде.

(defun ratPrint (x)
       (cond ((<> (car x) 'RATIONAL) (raiseerror "ratPrint: Не дробь"))
             (T
               (prog (tmp num denom sgn)
                 (setq tmp (simplify (cdr x)))    ;; на всякий случай...
                 (setq num (car tmp))             ;; числитель
                 (setq denom (cadr tmp))          ;; знаменатель
                 (setq sgn (sign num))            ;; знак дроби
                 (setq num (abs num))             ;; берем абс. величину
                 (cond ((= num 0) (go @1) ))      ;; числитель=0
                 (cond ((< sgn 0) (prints "-")))  ;; если знак отрицат.
                 (cond ((= denom num) (go @2) ))  ;; числитель=знаменатель
                 (cond ((= denom 1) (go @3) ))    ;; знаменатель=1 - целое
                 (cond ((> num denom)             ;; дробь неправильная
                        (prog nil                 ;; печатаем целую часть
                          (print (\ num denom))
                          (setq num (% num denom))
                        )
                       )
                 )
                 (prints "(")                     ;; печатаем дробную часть
                 (print num)
                 (prints "/")
                 (print denom)
                 (prints ")")
                 (go @Exit)
          @1     (print 0)
                 (go @Exit)
          @2     (print 1)
                 (go @Exit)
          @3     (print num)
          @Exit  (return x)
               )
             )
       )

)

==> ratPrint

(ratPrint (ratCreate 1 2))

(1/2)

==> (RATIONAL 1 2)

(ratPrint (ratCreate 45 75))

(3/5)

==> (RATIONAL 3 5)

(ratPrint (ratCreate 6 1))

6

==> (RATIONAL 6 1)

А вот и остальные функции манипулирования рациональными числами:

;;
;; Сложить две дроби
;;

(defun ratAdd (x y)
       (cond ((<> (car x) 'RATIONAL) (raiseerror "ratAdd: Не дробь"))
             ((<> (car y) 'RATIONAL) (raiseerror "ratAdd: Не дробь"))
             (T (cons 'RATIONAL
                  (simplify (list (+ (* (caddr y) (cadr x))
                                     (* (caddr x) (cadr y))
                                  )
                                  (* (caddr x) (caddr y))
                            )
                  )
                )
             )
       )
)

;;
;; Вычесть две дроби
;;

(defun ratSub (x y)
       (cond ((<> (car x) 'RATIONAL) (raiseerror "ratSub: Не дробь"))
             ((<> (car y) 'RATIONAL) (raiseerror "ratSub: Не дробь"))
             (T (cons 'RATIONAL
                  (simplify (list (- (* (caddr y) (cadr x))
                                     (* (caddr x) (cadr y))
                                   )
                                   (* (caddr x) (caddr y))
                            )
                  )
                )
             )
       )
)

;;
;; Перемножить дроби
;;

(defun ratMult (x y)
       (cond ((<> (car x) 'RATIONAL) (raiseerror "ratMult: Не дробь"))
             ((<> (car y) 'RATIONAL) (raiseerror "ratMult: Не дробь"))
             (T (cons 'RATIONAL
                  (simplify (list (* (cadr x) (cadr y))
                                  (* (caddr x) (caddr y))
                            )
                  )
                )
             )
       )
)

;;
;; Разделить дроби
;;

(defun ratDiv (x y)
       (cond ((<> (car x) 'RATIONAL) (raiseerror "ratDiv: Не дробь"))
             ((<> (car y) 'RATIONAL) (raiseerror "ratDiv: Не дробь"))
             (T (cons 'RATIONAL
                  (simplify (list (* (cadr x) (caddr y))
                                  (* (caddr x) (cadr y))
                            )
                   )
                 )
              )
       )
)

;;
;; Обратить дробь
;;

(defun ratInv (x)
       (cond ((<> (car x) 'RATIONAL) (raiseerror "ratInv: Не дробь"))
             (T (cons 'RATIONAL
                  (simplify (list (caddr x)
                                  (cadr x)
                            )
                  )
                )
             )
       )
)

;;
;; Из рационального в плавающее
;;

(defun rat2flo (x)
       (cond ((<> (car x) 'RATIONAL)(raiseerror "rat2flo: Не дробь"))
             (T (/ (cadr x) (caddr x)))
       )
)

;;
;; Модуль дроби
;;

(defun ratAbs (x)
       (cond ((<> (car x) 'RATIONAL)(raiseerror "ratAbs: Не дробь"))
             (T (cons 'RATIONAL
                  (list (abs (cadr x))
                        (caddr x)
                  )
                )
             )
       )
)

Следует обратить внимание на то, что функция деления дробей не анализирует случай деления на нуль. В этом нет необходимости, поскольку соответствующую ситуацию "отлавливает" функция simplify.

Мы построили вполне корректную рациональную арифметику, причем, достаточно скромными средствами.

Комплексные числа

Прежде, чем строить систему комплексных чисел, напомним их математическое определение. Комплексным числом будем называть упорядоченную пару вещественных чисел (a , b). Две таких пары (a1 , b1) и (a2 , b2) будем считать равными в том и только в том случае, когда a1 = a2 и b1 = b2.

Теперь введем на множестве пар естественные алгебраические операции сложение и умножение.

Суммой комплексных чисел (a1 , b1) и (a2 , b2) будем называть новое комплексное число (с , d), такое, что:

c = a1 + a2
d = b1 + b2

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

Произведением комплексных чисел (a1 , b1) и (a2 , b2) будем называть новое комплексное число (с , d), такое, что:

c = a1 * a2 - b1 * b2
d = a1 * b2 + a2 * b2

Для умножения комплексных чисел будет также будут выполняться сочетательный и переместительный законы. А для сложения и умножения - распределительный закон.

Рассмотрим теперь пары вида (a , 0). Легко убедиться, что сумма и произведение таких пар тоже имеет вид (a , 0). Причем, произведение пар (x , 0) и (y , 0) равно (xy , 0), а сумма - (x+y , 0). Кроме того, произведение пары (a , 0) на произвольную пару (x , y) даст в результате пару (ax , ay). Все это позволяет отождествить пары вида (a , 0) с вещественными числами.

А теперь рассмотрим комплексное число (0 , 1). Легко убедиться, что квадрат этого числа (в смысле определенного выше умножения) равен числу (-1 , 0). Последнее число мы договорились отождествить с обычным вещественным числом -1. Число (0 , 1) в математике обозначается символом i. Таким образом, оказывается верным равенство:

i2 = -1

Кроме того, произвольное комплексное число (a , b) можно представить в виде: a + b*i, где a - сокращенная запись числа (a , 0), а b - сокращенная запись числа (b , 0).

Для произвольного комплексного числа X = (a , b) вещественное число a называется действительной частью X (обозначается Re(X)); а вещественное число b называется мнимой частью X (обозначается Im(X)).

Для произвольного комплексного числа X = (a , b) вещественное число r = (a2 + b2)1/2 называется модулем комплексного числа, а число fi = arctg(b/a) - его аргументом. С использованием модуля и аргумента любое комплексное число может быть представлено в эквивалентной тригонометрической форме:

(a , b) = r*(cos(fi) + i * sin(fi))

При возведении комплексного числа в целую степень имеет место замечательное соотношение (формула Муавра):

(a , b)n = (rn)*(cos(n*fi) + i * sin(n*fi))

Здесь n - целый показатель степени. Сопряженным к произвольному комплексному числу (a , b) будем называть число (a , -b). Легко убедиться, что для произвольного (a , b) имеет место:

(a , b)*(a , -b) = a2 + b2

Произведение числа на сопряженное равно квадрату модуля числа.

После этого математического введения построить систему комплексных чисел на Лиспе будет совсем просто. Как и в случае рациональных чисел, будем моделировать комплексное число списком из трех элементов. Первый элемент этого списка - всегда атом COMPLEX - признак комплексного числа; два последующих элемента представляют собой действительную и мнимую части комплексного числа. Снова, как и в случае рациональных чисел, самой сложной функцией будет функция вывода комплексного числа. Остальные функции реализуются достаточно элементарно. Имена всех функций манипулирования комплексными числами будут начинаться с префикса "cplx".

Вот как выглядит функция cplxCreate - создать комплексное число:

;;
;;  Создать комплексное число
;;

(defun cplxCreate (x y)

        (cond ((not (numberp x)) (raiseerror "cplxCreate: нечисловой аргумент"))
              ((not (numberp y)) (raiseerror "cplxCreate: нечисловой аргумент"))
              (T (cons 'COMPLEX (list (+ x 0.0) (+ y 0.0))))
        )
)

==> cplxCreate

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

Как и при реализации рациональной арифметики, самой громоздкой функцией является функция печати комплексного числа. Тут важно предусмотреть печать комплексного числа без мнимой части, как действительного; чисто мнимого комплексного числа - без нулевой действительной части; у мнимой части не выводить коэффициент 1 при мнимой части. Собирая все вместе, получим:

;;
;;  Печать
;;

(defun cplxPrint (x)
       (cond ((<> (car x) 'COMPLEX) (raiseerror "cplxPrint: неверен тип аргумента"))
             (T
                (prog (re im)

                  (setq re (cadr x))

                  (setq im (caddr x))

                  (cond ((and (<= (abs re) 1.0E-15) (<= (abs im) 1.0E-15)) (go @PrZ)))

                  (cond ((and (> (abs re) 1.0E-15) (<= (abs im) 1.0E-15)) (go @PrR)))

                  (cond ((and (<= (abs re) 1.0E-15) (> (abs im) 1.0E-15)) (go @PrI)))

                  (print re)

                  (cond ((> im 0.0)
                           (prog nil
                               (prints "+")
                               (cond ((> (abs (- im 1)) 1.0E-15) (prog nil  (print im))))
                               (prints "J")
                           )
                        )
                        ((< im 0.0)
                           (prog nil
                               (prints "-")
                               (setq im (abs im))
                               (cond ((> (abs (- im 1)) 1.0E-15) (prog nil  (print im))))
                               (prints "J")
                           )
                        )
                  )

                  (go @Ret)

     @PrZ         (print 0)  (go @Ret)
     @PrR         (print re) (go @Ret)
     @PrI
                  (cond ((> im 0.0)
                           (prog nil

                               (cond ((> (abs (- im 1)) 1.0E-15) (prog nil (print im))))
                               (prints "J")
                           )
                        )
                        ((< im 0.0)
                           (prog nil
                               (prints "-")
                               (setq im (abs im))
                               (cond ((> (abs (- im 1)) 1.0E-15) (prog nil  (print im))))
                               (prints "J")
                           )
                        )
                  )

     @Ret         (return x)

                )
             )
       )
)

==> cplxPrint

Реализация остальных функций выглядят достаточно просто:

;;
;;  Действительная часть комплексного числа
;;

(defun cplxRe (x) (cond ((<> (car x) 'COMPLEX)
                        (raiseerror "cplxRe: неверен тип аргумента"))
                        (T (cadr x))
                  )
)

;;
;;  Мнимая часть комплексного числа
;;

(defun cplxIm (x) (cond ((<> (car x) 'COMPLEX)
                        (raiseerror "cplxIm: неверен тип аргумента"))
                        (T (caddr x))
                  )
)

;;
;;  Сопряжение
;;

(defun cplxConj (x) (cond ((<> (car x) 'COMPLEX)
                           (raiseerror "cplxConj: неверен тип аргумента"))
                          (T (cons 'COMPLEX (list (cadr x) (- (caddr x))))
                          )
                  )
)

;;
;; Модуль
;;

(defun cplxAbs (x) (cond ((<> (car x) 'COMPLEX)
                          (raiseerror "cplxAbs: неверен тип аргумента"))
                         (T (sqr (+ (* (cadr x) (cadr x)) (* (caddr x) (caddr x)))))
                   )
)

;;
;; Аргумент
;;

(defun cplxArg (x) (cond
                      ((<> (car x) 'COMPLEX)
                       (raiseerror "cplxArg: неверен тип аргумента"))
                      (T
                        (cond ((<= (abs (cadr x)) 0.1E-16) (* 0.5 _Pi (sign (caddr x))))
                               (T (atn (/ (caddr x) (cadr x))))
                         )
                       )
                   )
)

;;
;;  Сложение
;;

(defun cplxAdd (x y) (cond ((<> (car x) 'COMPLEX)
                            (raiseerror "cplxAdd: неверен тип аргумента"))
                           ((<> (car y) 'COMPLEX)
                            (raiseerror "cplxAdd: неверен тип аргумента"))
                           (T (cplxCreate (+ (cadr x) (cadr y)) (+ (caddr x) (caddr y))))
                     )
)

;;
;;  Вычитание
;;

(defun cplxSub (x y) (cond ((<> (car x) 'COMPLEX)
                            (raiseerror "cplxSub: неверен тип аргумента"))
                           ((<> (car y) 'COMPLEX)
                            (raiseerror "cplxSub: неверен тип аргумента"))
                           (T (cplxCreate (- (cadr x) (cadr y)) (- (caddr x) (caddr y))))
                     )
)

;;
;;  Умножение
;;

(defun cplxMult (x y) (cond ((<> (car x) 'COMPLEX)
                             (raiseerror "cplxMult: неверен тип аргумента"))
                           ((<> (car y) 'COMPLEX)
                             (raiseerror "cplxMult: неверен тип аргумента"))
                           (T (cplxCreate (- (* (cadr x) (cadr y)) (* (caddr x) (caddr y)))
                                          (+ (* (cadr x) (caddr y)) (* (caddr x) (cadr y))))
                           )
                     )
)

;;
;;  Деление
;;

(defun cplxDiv (x y) (cond ((<= (cplxAbs y) 0.1E-15)
                            (raiseerror "cplxDiv: Деление на нуль"))
                           (T
                              (prog (d Z)
                                (setq d (+ (* (cadr y) (cadr y)) (* (caddr y) (caddr y))))
                                (setq Z (cplxMult x (cplxConj y)))
                                (return (cons 'COMPLEX (list (/ (cadr Z) d) (/ (caddr z) d))))
                              )
                           )
                     )
)

;;
;; Возведение в степень (формула Муавра)
;;

(defun cplxPow (x n)

       (cond ((or (<> (car x) 'COMPLEX)
                  (Not (fixedp n)))
                  (raiseerror "cplxPow: неверен тип одного из аргументов")
              )
             (T
               (prog (rr fi)
                  (setq rr (cplxAbs x))
                  (setq fi (cplxArg x))
                  (return  (cplxCreate (* (^ rr n) (cos (* fi n))) (* (^ rr n) (sin (* fi n)))))
               )
             )
       )
)

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

(cplxPrint (cplxPow (cplxCreate 0 1) 2))
-1.0-6.98296672221876E-15J
==> (COMPLEX -1.0 -6.98296672221876E-15)

Результат вызова (cplxCreate 0 1) есть просто мнимая единица (J). Возведение мнимой единицы в квадрат должно дать -1. У результата, приведенного выше, действительная часть действительно равна -1, а мнимая часть есть очень маленькая величина, но не нуль. Причина заключается в погрешности при вычислении тригонометрических функций (sin и cos).

Векторы и матрицы

n-мерным вектором над полем действительных чисел называется упорядоченный набор из n чисел вида:

(x1, x2, ... , xn)

Числа x1, x2 и т.д. называются координатами вектора. Два вектора считаются равными тогда и только тогда, когда они имеют одинаковую размерность (число координат) и при всех i от 1 до n i-я первого вектора равна i-й координате второго вектора.

С векторами можно выполнять различные действия. Простейшими из этих действий являются сложение векторов и умножение вектора на действительное число.

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

(x1, x2, ... , xn)

и

(y1, y2, ... , yn)

то их суммой будет вектор с координатами:

((x1+y1), (x2+y2), ... , (xn+yn) )

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

Несколько более сложной операцией является скалярное произведение векторов. По определению скалярное произведение векторов

(x1, x2, ... , xn)

и

(y1, y2, ... , yn)

есть вещественное число, вычисляемое следующим образом:

(x1*y1) + (x2*y2) + ... + (xn*yn)

Модулем (или длиной) вектора называется квадратный корень из суммы квадратов координат вектора.

Как хорошо известно, векторы с двумя компонентами можно естественным образом отождествить с векторами на плоскости, а векторы с тремя компонентами - с векторами в пространстве. Теорема Пифагора показывает, что для векторов на плоскости и в пространстве модуль вектора совпадает с его обычной евклидовой длиной. Можно также убедиться, что скалярное произведение вектров есть произведение их модулей на косинус угла между ними.

Прямоугольной матрицей размерности (n*m) называется прямоугольная таблица вещественных чисел:

x1,1 | x1,2 | ... | x1,m
x2,1 | x2,2 | ... | x2,m
... | ... | ... | ...
xn,1 | xn,2 | ... | xn,m

Если количество строк матрицы равно количеству столбцов, то матрица называется квадратной. Две матрицы одинаковой размерности:

x1,1 | x1,2 | ... | x1,m
x2,1 | x2,2 | ... | x2,m
... | ... | ... | ...
xn,1 | xn,2 | ... | xn,m

и

y1,1 | y1,2 | ... | y1,m
y2,1 | y2,2 | ... | x2,m
... | ... | ... | ...
yn,1 | yn,2 | ... | yn,m

равны в том и только в том случае, когда при всех (1 <= i <= n) и (1 <= j <= m) имеет место xi,j=yi,j.

Суммой (разностью) двух матриц одинаковой размерности называется матрица, элементы которой вычисляются "покоординатным" сложением (вычитанием) элементов исходных матриц. Для приведенных выше матриц матричная сумма будет новой матрицей, элементы которой вычисляются в соответствии с соотношением: zi,j=xi,j + yi,j, а разностью - матрица, с элементами wi,j=xi,j - yi,j.

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

А вот произведение матриц определяется заметно сложнее, чем описанные выше операции.

Продолжение следует...


Установка и удаление HomeLisp

HomeLisp нормально работает в Win-98. Разработчик не проверял работоспособность программы в Win-95, Win-Me (за отсутствием под рукой этих ОС). Вполне успешно работает HomeLisp и в системах Win2k и более поздних (вплоть до Win-7).

Установка HomeLisp не должна вызвать трудностей. Компьютер, на который выполняется установка, будем называть целевым, а директорию, в которую выполняется установка - целевой.

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

Перед началом процедуры установки желательно закрыть все выполняющиеся приложения. Затем следует запустить инсталлятор iHomeLisp.exe (являющийся саморазворачивающимся rar-архивом) и задать директорию установки. В директории установки будут созданы поддиректории Lib, Samples, Patt, Web и Com. Назначение поддиректорий следующее:

Директория Lib содержит автозагружаемую библиотеку дополнительных функций;

Директория Samples содержит примеры программ;

Директория Patt содержит файл шаблонов стандартных конструкций языка;

Директория Web содержит реализацию Web-компоненты HomeLisp, рассчитанную на работу в среде IIS.

Директория Com содержит требуемые ActiveX-компоненты;

Поскольку программа HomeLisp написана на VB6, то ей для работы необходимы следующие дополнительные программные компоненты:

MSVBVM60.DLL - виртуальна машина VB 6.0. Предполагается, что эта библиотека есть на всех машинах, работающих под win-2k/XP. В связи с этим MSVBVM60.DLL не включена в пакет инсталляции;

Mscomctl.ocx - стандартная палитра компонентов (тулбар, статусбар и т.д.);

Msflxgrd.ocx - стандартный грид Микрософт;

Richtx32.ocx - RichText-бокс Микрософт;

cntAssoList.ocx, cntObjList.ocx, extCombo.ocx, tpw.dll, xCoder.dll - служебные компоненты.

С целю предотвращения конфликта устанавливаемых компонентов с существующими, разработчик использовал следующую методику установки:

Все компоненты распаковываются в поддиректорию Com;

Сразу после распаковки автоматически запускается программа VbSetup.exe, которая для каждого из перечисленных выше компонентов проверяет наличие на компьютере зарегистрированной копии. Проверка, разумеется, выполняется не по имени компонента, а по его GUIDу;

Если очередной компонент уже имеется на компьютере, соответствующий файл удаляется из директории Com;

Если же очередной компонент не найден в реестре, то выполняется его регистрация в директории Com, а имя компонента заносится в файл setup.log. Понятно, что этот файл не следует удалять после инсталляции!

После завершения процесса регистрации программа VbSetup.exe автоматически удаляется из целевой директории.

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

В целевой директории кроме исполяемого модуля HomeLispIde.exe устанавливается программа VbUnsetup.exe (программа деинсталляции), а также создаются файлы homelisp.ini и homelisp.stb. Файл homelisp.ini содержит первоначальные настройки программы. Файл homelisp.stb является файлом-заготовкой для режима создания exe-файлов. Весь процесс установки (включая распаковку и регистрацию компонентов) занимает 20-35 сек.

Для деинсталляции программы следует запустить VbUnsetup.exe и подтвердить деинсталляцию. Программа VbUnsetup.exe разрегистрирует зарегистрированные ранее компоненты (взяв их имена из файла setup.log), а затем удалит все поддиректории целевой директории вместе с содержимым.


Просмотр списка объектов и ассоциативного списка

Список объектов содержит все атомы, присутствующие в сеансе работы. Атомы создаются автоматически по мере необходимости, а также в процессе инициализации ядра Лиспа. Каждый атом имеет список свойств, состоящий из индикаторов. Ряд индикаторов являются стандартными (встроенными). Это APVAL, FIXED, FLOAT, BITS, EXPR, FEXPR, MACRO, SUBR, FSUBR и ряд других. Пользователь может создавать и свои индикаторы (см. ниже). Вся эта информация наглядно представляется в виде трехколоночного списка. В первой колонке стоят имена атомов, во второй - стандартные свойства, а в третьей - дополнительные свойства. Для вызова списка объектов следует нажать 12-ю кнопку тулбара (или выбрать соответствующий пункт меню). Чтобы вернуться к вводу S-выражений следует повторно нажать 12-ю кнопку тулбара.

Список объектов представляется следующим образом:

Из рисунка видно, что у атома VER установлены два стандартных индикатора (APVAL и STRING). Индикатор APVAL означает, что атом является константой. Значением такой константы может быть либо другой атом, либо этот же атом. У атома VER значением является другой атом - строковая константа.

При необходимости список объектов можно отсортировать по именам атомов. Для этого нужно щелкнуть мышью по заголовку колонки "имя атома". Если щелкнуть по заголовку "стандартные свойства", то содержание списка будет отсортировано по второй колонке. Этими возможностями удобно пользоваться для поиска в списке нужной информации. У атомов-имен функций заданных пользователем будет установлен один из индикаторов EXPR, FEXPR или MACRO. У таких атомов можно увидеть определяющее выражение функции. Для этого достаточно щелкнуть по имени атома в первой колонке списка:

Определяющее выражение функции можно увидеть, вызвав функцию GETD.

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

(grwCreate 'w-1 300 300 "Проба" _WHITE)

==>  w-1

Если теперь пользватель найдет атом W-1 в списке объектов, то он увидит следующую картину:

Для просмотра свойств атомов также существуют встроенная функция PROPLIST .

Значения, присваиваемые атомам с помощью конструкций SET/SETQ хранятся в ассоциативном списке. Для отображения ассоциативного списка служит 13-я кнопка тулбара. Ассоциативный список представляется в виде трехколоночной сеточной формы. Во второй колонке находится имя атома, в третьей - значение. Значением атома может быть произвольное S-выражение. Если в сеансе еще ни разу не выполнялись команды SET/SETQ, то ассоциативный список пуст, и 13-я кнопка тулбара неактивна. Однако, если пользователь введет команды:

(SETQ A1 123)

==> 123

(SETQ A2 '(1 2 3))

==> (1 2 3)

(SETQ A3 "Проба")

==> "Проба"

(SETQ A4 '(A . B))

==> (a . b)

и затем нажмет 13-ю кнопку, то увидит следующую картину:

Если щелкнуть мышью по красному кресту, расположенному в первой колонке, то можно удалить из системы соответствующую переменную. Чтобы вернуться к вводу S-выражений следует повтороно нажать 13-ю кнопку тулбара.


Описание строковых функций

Описание строковых функций HomeLisp

Строки являются важнейшим типом данных. Без использования строк, например, пректически невозможно организовать взвимодействие программы с пользователем. Строка в HomeLisp-е является специфическим атомом, первым и последним сиволом имени которого являются двойные кавычки.

Важнейшим отличием строк от атомов является то обстоятельство, что в строках заглавные и строчные буквы различаются. Так, ABC и abc - это один и тот же атом, а строки "ABC" и "abc" - различные атомы.

Имеется особенность при записи строк, содержащих в имени двойную кавычку: двойные кавычки внутри строки при записи должны удваиваться. Так, запись "Проверка "связи"" - неверная запись строки. Правильная запись этой строки имеет вид: "Проверка ""связи""". Двойные кавычки удваиваются только при записи; внутри строки каждая удвоенная кавычка хранится как один символ. В этом легко убедиться, вычислив длину строки "Проверка ""связи""" с помощью встроенной функции STRLEN; будет получен результат 16, а не 18, как могло бы показаться.

Обработка строк предполагает в качестве операций разрезание строк на отдельные части, замену отдельных символов на другие и т.д. Все эти действия можно назвать алхимией: атом может быть "разделен" на две или более частей! Следует, правда, отметить, что при "разделении" атома на части, исходный атом никуда не девается.

Все строковые функции являются функциями типа SUBR (их аргументы вычисляются).

Имена строковых функций в HomeLisp для удобства начинаются префиксом STR. Строковые функции доступны во всех режимах работы HomeLisp. Две функции (STRISFIX и STRISFLO) реализованы только в 13-й редакции ядра.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
STRASC | 1 | STRING | Дать ASCII-код первого символа строки
STRATRIM | 1 | STRING | Удалить лидирующие и хвостовые пробелы
STRCAT | Переменное | STRING | Склейка строк
STRCHR | 1 | FIXED | Дать односимвольную строку по коду ASCII аргумента
STRDEL | 3 | STRING, FIXED, FIXED | Удалить в строке подстроку
STRIND | 2 | STRING | Дать позицию (индекс) подстроки
STRINJ | 3 | STRING, FIXED, STRING | Вставить подстроку в строку
STRISFIX (13) | 1 | STRING | Является ли строка представлением числа типа FIXED
STRISFLO (13) | 1 | STRING | Является ли строка представлением числа типа FLOAT
STRLCASE | 1 | STRING | Перевести строку в нижний регистр
STRLEN | 1 | STRING | Дать длину строки
STRLTRIM | 1 | STRING | Удалить лидирующие пробелы
STRMID | 2 или 3 | STRING, FIXED, [FIXED] | Дать подстроку с заданной позиции и заданной длины (или до конца строки)
STRPRESS | 1 | STRING | Удалить из строки все пробелы (сжать)
STRREP | 3 | STRING | Заменить в строке все вхождения заданной подстроки
STRRTRIM | 1 | STRING | Удалить хвостовые пробелы
STRSPACE | 1 | FIXED | Дать строку из заданного к-ва пробелов
STRSTRING | 2 | FIXED | Дать строку из заданного к-ва символов с заданным ASCII-кодом
STRUCASE | 1 | STRING | Перевести строку в верхний регистр
STRUPD | 3 | STRING, FIXED, STRING | Заменить подстроку с заданного символа
STRASC

Функция STRASC принимает один аргумент типа STRING и возвращает результат типа FIXED, равный значению ASCII-кода первого символа строки-параметра. Если на вход функции подается пустая строка (нулевой длины), то возбуждается состояние ошибки.

Вот примеры вызова функции STRASC:

(strAsc "123")

==> 49

(strAsc "Bob")

==> 66

(strAsc "")

STRASC: аргумент - пустая строка
==> ERRSTATE

(strAsc """45")

==> 34
STRATRIM

Функция STRATRIM принимает один аргумент типа STRING и возвращает новую строку, из которой удалены лидирующие и хвостовые пробелы. Пробелы, стоящие внутри строки, не удаляются. Если входная строка не имеет лидирующих и хвостовых пробелов, то функцию возвращает значение аргумента.

Вот примеры вызова функции STRATRIM:

(stratrim "   Проба пера   ")

==> "Проба пера"

(stratrim "Проба пера")

==> "Проба пера"
STRCAT

Функция STRCAT принимает произвольное количество аргументов типа STRING и возвращает новую строку, равную конкатенации (склейке) значений своих аргументов. Если задан только один аргумент, то функция вернет значение этого аргумента. Если среди аргуметов встретится аргумент, не имеющий типа STRING, то возбуждается состояние ошибки.

Вот примеры вызова функции STRCAT:

(strCat "QQQ" "WWW" "EEE")

==> "QQQWWWEEE"

(strCat "QQQ")

==> "QQQ"

(strCat "10!=" (fact 10))

STRCAT: один из аргументов - не cтрока
==> ERRSTATE

(strCat "10!=" (fix2str (fact 10)))

==> "10!=3628800"

(strCat "QQQ" "")

==> "QQQ"

В первом примере выполняется склейка трех строк. Во втором примере на вход функции подан единственный аргумент. Результат функции - его значение. В третьем примере одним из аргументов является число типа FIXED; возникает ошибка. А если преобразовать значение типа FIXED в строку, то ошибки не возникает. Последний пример показывает пример склейки с пустой строкой.

STRCHR

Функция STRCHR принимает один аргумент типа FIXED, значение которого должно быть в диапазоне [0,255]. Функция возвращает односимвольную строку с символом, код ASCII, которого равен значению аргумента. Если значение аргумента находится за пределами допустимого диапазона, то возбуждается состояние ошибки.

Вот примеры вызова функции STRCHR:

(strChr 65)

==> "A"

(strChr 98)

==> "b"

(strChr 234)

==> "к"

(strChr -19)

STRCHR: значение параметра вне диапазона [0,255]
==> ERRSTATE
STRDEL

Функция STRDEL принимает три обязательных аргумента: первый - типа STRING, два последующих - типа FIXED. Функция возвращает результат удаления из строки, заданной первым аргументом, последовательности символов длиной, заданной третьим аргументом, с позиции, заданной вторым аргументом.

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

Вот примеры вызова функции STRDEL:

(strDel "Проба" 2 3)

==> "Па"

(strDel "Проба" 12 3)

STRDEL: начальная позиция за пределами строки
==> ERRSTATE

(strDel "Проба" -12 3)

STRDEL: один из числовых аргументов неположителен
==> ERRSTATE

(strDel "Проба" 2 30)

==> "П"

(strDel "Проба" 1 30)

==> ""
STRIND

Функция STRIND принимает два обязательных аргумента STRING. Функция возвращает номер позиции, начиная с которой строка, заданная вторым аргументом входит в строку, заданную первым аргументом. Если строка, заданная вторым аргументом НЕ входит в строку, заданную первым аргументом, то функция возвращает нуль. Вот примеры вызова функции STRIND:

(strInd "HomeLisp" "Lisp")

==> 5

(strInd "HomeLisp" "lisp")

==> 0
STRINJ

Функция STRINJ принимает три обязательных аргумента: первый - типа STRING, второй - типа FIXED, и третий - типа STRING. Функция возвращает строку, полученную из значения первого аргумента, путем вставки с позиции, заданной значением второго аргумента строки-значения третьего аргумента. При этом символы исходной строки, стоящие правее позиции вставки, сдвигаются вправо (в отличие от действия описанной ниже функции STRUPD, которая не раздвигает исходную строку, а заменяет символы правее позиции вставки новыми).

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

Вот примеры вызова функции STRINJ:

(strInj "Па" 2 "роб")

==> "Проба"

(strInj "Па" 1 "роб")

==> "робПа"

(strInj "Па" 3 "роб")

==> "Пароб"

(strInj "Па" 4 "роб")

STRINJ: начальная позиция за пределами строки
==> ERRSTATE

Следует обратить внимание на то, что если значение второго аргумента равно единице, то действие функции эквивалентно действию STRCAT с третьим и первым аргументами. Аналогично, если значение второго аргумента равно длине строки-первого аргумента, увеличенной на единицу, то действие функции эквивалентно действию STRCAT с третьим и первым аргументами.

STRISFIX

Функция STRISFIX (реализованная только в 13-й редакции ядра) принимает один аргумент типа STRING. Функция возвращает логическое значение (T или Nil) в зависимости от того, является ли значение аргумента корректным представлением числа типа FIXED.

Вот примеры вызова функции STRISFIX:

(strIsFix "90")

==> T

(strIsFix "9.0")

==> NIL

(strIsFix "9,0")

==> NIL

(strIsFix &H77)

STRISFIX: параметр - не строка
==> ERRSTATE

(strIsFix 78)

STRISFIX: параметр - не строка
==> ERRSTATE

Видно, что функция strIsFix контролирует тип своего аргумента (должен быть строкой) и ведет себя вполне предсказуемо.

STRISFLO

Функция STRISFLO (реализованная только в 13-й редакции ядра) принимает один аргумент типа STRING. Функция возвращает логическое значение (T или Nil) в зависимости от того, является ли значение аргумента корректным представлением числа типа FLOAT.

Вот примеры вызова функции STRISFLO:

(strIsFlo 7.9)

strIsFlo: параметр - не строка
==> ERRSTATE

(strIsFlo "7,9")

==> NIL

(strIsFlo "7.9")

==> T

(strIsFlo "7.9E2")

==> T

(strIsFlo "7.9D2")

==> T

(strIsFlo "792")

==> T

(strIsFlo (fix2str (fact 100)))

==> T

(strIsFlo (fix2str (fact 300)))

==> NIL

Видно, что функция strIsFix контролирует тип своего аргумента (должен быть строкой) и ведет себя вполне предсказуемо. Пусть читатель обратит внимание на два момента:

Символом порядка в числе с плавающей точкой могут быть буквы E или D;

Для чисел с фиксированной точкой, попадающих в диапазон [-10300,10300] функция STRISFLO возвращает T, а для целых, выходящих за пределы указанного диапазона - Nil.

Последнее хорошо видно на последней врезке: поскольку 100!~10157, то функция возвращает T, тогда как 300! лежит за пределами допустимого диапазона и функция возвращаете Nil.

STRLCASE

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

Вот примеры вызова функции STRLCASE:

(strLcase "HomeLisp")

==> "homelisp"

(strLcase "123")

==> "123"
STRLEN

Функция STRLEN принимает один обязательный аргумент типа STRING. Функция возвращает длину строки, заданной значением аргумента. Если аргумент представляет собой пустую строку, функция возвращает нуль.

Вот примеры вызова функции STRLEN:

(strLen "HomeLisp")

==> 8

(strLen (fix2str (fact 100)))

==> 158

(strLen "")

==> 0
STRLTRIM

Функция STRLTRIM принимает один аргумент типа STRING и возвращает новую строку, из которой удалены только лидирующие пробелы. Пробелы, стоящие внутри строки (а также хвостовые пробелы), не удаляются. Если входная строка не имеет лидирующих пробелов, то функцию возвращает значение аргумента.

Вот примеры вызова функции STRLTRIM:

(strltrim "   Проба пера   ")

==> "Проба пера   "

(strltrim "Проба пера")

==> "Проба пера"
STRMID

Функция STRMID принимает два или три аргумента. Первый из них должен иметь тип STRING; один или два последующих - FIXED. Функция возвращает подстроку, строки, заданной первым аргументом, с позиции, заданной вторым аргументом и длиной, равной значению третьего аргумента. Если третий аргумент опущен - возвращается построка с заданной позиции до конца строки. Если значение второго аргумента больше длины строки, то функция возвращает пустую строку. Значения второго и третьего аргументов (если последний задан) должны быть положительными, в противном случае возбуждается состояние ошибки.

Вот примеры вызова функции STRMID:

_ver

==> "HomeLisp Вер. 1.11.1 (Файфель Б.Л.)"

(strLen _ver)

==> 35

(strMid _ver 40 5)

==> ""

(strMid _ver 5 4)

==> "Lisp"

(strMid _ver 5)

==> "Lisp Вер. 1.11.1 (Файфель Б.Л.)"

(strMid _ver 1 4)

==> "Home"

(strMid _ver 1 -4)

STRMID: второй и/или третий параметр неположителен
==> ERRSTATE

(strMid _ver -1 4)

STRMID: второй и/или третий параметр неположителен
==> ERRSTATE

Здесь используется предопределенный атом _ver, содержащий сведения о версии программы. Этот атом имеет значением строку, которая приведена на врезке. Попытка получить подстроку с позиции за пределами строки возвращает строку нулевой длины.

STRPRESS

Функция STRPRESS принимает один аргумент типа STRING и возвращает новую строку, из которой удалены все пробелы (лидирующие, стоящие внутри строки и хвостовые). Если входная строка не содержит пробелов, то функцию возвращает значение аргумента.

Вот примеры вызова функции STRPRESS:

(strPress "   Проба пера   ")

==> "Пробапера"

(strPress "Проба пера")

==> "Пробапера"

В отличие от функций STRLTRIM, STRRTRIM и STRATRIM, функция STRPRESS удаляет из строки абсолютно все пробелы.

STRREP

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

Другими словами, STRREP осуществляет замену по схеме:

(strRep Где_заменять Что_заменять На_что_заменять)

Вот примеры вызова функции STRREP:

(strRep "Вышел котик на тропинку" "котик" "песик")

==> "Вышел песик на тропинку"

(strRep "котик котик Котик" "котик" "песик")

==> "песик песик Котик"

В последнем вызове не произошло замены строки "Котик" на строку "песик", поскольку строки "Котик" и "котик" не совпадают.

STRRTRIM

Функция STRRTRIM принимает один аргумент типа STRING и возвращает новую строку, из которой удалены только хвостовые пробелы. Пробелы, стоящие внутри строки (а также лидирующие пробелы), не удаляются. Если входная строка не имеет хвостовых пробелов, то функцию возвращает значение аргумента.

Вот примеры вызова функции STRRTRIM:

(strrtrim "   Проба пера   ")

==> "   Проба пера"

(strrtrim "Проба пера")

==> "Проба пера"
STRSPACE

Функция STRSPACE принимает один аргумент типа FIXED и возвращает строку, состоящую из пробелов, c длиной, равной значению аргумента. Значение аргумента не может быть отрицательным (возбуждается состояние ошибки) и не должно превышать 1000. Если значение аргумента равно нулю, функция вернет пустую строку.

Вот примеры вызова функции STRSPACE:

(strspace 5)

==> "     "

(strspace 0)

==> ""

(strspace -1)

strSpace: значение параметра вне диапазона [0,1000]
==> ERRSTATE
STRSTRING

Функция STRSTRING принимает два аргумента типа FIXED и возвращает строку, состоящую из символов с кодом ASCII, равным значению второго аргумента, длиной, равной значению первого аргумента. Значение первого аргумента должно лежать в диапазоне [0-1000]; при этом, если значение равно нулю, то функция вернет строку нулевой длины. Значение второго аргумента должно лежать в диапазоне [0-255]. Если значение хотя бы одного из аргументов лежит вне допустимого диапазона, фиксируется ошибка.

Вот примеры вызова функции STRSTRING:

(strstring 0 45)

==> ""

(strstring 0 -45)

strString: значение второго параметра вне диапазона [0,255]
==> ERRSTATE

(strstring -4 45)

strString: значение первого параметра вне диапазона [1,1000]
==> ERRSTATE

(strstring 10 45)

==> "----------"
STRUCASE

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

Вот примеры вызова функции STRUCASE:

(strUcase "HomeLisp")

==> "HOMELISP"

(strLcase "123")

==> "123"
STRUPD

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

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

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

Вот примеры вызова функции STRUPD:

(strUpd "QQQQQQWWWWWW" 5 "------")

==> "QQQQ------WW"

(strUpd "QQQQQQWWWWWW" 55 "------")

STRUPD: позиция замены за пределами строки
==> ERRSTATE

(strUpd "QQQQQQWWWWWW" 5 "")

==> "QQQQQQWWWWWW"

(strUpd "QQQQQQWWWWWW" 5 "----------------------")

==> "QQQQ--------"


Описание системных функций

Описание системных функций HomeLisp

В этом разделе описывается библиотека системных функций, встроенных в HomeLisp. Эти функции предназначены для получения системной информации, вызова стандартных диалогов, и выполнение некоторых других действий, без которых в среде Windows трудно писать удобные программы.

Большая часть этих функций представляют собой Лисповские надстройки над соответствующими функциями Visual Basic или Windows API.

Имена всех системных функций для удобства начинаются с префикса SYS. Системные функции доступны при работе в среде разработки, в составе EXE-файла, а также из библиотеки HomeLispLib.Exe. При работе WEB-компоненты функции недоступны.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
SYSARGC | - | - | Возвращает число аргументов командной строки
SYSARGV | 1 | FIXED | Возвращает аргумент командной строки заданного номера
SYSCOMPNAME | - | - | Возвращает имя рабочей станции (имя компьютера)
SYSCOPY | 2 | STRING | Копирует файл с именем, заданным значением ПЕРВОГО аргумента, в файл с именем, заданным значением ВТОРОГО аргумента.
SYSDATE | 1 (необязательный) | STRING | Возвращает системную дату, отформатированную в соответствии со строкой-значением параметра. Если параметр опущен - выдается дата в стандартном формате.
SYSDIR | 2 | 1-STRING; 2-BITS | Возвращает список файлов и/или директорий, соответствующий шаблону сканирования, заданному ПЕРВЫМ аргументом. Тип элементов оглавления задаются ВТОРЫМ параметром.
SYSDISKSPACE | 1 | STRING | Дает полный объем памяти и объем свободной памяти на заданном диске
SYSDRIVETYPE | 1 | STRING | Дает тип носителя для логического диска, заданного параметром
SYSENV | 1 | STRING или FIXED | Дает перменную окружения (по имени или по номеру)
SYSERASE | 1 | STRING | Удаляет файл с именем, заданным значением параметра
SYSEXEC | 3 | 1-STRING; 2-STRING; 3-FIXED | Запускает приложение (16-и или 32-х-битное) c именем исполняемого файла, заданного значением ПЕРВОГО аргумента и командной строкой, заданной значением ВТОРОГО аргумента. Статус окна задается третьим параметром. После завершения приложения функция возвращает код завершения.
SYSGETATTR | 1 | STRING | Возвращает характеристики файла с именем, заданным значением параметра
SYSGETCOLOR | - | - | Вызов стандартного диалога выбора цвета (ColorPicker)
SYSGETFONT | - | - | Вызов стандартного диалога выбора фонта
SYSGETOPENNAME | 2 | STRING | Вызывает стандартный диалог "Открыть"
SYSGETSAVENAME | 2 | STRING | Вызывает стандартный диалог "Сохранить как"
SYSHOME | - | - | Дает имя Home-директории
SYSLOGICALDRIVES | - | - | Дает информацию о логических дисках
SYSMEMSTATUS | - | - | Дает информацию о состоянии оперативной памяти
SYSMETRICS | - | - | Дает информацию о системных мериках (в частности, о разрешении экрана)
SYSMKDIR | 1 | STRING | Создает директорию с имененм, заданным значением параметра.
SYSMODE | - | - | Дает режим работы: 1-IDE; 2-EXE; 3-DLL
SYSOSVER | - | - | Дает версию ОС
SYSRENAME | 2 | STRING | Меняет имя у файла/директории, заданного значением ПЕРВОГО параметра на новое имя (заданное значением ВТОРОГО параметра)
SYSRMDIR | 1 | STRING | Удаляет (пустую) директорию с имененм, заданным значением параметра.
SYSSETATTR | 2 | 1-STRING; 2-BITS | Устанавливает байт атрибутов, заданный значением ВТОРОГО параметра у файла с именем, заданным значением ПЕРВОГО аргумента.
SYSSHELL | 1 | STRING | Выполняет заданный файл (ассоциативные действия)
SYSSYSDIR | - | - | Дает имя системной директории
SYSTIME | 1 (необязательный) | STRING | Возвращает системное время, отформатированное в соответствии со строкой-значением параметра. Если параметр опущен - выдается время в стандартном формате.
SYSTMPDIR | - | - | Дает имя временной директории
SYSTMPFILE | 3 | STRING, FIXED, FIXED | Дает имя временного файла
SYSUSERNAME | - | - | Дает имя пользователя
SYSWINDIR | - | - | Дает имя директории WINDOWS
SYSARGC

Функция SYSARGC возвращает количество аргументов командной строки. Если в командной строке аргументы не заданы, функция возвращает нуль. При вызове из среды разработки, функция возвращает количество аргументов конфигурационного параметра "командная строка". При вызове из выполняющегося EXE-файла, функция возвращает число аргументов командной строки, заданное при запуске EXE-файла.

При вызове функции из библиотеки HomeLispLib.Exe результат зависит от способа вызова. Если используется скриптовая оболочка CONLISP или WLISP, то функция вернет количество параметров командной строки. Если пользователь использует библиотеку HomeLispLib.Exe напрямую (создавая объект Lisp и вызывая его методы), то функция SYSARGC вернет нуль.

Предположим, что параметр "командная строка" среды разработки равен -h -k -j -L. При вызове функции SYSARGC будет возвращен результат 4.

SYSARGV

Функция SYSARGV принимает единственный параметр типа FIXED и возвращает аргумент командной строки с номером, равным значению параметра. Если значение аргумента превышает реальное количество параметров командной строки, то функция возвращает Nil.

При вызове функции из выполняющегося EXE-файла с нулевым значение параметра функция возвращает полное имя исполняемого файла. При вызове функции из среды разработки с нулевым значением параметра функция возвращает пустую строку. При вызове функции с нулевым параметром из выполняющегося скрипта (с интерпретатором CONLISP или WLISP) функция возвращает полное имя скриптового файла с соответствующим расширением.

Если значение аргумента меньше нуля, фиксируется состояние ошибки.

Предположим, что параметр "командная строка" среды разработки равен -h -k -j -L. Ниже приводятся примеры вызова функций SYSARGC и SYSARGV.

(for i 1 (sysargc) ((prints (sysargv i)) (terpri)))
-h
-k
-j
-L

==> 4

(sysargv 0)

==> ""
SYSCOMPNAME

Функция SYSCOMPNAME возвращает сетвое имя компьютера, как строку, заключенную в двойные кавычки:

(syscompname)

==> "BOB"
SYSCOPY

Функция SYSCOPY принимает два аргумента - оба типа STRING. Функция выполняет копирование файла, заданного первым аргументом в файл, заданный вторым аргументом. Оба имени могут содержать пути. Если путь не задан, предполагается файл, содержащийся в текущей директории. Если файл скопирован успешно, то функция возвращает нуль. Если исходный файл отсутствует, то выдается сообщение и фиксируется состояние ошибки. Вот примеры вызова Функции SYSCOPY:

(sysCopy "c:\autoexec.bat" "autoexec.bat")

==> T

(sysCopy "c:\autoexex.bat" "autoexex.bat")

sysCopy - файл c:\autoexex.bat не существует!

==> ERRSTATE

Файл "c:\autoexec.bat" копируется в текущую директорию. Копия завершается успешно. А попытка скопировать несуществующий файл вызывает ошибку с соответствующим предупреждением. Имена файлов могут содержать букву диска и директорию (как в приведенном выше примере), так и UNC-обозначение в виде \\Сервер\Ресурс\файл.

SYSDATE

Функция SYSDATE возвращает системную дату. Функция имеет один необязательный аргумент строкового типа. Этот аргумент задает формат выводимой даты. Если аргумент опущен, дата выдается в соответствии с установками пользователя.

Параметр функции (строка формата) может состоять из любых символов. Но следующие группы символов формата интерпретируются особым образом:

dd - день;

mm - месяц;

yy - год (две младшие цифры);

yyyy - все 4 цифры года;

Остальные символы строки формата просто копируются в строку-результат. Вот примеры вызова SYSDATE:

(sysDate "dd-mm-yy")

==> "13-01-09"

(sysDate "mm-yy")

==> "01-09"

(sysDate "dd-yy")

==> "13-09"

(sysDate "dd-mm-yyyy")

==> "13-01-2009"

(sysDate "Дата: dd-mm-yyyy")

==> "Дата: 13-01-2009"

(sysDate)

==> "01-13-2009"

По последнему примеру можно убедиться, что при вызове функции без строки формата результат форматируется в соответствии с системными установками пользователя. В предпоследнем примере в строку формата внесен текст "Дата:"; видно, что этот текст перенесен в результат без преобразования. Из примеров видно также, что можно задавать не все элементы формата. В этом случае возвращается часть даты (в соответствии с заданными элементами).

SYSDIR

Функция SYSDIR принимает два обязательных параметра: шаблон поиска файлов (строкового типа) и однобайтовую битовую шкалу атрибутов. Функция возвращает список имен файлов (или поддиректорий), удовлетворяющих шаблону поиска и имеющих атрибуты, заданные битовой шкалой.

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

1) Если путь не задан - составляется список файлов из текущей директории;

2) Символы ".\" означают ссылку на текущую директорию;

3) Символы "..\" означают ссылку на наддиректорию (директорию, расположенную уровнем выше текущей в дереве директорий).

Атрибуты в битовой шкале имеют следующий смысл:

Атрибут | Бит
Normal | &H00
Read Only | &H01
Hidden | &H02
System | &H04
Volume | &H08
Directory | &H10
Archive | &H20

Атрибуты можно комбинировать. Так шкала атрибутов &H06 обеспечит отбор файлов, у которых поднят Hidden или System. Важное замечание: Если в шкале атрибутов поднят бит директории (&H10), а сама шкала отлична от &H3F, то функция вернет список директорий.

Вот примеры вызова SYSDIR:

(sysDir "c:\*.txt" &H3F)

==> ("BOOTLOG.TXT" "FRUNLOG.TXT" "SETUPLOG.TXT" "NETLOG.TXT" "DETLOG.TXT"
     "SETUPXLG.TXT" "regdlls.txt")

(sysDir "c:\" &H02)

==> ("BOOTLOG.TXT" "BOOTLOG.PRV" "SUHDLOG.DAT" "MSDOS.---" "SYSTEM.1ST"
     "ffastun.ffl" "ffastun0.ffx" "ffastun.ffo" "ffastun.ffa" "HIGHVIEW.INI"
     "HIGHVIEW.HIS" "ASD.LOG")

(sysDir "c:\" &H06)

==> ("IO.SYS" "MSDOS.SYS" "BOOTLOG.TXT" "BOOTLOG.PRV" "SUHDLOG.DAT" "MSDOS.---"
     "DETLOG.TXT" "SYSTEM.1ST" "DETLOG.OLD" "ffastun.ffl" "ffastun0.ffx"
     "ffastun.ffo" "ffastun.ffa" "WINLFN.INI" "WINLFN1.INI" "HIGHVIEW.INI"
     "HIGHVIEW.HIS" "ASD.LOG")

(sysDir "c:\" &H10)

==> ("WINDOWS" "YAMAHA" "Мои документы" "Program Files" "Dell" "Inetpub" "MSSQL7"
     "HPFONTS" "DivX3.11" "Temp" "My Intranet" "php" "Tmp")

Здесь при первом вызове функции SYSDIR показываются все текстовые файлы в корневой директории диска c:.

Второй вызов дает все скрытые файлы в корневой директории диска c:.

Третий вызов дает список всех файлов, имеющих атрибуты Hidden и System.

Последний вызов дает список директорий диска c:.

SYSDISKSPACE

Функция SYSDISKSPACE принимает один обязательный параметр: имя диска (строкового типа). Если запрошенный диск существует, то функция возвращает список из трех чисел с плавающей точкой. Если же диск не существует, функция возвращает Nil.

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

Второе число списка-результата - это общее количество байтов диска.

Третье число списка-результата - это количество свободных байтов диска.

Вот примеры вызова SYSDISKSPACE:

(sysDiskSpace "t:")

==> NIL

(sysDiskSpace "a:")

==> (496128.0 1457664.0 496128.0)

(sysDiskSpace "z:")

==> (6289096704.0 9663676416.0 6289096704.0)

(/ (cadr (sysDiskSpace "c:")) (* 1024.0 1024.0 1024.0))

==> 9.75741577148438

Видно, что диска T: на компьютере нет; на диске a: свободно около 500 кБ, а все свободное пространство диска z: доступно текущему пользователю.

Последний вызов выделяет общую емкость диска (посредством вызова cadr), и переводит его в гигабайты делением на 10243. Получается, что диск c: имеет емкость ~ 9.76 Гб.

SYSDRIVETYPE

Функция SYSDRIVETYPE принимает один обязательный параметр: имя любой директории диска (строкового типа) и возвращает тип диска в виде целого числа. Если функция возвращает нуль, значит произошла ошибка. Возврат единицы означает, что запрошенная директория не существует.

Возможные значения типа диска приводятся ниже:

Тип диска | Характеристика
2 | Сменный диск (дискета)
3 | Жесткий диск
4 | Сетевой диск
5 | Компакт-диск
6 | Электронный диск (RAM-drive)

Вот примеры вызова SYSDRIVETYPE:

(sysDriveType "a:\")

==> 2

(sysDriveType "i:\")

==> 2

(sysDriveType "e:\")

==> 3

(sysDriveType "c:\")

==> 3

(sysDriveType "z:\")

==> 4

(sysDriveType "q:\")

==> 5

Из приведенных результатов видно, что диски a: и i: - сменные; диски c: и e: - жесткие; диск z: - сетевой, а диск q: - компакт-диск.

SYSENV

Функция SYSENV принимает один обязательный параметр: номер переменной окружения (типа FIXED, в диапазоне от 1 до 100) или имя переменной окружения (типа STRING). Если переменная окружения с заданным номером или именем не существует, функция возвращает Nil, а если существует - то в случае задания номера возвращается вся строка окружения в виде:

Имя=значение

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

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

(sysEnv 1)

==> "TMP=c:\windows\TEMP"

(sysEnv 2)

==> "TEMP=C:\windows\TEMP"

(sysEnv 3)

==> "winbootdir=C:\WINDOWS"

(sysEnv "winbootdir")

==> "C:\WINDOWS"

(sysEnv 0)

У функции sysEnv аргумент за пределами диапазона [1,100]
==> ERRSTATE

(sysEnv 40)

==> NIL

Следует обратить внимание на то, что при запросе третьей переменной окружения, функция возвращает строку вида Имя=Значение, а при запросе значения переменной окружения winbootdir возвращается только значение. Также, можно отметить, что переменной окружения с номером 40 на компьютере автора не существует.

Возникает естественный вопрос: как узнать число переменных окружения? В этом может помочь, например, приводимая функция numEnvVar:

(defun nEnvVar Nil

               (prog (i)
                     (setq i 1)
               @     (cond ((null (sysEnv i)) (return (- i 1))))
                     (setq i (+ 1 i))
                     (go @)
               )
)

==> nEnvVar

(nEnvVar)

==> 12

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

SYSERASE

Функция SYSERASE принимает один обязательный параметр строкового типа - имя удаляемого файла. Если заданный файл существует и у пользователя есть права на его удаления, то файл будет удален, а функция вернет T. В противном случае возбуждается состояние ошибки и выдается сообщение с кодом ошибки. Имя файла может содержать диск и путь. Если задается "чистое" имя файла, то будет сделана попытка удалить файл из текущей директории.

Имя файла может содержать метасимволы * и ?. Если заданы метасимволы, то удаляются все файлы, имена которых удовлетворяют заданному шаблону. Так, если имя удаляемого файла имеет вид путь\*.*, то будут удалены все файлы. Излишне напоминать, что это делает функцию SYSERASE крайне опасной в неумелых руках!

Предположим, что в текущей директории имеется поддиректория с именем 111, и что в этой директории находятся только файлы типа .jpg и .ico. Тогда приводимые ниже две команды удалят из этой директории сначала все иконки (.ico), а затем - все картинки (.jpg). Следующая команда делает попытку очистить уже пустую директорию; естественно она вызывает ошибку.

(sysErase ".\111\*.ico")

==> T

(sysErase ".\111\*.jpg")

==> T

(sysErase ".\111\*.*")

sysErase - ошибка. Код=53
==> ERRSTATE
SYSEXEC

Функция SYSEXEC позволяет запускать приложения Windows (как 16-битные, так и 32-х битные) и дожидаться их завершения. Функция ожидает три параметра:

1) Имя исполняемого файла приложения (типа STRING);

2) Командную строку (типа STRING);

3) Статус окна приложения (типа FIXED).

Имя исполняемого файла может содержать букву диска и путь. Если задано "чистое" имя файла, то этот файл ищется сначала в текущей директории, а затем во всех директориях, доступных через переменную окружения Path. Если файл приложения не найден ни в одной директории, то генерируется ошибка.

Статус окна приложения имеет смысл употреблять при запуске 16-битных (DOS-овых) или 32-битовых консольных приложений. Этот параметр может принимать такие значения:

Значение параметра | Характеристика
0 | Скрытое окно с фокусом
1 | Нормальное окно с фокусом
2 | Минимизированное окно с фокусом
3 | Максимизированное окно с фокусом
4 | Нормальное окно без фокуса
6 | Минимизированное окно без фокуса

Ниже приводится пример вызова архиватора "pkzip.exe" для упаковки в текущей директории всех файлов с расширением ".bas" в архив all_bas.zip:

(sysExec "pkzip.exe" "all_bas.zip *.bas" 1)

==> 0

В процессе работы пользователь увидит окно следующего вида:

Чтобы после завершения 16-битного приложения его окно автоматически убиралось с экрана, рекомендуется создать для запускаемого приложения pif-файл и устновить свойство автоматически закрывать поcле завершения работы.

SYSGETATTR

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

1) Длина файла (для директории нуль)

2) Дата и время создания в виде "дд.мм.гггг чч:мм:сс"

3) Байт атрибутов в виде битовой константы. Биты байта атрибутов описаны в разделе, посвященном функции SYSDIR.

Если заданный файл или директория отсутствуют, то функция возвратиь Nil.

Вот примеры вызова функции SYSGETATTR:

(sysgetattr "c:\windows")

==> (0 "01.11.2001 10:43:08" &H10)

(sysgetattr "c:\файла.нет")

==> NIL

(sysgetattr "c:\autoexec.bat")

==> (1095 "04.06.2008 21:13:44" &H20)

В первом случае возвращаются сведения о директории C:\Windows. Видно, что C:\Windows - именно директория, поскольку в байте атрибутов поднят бит признака директории. Попытка запросить атрибуты несуществующего файла дает результат Nil. И, наконец, приводятся сведения о реально существующем файле c:\autoexec.bat.

SYSGETCOLOR

Функция SYSGETCOLOR не принимает параметров. Назначение этой функции - вызов стандартного диалога выбора цвета. Функция возвращает выбранный пользователем цвет в виде битовой RGB-константы или Nil (в случае, когда пользователь нажал кнопку Отказаться).

Диалог выбора цвета выглядит так:

Вот примеры вызова функции SYSGETCOLOR:

(sysGetColor)

==> NIL

(sysGetColor)

==> &HFF0000

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

SYSGETFONT

Функция SYSGETFONT не принимает параметров. Назначение этой функции - вызов стандартного диалога выбора шрифта. Функция возвращает выбранный пользователем шрифт в виде списка свойств или Nil (в случае, когда пользователь нажал кнопку Отказаться).

Список свойств шрифта состоит из четырех элементов: имени шрифта, размера шрифта в пунктах и двух логических значений T/Nil. Первое из них отвечает за жирное начертание (Bold) или его отсутствие. Второе значение отвечает за курсивное начертание.

Диалог выбора шрифта выглядит так:

Вот примеры вызова функции SYSGETFONT:

(sysGetFont)

==> NIL

(sysGetFont)

==> ("Arial" 14 T T)

Сначала пользователь вызвал диалог выбора шрифта и отказался от выбора. Во втором случае он выбрал шрифт Arial размером 14 пунктов жирного и курсивного начертания. Назначение функции состоит в возможности диалогового выбора пользователем нужного шрифта во время выполнения программы.

SYSGETOPENNAME

Назначение этой функции - вызов стандартного диалога OPEN (Открыть). Функция SYSGETOPENNAME принимает два обязательных параметра и один необязательный. Первый параметр - имя начальной директории, содержимое которой будет отображаться. Второй параметр задает фильтр имен файлов. Третий (необязательный) параметр задает заголовок окна диалога. Следует обратить внимание: функция не открывает выбранный файл, а только возвращает его имя.

Фильтр имеет следующую внутреннюю структуру:

Описание типа|шаблон типа|Описание типа|шаблон типа|...

Описание типа представляет собой произвольную строку (например, "Такстовые файлы") Шаблон типа задает шаблон поиска файлов в директории (например, *.txt).

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

Диалог "открыть" выглядит так:

Вот примеры вызова функции SYSGETOPENNAME:

(sysGetOpenName "c:\Windows" "Драйверы Windows|*.drv|Текстовые файлы|*.txt")

==> "C:\WINDOWS\ErrorLog.txt"

(sysGetOpenName "c:\Windows" "Драйверы Windows|*.drv|Текстовые файлы|*.txt")

==> NIL

Здесь видно, что пользователь выбрал диалог "Открыть" с двумя фильтрами "*.txt" и "*.drv". В процессе диалога был выбран файл "C:\WINDOWS\ErrorLog.txt". Во втором случае пользователь выбрал тот же набор фильтров, но не отказался от выбора файла.

SYSGETSAVENAME

Назначение этой функции - вызов стандартного диалога SAVE AS (Сохранить как). Функция SYSGETSAVENAME принимает два обязательных параметра и один необязательный. Первый параметр - имя начальной директории, содержимое которой будет отображаться. Второй параметр задает фильтр имен файлов. Третий (необязательный) параметр задает заголовок окна диалога.

Фильтр имеет следующую внутреннюю структуру:

Описание типа|шаблон типа|Описание типа|шаблон типа|...

Описание типа представляет собой произвольную строку (например, "Такстовые файлы") Шаблон типа задает шаблон поиска файлов в директории (например, *.txt).

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

Диалог "Сохранить как" выглядит так:

Вот примеры вызова функции SYSGETSAVENAME:

(sysGetSaveName (sysHome) "Текст|*.txt|Лисп|*.lsp" "Выбор файла для сохранения")

==> "D:\Vb5ee\MYSOURCE\Samples.My\Lisp011\W.lsp"

(sysGetSaveName (sysHome) "Текст|*.txt|Лисп|*.lsp" "Выбор файла для сохранения")

==> NIL

Здесь видно, что пользователь выбрал диалог "Сохранить как" с двумя фильтрами "*.txt" и "*.lsp". В процессе диалога был выбран файл "W.lsp". Во втором случае пользователь выбрал тот же набор фильтров, но не отказался от выбора файла.

SYSHOME

Функция SYSHOME не имеет параметров. Вызов этой функции возвращает имя текущей директории. Вот пример вызова функции SYSHOME:

(sysHome)

==> "D:\VB5EE\MYSOURCE\SAMPLES.MY\LISP011"
SYSLOGICALDRIVES

Функция SYSLOGICALDRIVES не имеет параметров. Вызов этой функции возвращает битовую шкалу, в которой поднятый бит с номером n означает наличие логического диска с буквой из позиции n в последовательности ABCDEFGHIJKLMNOPQRSTUVWXYZ. Так, возврат шкалы &H5 означает наличие дисков A: и С:, поскольку в двоичном виде шкала &H5 имеет вид 00000101. Нумерация битов в шкале идет от младших разрядов к старшим (справа налево).

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

(defun dskList nil
   (prog (Dsk i Ldsk mask)
         (setq Ldsk "")
         (setq Dsk (sysLogicalDrives))
         (for i 0 25
            (
              (setq mask (fix2bit (^ 2 i)))
              (setq Ldsk
                    (strCat Ldsk
                            (cond
                              ((neq (logand mask Dsk) &H0) (strCat (strChr (+ 65  i)) ": " ))
                              (t "")
                            )
                    )
              )
            )
         )
         (return Ldsk)
   )
)

 ==> dskList

(dsklist)

 ==> "A: C: D: E: F: G: H: K: L: M: Q: W: X: Z: "
SYSMEMSTATUS

               Функция SYSMEMSTATUS не имеет параметров. Вызов этой функции возвращает
               список из семи чисел, означающих следующее:

1) Процент использования оперативной памяти;

2) Общий размер физической памяти в байтах;

3) Доступный размер физической памяти в байтах;

4) Общий размер страничного файла в байтах;

5) Доступный размер страничного файла в байтах;

6) Общий размер виртуальной памяти в байтах;

7) Доступный размер виртуальной памяти в байтах;

Ниже приводится пример вызова функции SYSMEMSTATUS:

(sysMemstatus)

==> (100.0 132612096 4096 2014867456 1833775104 2143289344 1986723840)
SYSMETRICS

Функция SYSMETRICS принимает один параметр типа FIXED и, в зависимости от значения параметра, возвращает разнообразные системные метрики. Ниже приводятся допустимые значения параметров и возвращаемые результаты. Эти значения взяты из хелпа по Windows API ("Русский проект"; автор А. Климов)

Значение константы | Результат
0 | Возвращает ширину экрана.
1 | Возвращает высоту экрана.
2 | Начало позиции, помещающее значки в верхнем левом углу экрана.
2 | только для Win 95/98: Возвращает ширину вертикальной полосы прокрутки.
3 | Начало позиции, помещающее значки в верхнем правом углу экрана.
3 | только для Win 95/98: Возвращает высоту горизонтальной полосы прокрутки.
4 | Возвращает высоту нормальной области надписи.
5 | Win NT: Возвращает ширину границы окна. Win 95/98: Возвратите ширину отдельной границы окна.
6 | Win NT: Возвращает высоту границы окна. Win 95/98: Возвращает высоту отдельной границы окна.
7 | только для Win NT: Возвращает ширину рамки окна, имеющей стиль диалогового окна.
8 | Не размещает значки на экране (то есть, скрывает их).
8 | только для Win NT: Возвращает высоту рамки окна, имеющей стиль диалога.
9 | Возвращает высоту вертикального бегунка полосы прокрутки.
10 | Возвращает ширину горизонтального бегунка полосы прокрутки.
11 | Возвращает заданную по умолчанию ширину значка.
12 | Возвращает заданную по умолчанию высоту значка.
13 | Win NT: Возвращает ширину курсора. Win 95/98: Возвращает ширину стандартного точечного рисунка курсора.
14 | Win NT: Возвращает высоту курсора. Win 95/98: Возвращает высоту стандартного точечного рисунка курсора.
15 | Возвращает высоту отдельной строки меню.
16 | Возвращает ширину клиентской области полноэкранного окна.
17 | Возвращает высоту клиентской области полноэкранного окна.
18 | Возвращает высоту окна Kanji (для версий набора символов с двойным байтом Windows).
19 | Возвращает ненулевое значение, если мышь установлена и присутствует, 0 если нет.
20 | только для Win 95/98: Возвращает высоту точечного рисунка стрелки на вертикальной полосе прокрутки.
21 | только для Win 95/98: Возвращает ширину точечного рисунка стрелки на горизонтальной полосе прокрутки.
22 | Возвращает ненулевое значение, если версия отладки USER.EXE установлена, 0 если нет.
23 | Возвращает ненулевое значение, если левые и правые кнопки мыши поменялись, 0 если нет.
28 | Возвращает минимальную ширину окна.
29 | Возвращает минимальную высоту окна.
30 | Win NT: Возвращает ширину точечного рисунка в заголовке. Win 95/98: Возвращает ширину кнопки заголовка.
31 | Win NT: Возвращает высоту точечного рисунка области заголовка. Win 95/98: Возвращает высоту надписи кнопки.
32 | только для Win NT: Возвращает ширину границы окна изменяемого размера.
32 | только для Win 95/98: Возвращает ширину толстой рамки окна.
33 | только для Win NT: Возвращает высоту границы окна изменяемого размера.
33 | только для Win 95/98: Возвращает высоту толстой рамки окна.
34 | только для Win 95/98: Возвращает заданную по умолчанию минимальную ширину
35 | только для Win 95/98: Возвращает заданную по умолчанию минимальную высоту, если пользователю позволено изменять размеры окна.
36 | Ширина прямоугольника, в пределах которого оба щелчка мыши должны быть опознаны,как двойной щелчок.
37 | Возвращает высоту прямоугольника, в пределах которого оба щелчка мыши должны быть опознаны как двойной щелчок.
38 | Win NT: Возвращает ширину ячейки для позиционирования значков. Win 95/98: Возвращает ширину ячейки сетки для элементов в виде крупных значков.
39 | Win NT: Возвращает высоту ячейки для позиционирования значков. Win 95/98: Возвращает высоту ячейки сетки для элементов в представлении в виде крупных значков.
40 | Возвращает ненулевое значение, если всплывающие меню появляются справа, 0 если слева.
41 | Return a non-zero value if the Microsoft Windows for Pen computing extensions are installed, 0 if not.
42 | Возвращает ненулевое значение, если версия набора символов с двойным байтом USER.EXE установлена, 0 если нет.
43 | Возвращает число кнопок на установленной мыши, или 0, если мышь не установлена.
44 | только для Win 95/98: Возвращает количество параметров системы.
44 | Возвращает ненулевое значение, если защита присутствует и активна, 0 если нет.
45 | только для Win 95/98: Возвращает ширину 3D границы окна.
46 | только для Win 95/98: Возвращает высоту трехмерной границы окна.
47 | только для Win 95/98: Возвращает ширину прямоугольника ячейки сетки используя позицию свернутого окна.
48 | только для Win 95/98: Возвращает высоту прямоугольника ячейки сетки используемой для позиционирования свернутых окон.
49 | только для Win 95/98: Возвращает рекомендуемую ширину для маленьких значков.
50 | только для Win 95/98: Возвращает рекомендуемую высоту для маленьких значков.
51 | только для Win 95/98: Возвращает высоту маленькой области заголовка.
52 | только для Win 95/98: Возвращает ширину маленькой надписи кнопки.
53 | только для Win 95/98: Возвращает высоту кнопки маленькой надписи.
54 | только для Win 95/98: Возвращает ширину кнопки строки меню.
55 | только для Win 95/98: Возвращает высоту кнопки строки меню.
56 | только для Win 95/98: Возвращает метод отображения свернутых окон. Возвращаемое значение - комбинация двух из следующих флажков, одно определяет исходную позицию для свернутых значков и другое определяет направление, в котором новые значки будут добавлены:
57 | только для Win 95/98: Возвращает ширину нормально свернутого окна.
58 | только для Win 95/98: Возвращает высоту нормально свернутого окна.
59 | только для Win 95/98: Возвращает заданную по умолчанию максимальную ширину,если пользователю разрешено изменять размеры окна.
60 | только для Win 95/98: Возвращает заданную по умолчанию максимальную высоту,если пользователю позволено изменять размеры окна.
61 | только для Win 95/98: Возвращает заданную по умолчанию ширину развернутого окна.
62 | только для Win 95/98: Возвращает заданную по умолчанию высоту развернутого окна.
63 | Установка &H1 бит возвращаемого значения, если сеть установлена. Все другие биты возвращаемого значения зарезервированы и неопределенны.
67 | только для Win 95/98: Возвращает значение, определяющее, как компьютер был загружен. 0 - normal bootup, 1- fail-safe (a.k.a. SafeBoot) bootup, и 2 значит a fail-safe bootup with the network installed.
68 | Возвращает минимальную ширину,при которой курсор должен двигаться, чтобы начать операцию перетаскивания.
69 | Возвращает минимальную высоту,на которую курсор должен сдвинуться, чтобы начать операцию перетаскивания.
70 | Возвращает ненулевое значение, если приложение показывает визуальную команду вызова программы для всех звуков, 0 если нет.
71 | только для Win 95/98: Возвращает ширину заданного по умолчанию точечного рисунка метки меню.
72 | только для Win 95/98: Возвращает высоту заданного по умолчанию точечного рисунка метки меню.
73 | Возвращает ненулевое значение, если система имеет медленный процессор, 0 если нет.
74 | Возвращает ненулевое значение, если система использует еврейский и арабские языки, 0 если нет.

Ниже приводится пример вызова функции SYSMETRICS для определения горизонтального и вертикального разрешения экран:

(sysMetrics 0)

==> 1024

(sysMetrics 1)

==> 768
SYSMKDIR

Функция SYSMKDIR позволяет создать директорию. Она принимает имя создаваемой директории в виде значения типа STRING. Имя создаваемой директории может содержать букву диска и путь. Если имя не содержит пути, то делается попытка создать директорию, как поддиректорию текущей директории. При успешном создании директории функция возвращает T.

Ниже приводится пример вызова функции SYSMKDIR:

(sysMkdir "zzz")

==> T

(sysMkdir "zzz")

sysMkDir - ошибка. Код=75
==> ERRSTATE

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

SYSMODE

Функция SYSMODE позволяет программе распознать режим выполнения. Функция не имеет параметров. При вызове функции из интегрированной среды разработки, результат будет 1. При вызове функции из EXE-файла результат будет 2. А если вызвать функцию SYSMODE из кода, который использует библиотеку HomeLispLib.Exe, то результат будет 3.

SYSOSVER

Функция SYSOSVER возвращает версию операционной системы в виде строки следующего вида:

"IdP/MajorV/MinorV Доп.Инф."

Здесь IdP - т.н. идентификатор платформы, принимающий значения 001 - Win-9x; 002 - Win-NT (а также - Win2k, WinXp).

MajorV - основной номер версии;

MinorV - дополнительный номер версии;

Доп.Инф. - дополнительная информация об операционной системе (в т.ч. версии сервиспаков);

SYSRENAME

Функция SYSRENAME позволяет переименовывать файлы или директории. Функция принимает два параметра типа STRING. Первый параметр задает исходное имя, второй - целевое имя переименовываемого файла или директории. Если переименование прошло успешно - функция возвращает T.

Ниже приводится пример вызова функции SYSRENAME:

(sysMkdir "zzz")

==> T

(sysRename "zzz" "qqq")

==> T

(sysRename "aaaaaaaa.aaa" "bbbbbbbb.bbb")

==> T

(sysRename "bbbbbbbb.bbb" "d:\aaaaaaaa.aaa")

==> T

(sysDir ".\*.bbb" &H3F)

==> NIL

(sysDir "d:\*.aaa" &H3F)

==> ("aaaaaaaa.aaa")

В текущей директории создается поддиректория Zzz, которая затем переименовывается в Qqq. Затем существующий в текущей директории файл Aaaaaaaa.aaa переименовывается в Bbbbbbbb.bbb. Следующая команда переименовывает файл Bbbbbbbb.bbb в d:\Aaaaaaaa.aaa. При этом файл исчезает из текущей директории и оказывается в корне диска d: (что показывают последующие вызовы SysDir). Автор обращает внимание на то, что команда SysRename позволяет при переименовании менять букву целевого логического диска.

SYSRMDIR

Функция SYSRMDIR позволяет удалить пустую директорию. Функция принимает единственный параметр типа STRING - имя удаляемой директории. Если переименование прошло успешно - функция возвращает T.

Ниже приводится пример вызова функции SYSRMDIR:

(sysMkdir "qqq")

==> T

(sysDir "qqq\*.*" &H3F)

==> (NIL)

(sysRmDir "qqq")

==> T

(sysRmDir "qqq")

sysRmDir - директория qqq не существует
==> ERRSTATE

(sysDir "Coll\*.*" &H3F)

==> ("P01" "P02" "P03" "P04")

(sysRmDir "Coll")

sysRmDir - ошибка. Код=75
==> ERRSTATE

В текущей директории создается поддиректория Qqq, которая затем успешно удаляется, поскольку является пустой. Повторная попытка удалить поддиректорию Qqq вызывает ошибку. Ошибку вызывает и попытка удалить непустую директорию Coll.

SYSSETATTR

Функция SYSSETATTR позволяет установить или сбросить у файла или директории атрибуты элемента оглавления. Биты байта атрибутов описаны в разделе, посвященном функции SYSDIR. Функция требует два параметра: имя файла/директории (типа STRING) и новое значение байта атрибутов (типа BITS). Если функция завершается успешно, она возвращает T. В противном случае возбуждается сообщение об ошибке и выдается соответствующее сообщение. Функция SYSSETATTR, разумеется, не позволит установить у файла бит директории или метки тома. А вот установить у директории бит hidden вполне возможно.

Ниже приводится пример вызова функции SYSRMDIR:

(sysSetAttr "aaaaaaaa.aaa" &H06)

==> T

(sysSetAttr "aaaaaaaa.aaa" &H0)

==> T

(sysSetAttr "aaaaaaaa.aaa" &H10)

sysSetAttr - ошибка при установке атрибутов Код=5
==> ERRSTATE

У файла aaaaaaaa.aaa устанавливаются атрибуты скрытый системный. Потом эти биты сбрасываются. Обе операции завершаются успешно. А попытка установить бит директории у файла вызывает ошибку.

SYSSHELL

Функция SYSSHELL принимает единственный параметр строкового типа - имя файла. Функция выполняет для заданного файла действия, предусмотренные расширением файла. Так, для расширения .doc запускается Microsoft Word и заданный файл загружается в окно документа. Для расширения .exe делается попытка выполнить заданный файл и т.д. Если действия удалось выполнить успешно, функция возвращает T. В случае возникновения ошибки функция возвращает код ошибки в соответствии с прилагаемой таблицей:

Код завершения | Причина
2 | Файл не найден
3 | Путь не найден
5 | Доступ запрещен
8 | Не хватает памяти
11 | Неверный формат EXE-файла или испорчен образ EXE-файла в памяти
26 | Произошло нарушение защиты для разделяемого ресурса
32 | DLL не найдена
27 | Неполная или неверная ассоциация для файла
28 | Истек тайм-аут у сервера DDE
29 | Неудачное завершение DDE-транзакции
30 | Сервер DDE занят
31 | Для расширения не найдено ассоциации
SYSSYSDIR

Функция SYSSYSDIR не требует параметров и возвращает имя системной директории:

(syssysdir)

==> "C:\WINDOWS"
SYSTIME

Функция SYSTIME возвращает системное время. Функция имеет один необязательный аргумент строкового типа. Этот аргумент задает формат выводимого времени. Если аргумент опущен, время выдается в соответствии с установками пользователя.

Параметр функции (строка формата) может состоять из любых символов. Но следующие группы символов формата интерпретируются особым образом:

hh - часы;

mm - минуты;

ss - секунды;

Остальные символы строки формата просто копируются в строку-результат. Вот примеры вызова SYSTIME:

(systime)

==> "15:17:41"

(systime "hh:mm")

==> "15:17"

(systime "mm:hh")

==> "12:15"

(systime "ss")

==> "38"

(systime "Время hh:mm:ss")

==> "Время 15:18:12"

По последнему примеру можно убедиться, что при вызове функции без строки формата результат форматируется в соответствии с системными установками пользователя. В предпоследнем примере в строку формата внесен текст "Время:"; видно, что этот текст перенесен в результат без преобразования. Из примеров видно также, что можно задавать не все элементы формата. В этом случае возвращается часть времени (в соответствии с заданными элементами).

SYSTMPDIR

Функция SYSTMPDIR не требует параметров и возвращает имя временной директории:

(systmpdir)

==> "c:\windows\TEMP\"
SYSTMPFILE

Функция SYSTMPFILE позволяет построить имя временного файла в заданной директории.

Функция требует трех обязательных параметров. Первый параметр (типа STRING) задает имя директории, в которой будет создаваться временый файл. Второй параметр (тоже типа STRING)задает не более чем трехсивольный префикс имени файла (если задать более трех символов, то эффект окажут первые три). Третий параметр типа FIXED имеет следующий смысл:

Если значение параметра равно нулю, то строится временное имя и файл с этим именем создается в заданной директории. При этом гарантируется уникальность имени файла.

Если же значение параметра не равно нулю, то это значение переводится в шестадцатеричную систему и используется в качестве суффикса имени файла. Сам файл при этом не создается.

Ниже приводятся примеры вызова функции SYSTMPFILE.

(sysTmpFile (sysHome) "qwe" 45)

==> "D:\Vb5ee\MYSOURCE\Samples.My\Lisp011\qwe2D.TMP"

(sysTmpFile (sysHome) "qwe" 0)

==> "D:\Vb5ee\MYSOURCE\Samples.My\Lisp011\qweE335.TMP"

(sysTmpFile (sysHome) "ZXCVBN" 0)

==> "D:\Vb5ee\MYSOURCE\Samples.My\Lisp011\ZXC7203.TMP"

(sysTmpFile (sysHome) "Z" 0)

==> "D:\Vb5ee\MYSOURCE\Samples.My\Lisp011\Z8204.TMP"

(sysTmpFile (sysHome) "qwe" 45)

==> "D:\Vb5ee\MYSOURCE\Samples.My\Lisp011\qwe2D.TMP"

(sysDir (strCat (sysHome) "\*.tmp") &H3F)

==> ("ZXC7203.TMP" "Z8204.TMP" "QWEE335.TMP")

Следует обратить внимание на первый и последний вызовы: строится одинаковое имя файла (а сам файл не создается; последний вызов sysDir показывает, что созданы только файлы при нулевом значении третьего параметра). Кстати, 4510=2D16.

SYSUSERNAME

Функция SYSUSERNAME не требует параметров и возвращает имя пользователя:

(sysusername)

==> "blfaifel"
SYSWINDIR

Функция SYSWINDIR не требует параметров и возвращает имя системной директории Windows:

(syswindir)

==> "C:\WINDOWS"


Описание функций работы с таймером

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

Функций, предназначенной для работы с таймером, всего три. Имена этих функций начинаются префиксом tim. При работе WEB-компоненты функции недоступны.

Имя функции | К-во аргументов | Тип аргументов | Выполняемое действие
TIMSTART | 2 | 1 - ANY; 2 - FIXED | Запускает таймер. При успехе возвращает T.
TIMSTOP | - | - | Останавливает таймер.
TIMACTIVEP | - | - | Проверяет, запущен ли таймер. При запущенном таймере возвращает T, в противном случае возвращает Nil.
TIMSTART

Функция TIMSTART принимает два аргумента. Первый аргумент есть произвольное S-выражение. Значением второго должно быть целое число (не превышаещее 231), которое задает длину интервала в миллисекундах. При успехе функция возвращает T. Начиная с этого момента, S-выражение, заданное первым аргументом будет периодически вычисляться. Результат вычисления пропадает.

Рассмотрим пример:

(defun prt () (printsline "я здесь!"))

==> prt

(timStart '(prt) 3000)

==> T

я здесь!
я здесь!
я здесь!

Здесь создана функция, которая просто печатает строку "я здесь!". Далее запускается таймер с интервалом 3 сек. Если запустить этот код в IDE, то строка "я здесь!" будет каждые 3 сек появляться в области вывода. Пользователь может параллельно с работой таймера выполнять какие-либо действия.

Этот пример не удастся выполнить в консольном режиме, что обусловлено спецификой взаимодействия ActiveX-библиотеки HomeLispLib.exe с консольным движком Conlisp.exe. Но это не означает, что таймером нельзя пользоваться в консольном режиме. Рассмотрим следующий пример:

Здесь сначала создаются два замыкания с общим лексическим контекстом - функциям next и reset доступна общая лексическая переменная c. При каждом вызове next значение c увеличивается на единицу. Функция next вызывается посредством таймера каждые 2 сек. Затем результат вызова timActivep показывает, что таймер активен. Для того, чтобы убедиться, что параллельно с работой таймера можно выполнять команды, вычисляется значение 100!. Далее, явный вызов next подтверждает, что таймер "тикал" все это время - значение c оказывается равным 15 (Разумеется, 100! вычислялся не 30 сек, просто автор нарочно немного подождал...)

TIMSTOP

Функция TIMSTOP не требует параметров. Если таймер был запущен, вызов функции остановит его. В противном случае выполнение функции ничего не меняет в сосотоянии Лисп-машины. Функция всегда возвращает T.

TIMACTIVEP

Функция-предикат TIMACTIVEP не требует параметров. Если таймер был запущен, функция вернет T, в противном случае функция вернет Nil.


Web-компонента

HomeLispLib и WEB

В начале работы над проектом автор обнаружил в Интернете любопытный материал [8]. В нем описывалась реализация Лиспа для целей обучения и планировалось ее использование как в виде настольного приложения, так и в виде ActiveX-компонента, выполняемого на WEB-странице. Тогда у автора возникла мысль о том, чтобы попытаться запустить HomeLisp как серверное WEB-приложение. Запускать HomeLisp как клиентское приложение браузера представляется автору, мягко говоря, не очень плодотворным... А вот используя HomeLisp как серверное WEB-приложение, можно построить учебный класс, в котором на клиентских рабочих местах используются маломощные компьютеры, а все вычисления происходят на сервере. На нем же и инсталлируется HomeLisp в виде WEB-компоненты.

О г л а в л е н и е
Что такое Web-компонента и как она работает.
Настройка Web-компоненты.
Интерфейс конечного пользователя при работе с Web-компонентой.
Дополнительные возможности авторизованного пользователя.
Завершение сеанса.
Администрирование Web-компоненты.
Как устроено рабочее пространство пользователя.
Сообщения об ошибках Web-компоненты.
Что такое WEB-компонента и как она работает.

Первоначально разработчик полагал, что наиболее простым решением будет создание библиотеки ActiveX-Dll, включающей всю необходимую функциональность HomeLisp. Из этой библиотеки предполагалось создавать объект в серверном скрипте (с использованием функции CreateObject) и вызывать из скрипта свойства и методы этого объекта.

Задача оказалась заметно сложнее, чем вначале показалось автору. Дело в том, что HomeLisp весьма требователен к ресурсам процессора и, особенно, к стековому пространству (рекурсия, однако...). Один объект создавать еще кое-как удавалось, а вот со вторым уже возникали проблемы - ведь WEB-сервер должен в основном не вычислять, а отвечать на запросы пользователей. А что произойдет, если программа пользователя зациклится (для обучающихся Лиспу - ситуация нередкая)? Зациклившаяся программа одного пользователя завесит весь WEB-сервер, что абсолютно недопустимо.

Короче говоря, cтало очевидно, что HomeLisp нужно запускать не в простанстве WEB-сервера, а как самостоятельный процесс в отдельном адресном простанстве. В принципе, это вполне резонный подход, - именно так до сих пор работают т.н. CGI-программы. Но и здесь разработчика подстерегала проблема...

Классическая CGI-программа работает так: получив входные параметры (из переменых окружения или стандартного ввода), программа выполняет обработку и завершается. Следующий вызов программы вновь начинается "с чистого листа". Описанная ситуация является печальным следствием особенности протокола HTTP (протокол без сохранения состояния). Обычно этот очевидный недостаток HTTP-протокола исправляется посредством файлов COOKIES (которые передаются в заголовке http-запроса и хранят информацию, которую следует помнить до конца сеанса). Совершенно очевидно, что для Лисп-машины такой сценарий крайне нерационален:

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

Вычисление каждого S-выражения будет предваряться инициализацией ядра Лиспа и загрузкой библиотеки. Это снизит среднюю производительность в десятки раз.

Все сказанное выше привело автора к тому, что:

Обслуживания каждого пользователя, должно выполняться в рамках отдельного изолированного процесса (существующего в системе в течение всего сеанса работы, а не завершающегося после каждого запроса, как CGI-программа). Этот процесс (ядро Лиспа) будет, естественно, сохранять состояние в течение всего сеанса с пользователем (ядро инициализируется один раз при запуске и библиотека загружается тоже один раз; функции, созданные пользователем "живут" в ядре и могут быть сохранены вызовом WRS;

Взимодействие ядра Лиспа с активным скриптом на WEB-сервере должно выполняться каким-либо подходящим методом межпроцессорного взаимодействия (в ОС Windows их масса!).

Запуск HomeLisp для каждого пользователя в отдельном процессе предотвращает зависание WEB-сервера и обеспечивает изолированность пользователей друг от друга. Но, к сожалению, имеется еще одна проблема...

Что, произойдет, если пользователь просто закроет окно своего браузера? Браузер закроется, а процесс HomeLisp останется (и будет занимать оперативную память - превратится в процесс-сироту). Следовательно, нужен механизм, в соответствии с которым, процесс HomeLisp, запущенный на сервере, "узнавал", что браузер закрыл обмен.

Автор приял следующее решение: в активном скрипте создается т.н. коммуникационный объект (из входящей в поставку HomeLisp библиотеки Tpw.Dll). Коммуникационный объект запускает исполняемый модуль HomeLispWeb.exe и устанавливает с ним связь, используя именованные каналы (pipes). У объекта, создаваемого из библиотеки Tpw.Dll, есть метод отправки команды HomeLisp и метод получения отклика. Данные, получаемые с формы ввода главной страницы, передаются "своему" экземпляру ядра HomeLisp, после чего коммуникационный объект дожидается ответа ядра. Полученный ответ отображается средствами HTML. Если время ожидания ответа превысило некое граничное значение, то скрипт считает, что ядро HomeLisp завершилось аварийно. При этом происходит автоматический рестарт ядра.

Если пользователь надолго "задумается" и не будет вводить никаких команд, то ядро может "решить", что пользователь просто закрыл браузер. Чтобы дать возможность пользователю думать столько, сколько требуется, в активном скрипте предусмотрена следующая возможность. Один раз в секунду скрипт посылает ядру сигнал квитирования (короткую условную последовательность байтов). Для ядра прием этого сигнала означает, что клиент работает (браузер не закрыт). Но если ядро не получает сигнала квитирования в течение 20-30 сек, то это означает, что клиент закрыл браузер. В этом случае ядро тоже завершает свою работу. Этот механизм гарантирует отсутствие процессов-сирот на WEB-сервере.

Архитектура получившегося WEB-приложения, описанная выше, показана на следующем рисунке.

Настройка WEB-компоненты.

Для функционирования WEB-компоненты нужно обеспечить наличие на компьютере-сервере WEB-сервера IIS или PWS (персональный WEB-сервер). Здесь не будет описываться в подробностях, как инсталлировать это программное обеспечение, поскольку на эту тему имеется достаточно материалов в Интернете. В системе Win-9x инсталлятор персонального WEB-сервера находится на инсталляционном диске в папке \Add-On. Нужно запустить инсталлятор и отвечать на его вопросы. В системах Win-2k/XP для установки IIS необходимо пройти по цепочке:

Панель управления -> Установка и удаление программ -> Установка компонентов Windows

выбрать "Установка IIS" и отвечать на естественные вопросы (потребуется установочный диск Windows). За подробностями автор отправляет читателя к специальным руководствам.

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

\Dump - в этой директории размещаются снимки списка объектов и ассоциативного списка, которые запрашиваются пользователями во время работы;

\Tmp - в этой директории размещаются протоколы работы пользователей;

\Help - в этой директории размещаются html-файлы документации;

\Lib - в этой директории размещаются файлы автозагружаемых библиотек;

\Pwd - в этой директории размещается парольный файл usertab.txt;

\Img - в этой директории размещается необходимые графические файлы;

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

xCoder.Dll - библиотека кодирования;

Tpw.Dll - библиотека связи IIS с Web-компонентой;

HomeLispWeb.Exe - Web-компонента HomeLisp;

HomeLisp.ini ini-файл Web-компоненты;

Чтобы Web-приложение стало работоспособным, необходимо выполнить три действия:

1) Решить, оставлять директорию \Web на месте или скопировать ее в другое место файловой системы.

2) Приняв окончательное решение о размещении директории \Web (и, при необходимости, выполнив копирование), зарегистрировать библиотеки xCoder.Dll и Tpw.Dll, выполнив команды: regsvr32 xCoder.Dll и regSvr32 Tpw.Dll.

3) Войти в режим администрирования сервера IIS и создать виртуальную директорию, указав в качестве реальной директории путь к окончательному месторасположению директории \Web.

Дальнейшее изложение будет вестись применительно к Win2K/XP. Для Win9x последовательность создания виртуального каталога несколько иная. Пользователей Win9x автор адресует к специальным руководствам.

Создание виртуальной директории выполняется следующим образом: необходимо пройти по цепочке: Панель управления -> Администрирование -> Internet Information Services. Пользователь увидит т.н. оснастку администрирования IIS:

Необходимо щелкнуть правой клавишей мыши по иконке "Default Web Cite", выбрать в появившемся контекстном меню пункт "Cоздать" и далее - "Виртуальный каталог":

После щелчка мыши по надписи "Виртуальный каталог" запустится мастер создания виртуальных каталогов:

Следует нажать кнопку Далее. Пользователю будет предложено задать виртуальное имя (псевдоним) каталога. Именно это имя должны будут вводить в адресной строке браузера пользователи Web-компоненты. Виртуальное имя может быть любым (на приводимом ниже рисунке задано lisp):

Задав виртуальное имя, следует нажать кнопку "Далее" - будет предложено ввести реальное имя директории. Его можно ввести в поле ввода, либо восопользоваться стандартным диалогом выбора папки, нажав кнопку "Обзор":

На приведенном выше рисунке виртуальное имя Lisp приcваивается реальной директории e:\HLWeb. После нажатия кнопки "Далее", потребуется установить Web-права на создаваемую директорию:

Галочки "Чтение" и "Запуск сценариев" должны быть включены. После нажатия кнопки "Далее" мастер создания виртуальных директорий завершает работу:

Если все описанное выше, проделано верно, то при вводе в адресной строке браузера адреса http://bob/lisp (Здесь bob - имя сервера автора; пользователь, естественно, должен задать имя своего сервера) отобразится показанная ниже картинка:

Интерфейс конечного пользователя при работе с Web-компонентой.

Интерфейс пользователя Web-компоненты HomeLisp очень прост - он включает в себя следующие элементы:

Гиперссылку с надписью HomeLisp, с которой связана Help-система;

Панель инструментов с тринадцатью иконками (каждая из которых снабжена контекстной подсказкой). Для пользователя, прошедшего авторизацию, становятся доступными две дополнительные иконки ("сохранить состояние" и "редактор");

Область ввода;

Область ответа;

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

Назначение иконок панели инструментов приводится в следующей таблице:

Иконка | Назначение
 | Выполнить команду из области ввода
 | Листать протокол "вверх" (от первых команд к последним)
 | Листать протокол "вниз" (от последних команд к первым)
 | Очистить протокол
 | Вызвать сборку мусора (передать ядру HomeLisp команду (GC))
 | Сформировать снимок списка объектов (передать ядру HomeLisp команду (OBJLIST))
 | Сформировать снимок ассоциативного списка (передать ядру HomeLisp команду (ASSOLIST)). Снимок ассоциативного списка будет сформирован только в случае, если в ядре имеются атомы-переменные. В противном случае снимок не формируется и выдается предупреждение.
 | Включить режим статистики (передать ядру HomeLisp команду (STAT))
 | Включить режим статистики (передать ядру HomeLisp команду (UNSTAT))
 | Выключить режим дампа (передать ядру HomeLisp команду (UNDUMP*))
 | Выключить режим дампа (передать ядру HomeLisp команду (DUMP*))
 | Зарегистрироваться в системе (потребуется ввод логина и пароля).
 | Завершить работу c HomeLisp.

После того, как Web-компонента выдала приглашение к вводу, пользователь может вводить команды. Команды вводятся в область ввода, расположенной под панелью инструментов с иконками. Пользователь вводит S-выражение и щелкает мышью по иконке с изображением желтой молнии. Если введенное в область ввода S-выражение корректно (скобки и кавычки сбалансированы), то оно передается Web-компоненте, вычисляется, а результат отображается в браузере. Если же выражение некорректно, то пользователю выдается предупреждение, а выражение остается в области ввода для последующей правки.

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

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

Как можно убедиться, ответ состоит из нескольких областей - полос. В первой полосе продублирована введенная команда. Вторая полоса содержит результат вычисления введенного S-выражения. Третья полоса содержит статистические сведения: время вычисления (в сек.), количество занятых списочных ячеек, количество атомов (после слэша указано количество переменных) и ориентировочный объем использованной памяти.

Результат запроса снимка ассоциативного списка (щелчком по шестой иконке или вводом команды (OBJLIST)) показан ниже:

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

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

Если завести переменные (с использованием функции SET/SETQ), то станет доступен просмотр ассоциативного списка. Для просмотра ассоциативного списка следует щелкнуть по седьмой иконке, - будет сформирована гиперссылка для просмотра ассоциативного списка:

Сам ассоциативный список выглядит так:

Атомы в ассоциативном списке не упорядочиваются по именам, а показываются в порядке их создания.

Если введенное S-выражение использует вывод, т.е. функции PRINT/PRINTLINE, то в браузере вывод представляется отдельной полосой. Так, если пользователь ввел команду

(for i 1 30 ((printline (fact i))))

то результат будет таким:

Если пользователь включил режим статистики (нажав 8-ю иконку панели инструментов или явно введя команду (STAT)), то статистические данные о количестве вызовов функций будут также представлятся отдельной полосой:

Если пользователь включил режим дампирования (нажав 10-ю иконку панели инструментов или явно введя команду (DUMP*)), то в отдельной полосе приводится сылка на снимок дампа:

Сам дамп вычислений можно просмотреть в отдельном окне, щелкнув по ссылке "Просмотр дампа":

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

Дополнительные возможности авторизованного пользователя.

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

На приведенном выше рисунке приведена форма авторизации с введенными логином и паролем. Если пароль верен, то при нажатии Enter будет снова отображена главная форма Web-компоненты. Для авторизованного пользователя она имеет вид:

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

Иконки | Назначение
 | Сохранить состояние Лисп-машины в рабочей папке пользователя
 | Вызвать файловый менеджер

При щелчке по иконке с изображением дискеты (сохранение состояния), пользователю будет показана следующая экранная форма:

Пользователь должен в поле ввода, озаглавленном "Описание файла" ввести краткое описание создаваемого файла, а в поле "Имя файла" задать имя файла с расширением lsp. Имя файла не может содержать буквы диска и имен директорий. При щелчке по надписи "Сохранить состояние" в рабочей папке пользователя будет создан файл, содержащий S-выражения, составляющие текущее состояния Лисп-машины пользователя.

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

(WRS "имя_файла" "Описание_файла")

Сохраненное состояние можно затем восстановить из файлового менеджера.

Для вызова файлового менеджера пользователь должен щелкнуть мышью по предпоследней иконке панели инструментов (на ней изображено дерево файлов и папок). Главная форма файлового менеджера имеет вид:

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

Иконка | Назначение
 | Вернуться на главную форму (для ввода S-выражений)
 | Создать новый файл.
 | Занести файл в хранилище с внешнего носителя или с жесткого диска клиентского компьютера.
 | "Листать" список файлов "назад" (от последних файлов к первым).
 | "Листать" список файлов "вперед" (от первых файлов к последним).
 | Выгрузить соответствующий файл на внешний носитель.
 | Редактировать файл.
 | Удалить файл.

При щелчке по иконке "Cоздать новый файл" открывается окно простого web-редактора, имеющего вид:

Пользователь должен задать имя создаваемого файла (по умолчанию - noname), описание создаваемого файла и ввести содержимое файла:

Теперь следует щелкнуть по иконке с изображением дискеты. Произойдет сохранение файла в рабочей папке пользователя:

Как видно из рисунка, имя файла зафиксировалось. Файл можно редактировать и сохранять многократно. Если теперь вернуться в файловый менеджер (щелкнув по иконке со стрелкой), то можно увидеть, что файл добавился в оглавление:

Теперь, чтобы передать созданный файл ядру Лиспа, нужно просто щелкнуть мышью по названию файла. Будет выдано предупреждение:

После подтверждения загрузки файл будет обработан ядром Лиспа. Можно убедиться, что заданная функция работает предсказуемо: попытка деления на нуль возвращает атом infinite (бесконечность):

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

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

Теперь следует щелкнуть по 3-ей иконке панели инструментов. При успешной загрузке будет выдано сообщение и файл с комментарием добавится в хранилище:

Теперь этот файл можно корректировать и передавать ядру Лиспа для исполнения.

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

При щелчке по этой ссылке появится знакомый пользователям Интернета диалог сохранения:

Нажатие кнопки "Сохранить" позволяет сохранить файл на клиентском компьютере (на локальном или сетевом диске, а также на сменном носителе).

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

Завершение сеанса.

Закрыть сеанс можно, разумеется, просто закрыв окно браузера "крестом". Однако более коррекным способом закрытия сеанса будет щелчок мыши по последней иконке панели инструментов. При этом автоматически гасится процесс ядра Лиспа пользователя, а в браузере отображается вот такая картинка:

Красным выделяется чистое время центрального процессора, которое было потрачено в сеансе пользователя.

Администрирование WEB-компоненты.

Для администрирования WEB-компоненты необходимо в адресной строке браузера браузере ввести адрес:

http://Имя_Сервера/Имя_виртуальной_директории/admin.asp

У автора этого руководства приведенный выше адрес имеет вид:

http://Bob/lisp/admin.asp

Совершенно естественно, что любое администрирование начинается с запроса пароля:

Пусть читатель обратит внимание на то, что имя пользователя (admin) вообще не вводится. При первом запуске следует задать пароль password (строчными буквами). Впоследствии пароль администратора можно изменить (и это рекомендуется сделать!) После ввода пароля следует нажать Enter. Если пароль верен, то будет отображена WEB-страница администрирования:

Красным цветом выделено количество активных пользователей (точнее, число еще не истекших сессий ASP). Ниже каждый из административных режимов будет описан подробнее.

При выборе пункта "Конфигурировать параметры ядра", администратор увидит следующую картинку:

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

Особенно следует обратить внимание на параметр "Количество запускаемых экземпляров Web-компоненты". Значение этого параметра следует установить большим на 1-2, чем число предполагаемых рабочих мест. Параметр позволяет принудительно ограничить число процессов, в которых запускается модуль HomeLispWeb.Exe. При попытке запустить большее, чем допустимо, количество Web-компонент, пользователю будет показан следующий экран:

При щелчке по надписи "Попробовать еще раз" - делается попытка запустить экземпляр Web-компоненты. Если количество уже запущенных Web-компонент позволяет запустить еще одну, то она будет запущена. В противном случае пользователю вновь будет показана приведенная выше картинка.

При щелчке по надписи "Добавить пользователя", администратору будет предложено задать логин пользователя, его пароль, а также подтверждение пароля (все, как обычно...):

После заполнения всех полей следует нажать Enter. Если пароль не совпадает с контрольным вводом (содержимое второго поля ввода отлчается от содержимого третьего), то выдается соответствующее предупреждение, а пользователь не создается. Если делается попытка создать уже существующего пользователя, то также выдается сообщение об ошибке. Естественно, что нельзя создать пользователя admin. Если же контроль прошел успешно, новый пользователь будет создан и ему будет выделено рабочее пространство на диске.

При щелчке по надписи "Удалить пользователя", администратору будет показан список всех пользователей (разумеется, кроме администратора):

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

При щелчке по надписи "Сменить пароль пользователя", администратору будет показан список всех пользователей (включая администратора):

Администратор должен щелчком мыши выбрать нужного пользователя и ввести для него новый пароль и подтверждение нового пароля:

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

В заключение осталось сказать, что логины и пароли хранятся в файле \Pwd\usertab.txt в зашифрованном виде. Трогать этот файл "руками" не рекомендуется категорически. После переназначения пароля администратора и назначения паролей пользователям желательно сделать копию этого файла в надежном месте.

Как устроено рабочее пространство пользователя.

Рабочее пространство пользователя представляет собой папку с именем usr_Логин. Так, для пользователя Bob имя папки будет usr_Bob. Внутри этой папки располагается следующая информация:

Папка Tmp, в которой хранятся протоколы работы пользователя;

Текстовый файл descript.ion, котором каждая строчка содержит информацию об одном файле, загруженном в рабочее пространство;

Списка файлов вида "_nnnn.lsp", где nnnn - порядковый номер загруженного файла.

Файлы, загружаемые в рабочее пространство пользователя, переименовываются, а их исходные имена (а также длины, даты создания и описания, заданные пользователем) - хранятся в файле descript.ion. Ниже приводится фрагмент файла descript.ion:

0017.lsp;123.lsp;43;12/07/2008 18:32:12;Создание списка-2
0018.lsp;noname.lsp;14;13/07/2008 18:13:49;Вычисляем факториал
0020.lsp;lib-8.lsp;11879;18/07/2008 21:20:29;Версия с mklist
0021.lsp;atest;127;14/10/2009 21:44:15;Тест MKLIST1 и SUMLIST
0022.lsp;test1.lsp;233;14/10/2009 21:50:55;Тест FACT и TRY
0023.lsp;t1.lsp;97;16/10/2009 19:23:36;Проверка работы функции TRY

Можно видеть, что файл _0022.lsp первоначально назывался test1.lsp, имеет длину 233 байта и был загружен 10.10.2009. При загрузке пользователь снабдил этот файл комментарием: "Тест FACT и TRY".

Сообщения об ошибках WEB-компоненты.

Ошибки, которые могут возникать при работе Web-компоненты, можно разделить на две группы:

Ошибки, перехватываемые средой HomeLisp (деление на нуль, попытка вычислить CDR от атома и т.д.). Сообщения о таких ошибках выводятся отдельной полосой в области ответа. Эти ошибки, как правило, не вызывают завершения выполнения HomeLispWeb.exe.

Ошибки, вызывающие завершение выполнения HomeLispWeb.exe. При возникновении таких ошибок пользователю выводится сообщение вида, приведенного ниже, а через 10 сек происходит автоматический рестарт Web-компоненты.

Автор выражает надежду, что подобные ошибки будут возникать чрезвычайно редко (или вообще не будут возникать)...