Lisp зі смаком Pascal або 8501-й мова програмування

Деякий час тому (три роки) вирішив почитати підручник з Лиспу. Без будь-якої конкретної цілі, просто для загального розвитку і можливості шокувати співрозмовників екзотикою (один раз здається, навіть вийшло).

Але при найближчому розгляді Лисп виявився дійсно потужним, гнучким і як, не дивно, корисним в «побуті». Всі дрібні завдання автоматизації швидко перекочували в скрипти на Ліспі, а так само з’явилися можливості для автоматизації більш складною.

Тут варто відзначити, що під «можливістю автоматизації» я маю на увазі ситуацію, коли сумарний час на написання і налагодження програми менше, ніж час, що витрачається на рішення тієї ж задачі вручну.

Пол Грем написав не одну статтю і навіть книгу про переваги Лиспа. На момент написання цієї статті Lisp займає 33-е місце в рейтингу TOIBE (в три рази мертві мертвого Delphi). Виникає питання: чому мова так мало поширений якщо він так зручний? Приблизно два роки використання дали кілька натяків на причини.

Недоліки

1. Загальні структури даних
Концепція, що дозволяє оптимізувати функціональні програми, але яка загрожує трудноуловимыми помилками в імперативних. Можливість випадкового пошкодження сторонньої структури даних при модифікації змінної, що не має видимої зв’язку зі структурою, вимагає від програміста постійного контролю відбувається за лаштунками і знання внутрішньої реалізації кожної використовуваної функції (і системної, і для користувача). Найдивніше — це можливість пошкодити тіло функції, модифікацією її значення, що повертається.

2. Відсутність інкапсуляції
Поняття пакета хоча й існує, але не має нічого спільного з package у Ada або unit в Delphi. Будь-код може додати що завгодно в будь-який пакет (крім системних). Будь-код може отримати що завгодно з будь-якого пакета, використовуючи оператор ::.

3. Безсистемні скорочення
Чим відрізняється MAPCAN від MAPCON? Чому в SETQ, остання буква Q? З урахуванням віку мови можна зрозуміти причини такого стану справ, але хочеться мови трохи чистіше.

4. Багатопоточність
Цей недолік побічно відноситься до Лиспу і, в основному, стосується використовуваної мною реалізації — SteelBank Common Lisp. Стандартом Common Lisp багатопоточність не передбачена. Спроба використання реалізації, що надається SBCL, до успіху не привела.

Відмовлятися від такого зручного інструменту шкода, але незадоволеність поступово накопичується.

Пошук рішення

Спочатку можна зайти на Вікіпедію на сторінки Лиспа. Оглянути розділ «Діалекти». Прочитати короткий вступ до кожного. І усвідомити, що на смак і колір всі фломастери різні.
Якщо хочеш щось зробити, чи потрібно це робити самому
— Жан Батист Еммануель Зорг Спробуємо створити свій правильний Лисп, додавши в нього трохи Ади, багато Delphi і зовсім краплю Оберона. Назвемо отриману суміш що здійснюва-лися.

Основні концепції

1. Ніяких покажчиків
В рамках боротьби з ПРОБЛЕМОЮ-1 всі операції повинні проводитися шляхом копіювання значень. По виду структури даних в коді або при виведенні на друк повинні бути повністю видно всі її властивості, зовнішні і внутрішні зв’язки.

2. Додамо модулі
В рамках боротьби з проблемою-2 імпортуємо з Ади оператори package, with й use. У процесі, відкинемо надмірно складну схему імпорту/затінення символів Лиспа.

(package ім'я-пакету (список експортованих символів)
(реалізації)
(функцій))

(with ім'я-пакету) ;пошук файлу «ім'я-пакету.lisya» і імпорт вмісту

(use ім'я-пакету) ;аналогічно, але символи імпортуються без імені пакета 

3. Менше скорочень
Найбільш часті і загальновживані символи все одно будуть з скороченнями, але переважно найбільш очевидні: const, var. Функція форматованого виводу — FMT вимагає скорочення, оскільки часто зустрічається всередині виразів. Elt — взяття елемента — просочився з Common Lisp і прижився, хоча необхідність у скороченні немає.

Читайте також  Проблематика сови і глобуса: підключення двох збірок з ідентичними просторами імен і назв класів

4. Регистронезависимые ідентифікатори
Я вважаю, що правильна мова (і файлова система) {$HOLYWAR+} повинен бути регістронезавісімий {$HOLYWAR-}, щоб не ламати зайвий раз голову.

5. Зручність використання з російською розкладкою клавіатури
Синтаксис Лісі всіляко уникає використання символів, недоступних в одній з розкладок. Немає квадратних і фігурних дужок. Немає #, ~, &, <, >, |. При читанні чисельних літералів правильними десятковими значеннями, вважаються як кома, так і крапка.

6. Розширений алфавіт
Однією з приємних рис SBCL виявився UTF-8 в коді. Можливість оголошувати константи ВЕДМІДЬ, ГОРІЛКА і БАЛАЛАЙКА значно спрощує написання прикладного коду. Можливість вставляти Ω, Ψ і Σ робить формули в коді наочніше. Хоча теоретично існує можливість використовувати будь-які символи юнікоду, гарантувати коректність роботи з ними складно (швидше лінь, ніж складно). Обмежимося кирилицею, латинкою і грецьким.

7. Чисельні літерали
Це найбільш корисне для мене розширення мови.

10_000 ;роздільниками розрядів для читання
10k ;десяткові приставки для цілих і дробових чисел
10к ;росіяни десяткові приставки для мінімізації перемикання розкладки
10° 10pi 10deg 10гр ;не десяткові приставки
10π ;приставка pi в більш естетичному варіанті
10+i10 ;літерал комплексного числа 
10+м10 ;ще раз комплексне число 
10а10deg ;літерал комплексного числа у показовій формі з аргументом у градусах

Останній варіант мені здається самим не естетичним, але він найпопулярніший.

8. Цикли
Цикли в Ліспі нестандартні і неабияк заплутані. Спростимо до мінімального стандартного набору.

(for i 5 
 ;повторити п'ять разів i = 0..4
)
(for i 1..6 
 ;повторити п'ять разів i = 1..5
)
(for i список 
 ;повторити для кожного елементу списку 
 ;допускається призначення нового значення змінної циклу
)
(for i (subseq список 2) 
 ;повторити для елементів списку починаючи з другого елемента і до кінця
 )

Змінна циклу за його межами не видно.

(while умова
 )

9. GOTO
Не дуже потрібний оператор, але без нього складно продемонструвати нехтування правилами структурного програмування.

(block
:мітка
 (goto :мітка)) ;цей блок коду іноді зависає

10. Уніфікація областей видимості
В Ліспі є два різних типи областей видимості: TOPLEVEL і локальна. Відповідно є два різних способи оголошення змінних.

(defvar A 1)
(let ((a 1)) ...)

У Лисиці тільки один спосіб, використовуваний як на верхньому рівні скрипта, так і в локальних областях, включаючи пакети.

(var A 1)

При необхідності обмежити область видимості використовується оператор

(block 
 (var A 1)
 (set A 2)
 (fmt A nil))

Тіло циклу міститься в неявному операторі BLOCK (як і тіло функції/процедури). Всі оголошені в циклі змінні знищуються в кінці ітерації.

11. Однослотовость символів
В Ліспі функції є особливими об’єктами і зберігаються в спеціальному слоті символу. Один символ може одночасно зберігати змінну функцію і список властивостей. У Лисиці кожен символ пов’язаний тільки з одним значенням.

12. Зручний ELT
Типовий доступ до елементу складної структури в Ліспі виглядає так

Читайте також  Google вчить розпізнавати користувачів фішингові e-mail
(elt (slot-value (elt структура 1) 'слот-2) 3) 

У Лисиці реалізований уніфікований оператор ELT, який забезпечує доступ до елементів будь-яких складових типів (списків, рядків, записів, масивів байт, хеш-таблиць).

(elt структура 1 слот-2 3)

Ідентичну функціональність можна отримати і макросом на Ліспі

(defmacro field (object &rest f)
 "Витягує елемент складної структури по вказаному шляху.
 (field *object* 0 :keyword symbol "string")
 Кожен числовий параметр трактується як індекс масиву.
 Кожне ключове слово трактується як властивість у plist.
 Кожен символ (не ключовий) трактується як функція доступу.
 Кожна рядок трактується як ключ в асоціативному масиві."
 (if f (symbol-macrolet ((f0 (elt f 0))(rest (subseq f 1))) 
 (cond 
 ((numberp f0) `(field (elt ,object ,f0) ,@rest))
 ((keywordp f0) `(field (getf ,object ,f0) ,@rest))
 ((stringp f0) `(field (cdr (assoc ,f0 ,object :test 'equal)) ,@rest))
 ((and (listp f0) (= 2 (length f0)))
 `(field (,(car f0) ,(cadr f0) ,object) ,@rest))
 ((symbolp f0) `(field (,f0 ,object) ,@rest))
 (t `(error "Помилку форматування імені поля"))))
 object))

13. Обмеження режимів передачі параметрів підпрограм
В Ліспі є, як мінімум п’ять режимів передачі параметрів: обов’язкові, &optional, &rest, &key, &whole і дозволена їх довільна комбінація. Насправді, більшість комбінацій дають дивні ефекти.
У Лисиці дозволено використовувати тільки комбінацію з обов’язкових параметрів і одного з наступних режимів на вибір :key, :optional, :flag, :rest.

14. Багатопоточність
З метою граничного спрощення написання багатопоточних програм, була прийнята концепція відділення пам’яті. При породженні потоку всі змінні, доступні новому потоку, копіюються. Всі посилання на ці змінні замінюються посиланнями на копії. Передача інформації між потоками можливе лише за допомогою захищених об’єктів або через результат, що повертається потоком при завершенні.

Захищені об’єкти завжди містять критичні секції для забезпечення атомарности операцій. Вхід в критичні секції здійснюється автоматично — окремих операторів для цього в мові немає. До захищених об’єктів належать: черга повідомлень, консоль і файлові дескриптори.

Створення потоків можливо многопоточной функцією відображення

(map-th (function (x) ...) дані для обробки) 

Map-th автоматично запускає кількість потоків дорівнює кількості процесорів в системі (або в два рази більше, якщо у вас Intel inside). При рекурсивном виклик, наступні виклики map-th працюють в один потік.

Додатково є вбудована функція thread, що виконує процедуру/функцію в окремому потоці.

;приклад асинхронного виконання
(var потік (thread тривалі-обчислення-1))
(+ (тривалі-вычисения-2) (wait потік))

15. Функціональна чистота в імперативному коді
У Лисиці є функції для функціонального програмування і процедури для процедурного. На підпрограми, оголошені з використанням ключового слова function, накладаються вимоги відсутності побічних ефектів і незалежності результату від зовнішніх факторів.

Нереалізоване

Деякі цікаві можливості Лиспа залишилися не реалізованими в силу низького пріоритету.

1. Узагальнені методи
Можливість виконувати перевантаження функцій за допомогою defgeneric/defmethod.

2. Спадкування

3. Вбудований відладчик
При виникненні виключення інтерпретатор Лиспа перемикається в режим відладчика.

4. UFFI
Інтерфейс для підключення модулів, написаних на інших мовах.

5. BIGNUM
Підтримка цілих чисел довільної розрядності

Відкинуті

Деякі можливості Лиспа були розглянуті і полічені марними/шкідливими.

1. Керована комбінація методів
При виклику методу для класу виконується комбінація методів батьків і існує можливість змінювати правила комбінації. Підсумкове поведінка методу представляється слабо передбачуваним.

Читайте також  Загальні спринти в Atlassian Jira Software

2. Перезапуски
Обробник виключення може внести зміни в стан програми і послати команду перезапуску кодом, сгенерировавшему виняток. Ефект від застосування аналогічний використання оператора GOTO для переходу з функції у функцію.

3. Римський рахунок
Лисп підтримує систему числення, яка застаріла незадовго до його появи.

Використання

Наведу кілька простих прикладів коду

(function crc8 (data :optional seed)
 (var result (if-nil seed 0))
 (var s_data data)

 (for bit 8
 (if (= (bit-and (bit-xor result s_data) $01) 0)
 (result set (shift result -1 8))
(else
 (result set (bit-xor result $18))
 (result set (shift result -1 8))
 (result set (bit-or result $80))))
 (set s_data (shift s_data -1 8)))
 result)

;поелементне зведення списку в квадрат
(map (function (x) (** x 2)) (1 2 3))

;витяг із списку рядків, що починаються з qwe і довжиною більше п'яти символів
(filter (function (x) (regexp:match x «^qwe...»)) список-рядків)

;але якщо багато рядків, а шестиядерний процесор, то краще так
(filter-th (function (x) (regexp:match x «^qwe...»)) список-рядків)

Реалізація

Інтерпретатор написаний на Delphi (FreePascal в режимі сумісності). Збирається в Lazarus 1.6.2 і вище, під Windows і Linux 32 і 64 біта. Із зовнішніх залежностей вимагає libmysql.dll. Містить близько 15_000..20_000 рядків. Є близько 200 вбудованих функцій різного призначення (деякі перевантажені вісім разів).

Зберігається тут

Підтримка динамічної типізації виконана тривіальним чином — всі використовувані типи даних представлені спадкоємцями одного класу TValue.

Найважливіший для Лиспа тип — список є, як і прийнято в Delphi, класом, що містить динамічний масив об’єктів типу TValue. Для даного типу реалізований механізм CopyOnWrite.

Автоматичне керування пам’яттю на основі підрахунку посилань. Для рекурсивних структур виконується підрахунок всіх посилань в структурі одночасно. Звільнення пам’яті запускається відразу при виході змінних з області видимості. Механізми відкладеного запуску збирача сміття відсутні.

Обробка винятків працює на механізмі вбудованому в Delphi. Таким чином, помилки, що виникають в коді інтерпретатора, можуть бути оброблені виконуваним кодом на Лиса.

Кожен оператор або вбудована функція Лісі реалізований як метод або функція в коді інтерпретатора. Виконання скрипта здійснюється шляхом взаємно-рекурсивного виклику реалізацій. У коду інтерпретатора і скрипта загальний стек викликів.

Змінні скрипта зберігаються в динамічній пам’яті незалежно. Кожна користувацька функція має свій стек для зберігання посилань на змінні, незалежний від стека верхнього рівня або стека батьківського функцій.

Особливу складність являє собою реалізація оператора присвоювання (set) для елементів структур. Безпосереднє обчислення вказівника на необхідний елемент призводить до ризику появи висячих посилань, оскільки синтаксис Лісі не забороняє модифікацію структури в процесі обчислення необхідного елемента. Як компромісне рішення реалізований «цепочечный покажчик» — об’єкт, що містить посилання на змінну і масив числових індексів для вказівки шляху всередині структури. Такий покажчик так само схильний проблеми висячих посилань, але в разі збою генерує осмислене повідомлення про помилку.

Інструменти розробки

1. Консоль

2. Текстовий редактор
Обладнаний підсвічуванням синтаксису і можливістю запуску редагованого скрипта F9.

Висновок

В поточному стані проект вирішує ті проблеми, заради яких готувався, і не вимагає подальшої активної доопрацювання. Безліч присутніх недоробок не роблять істотного впливу на роботу.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *