You are on page 1of 211

НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ

«КИЄВО-МОГИЛЯНСЬКА АКАДЕМІЯ»

Серія «Могилянський підручник»

Глибовець М. М., Кирієнко О. В., Проценко В. С.

МОДЕЛІ ОБЧИСЛЕНЬ
У ПРОГРАМНІЙ ІНЖЕНЕРІЇ

Київ

Видавничий дім
«Києво-Могилянська академія»
2019
УДК 004.4/.9(075.8)
Г54

Серію засновано 2017 року

Рекомендувала до друку Вчена рада факультету інформатики


Національного університету «Києво-Могилянська академія»
(протокол № 12 від 30 жовтня 2018 року)

Рецензенти:
Анісімов А. В., член-кореспондент НАН України, доктор фізико-мате-
матичних наук, професор, декан факультету кібернетики Київ-
ського національного університету імені Тараса Шевченка;
Мейтус В. Ю., доктор фізико-математичних наук, старший науковий
співробітник Міжнародного науково-навчального центру інфор-
маційних технологій та систем НАН України та МОН України.

У навчальному посібнику викладено теоретичний базис, який є під-


ґрунтям більшості елементів програмного забезпечення, – головні моделі об-
числень, що лежать в основі провідних мов програмування. Розглядаються
різні методи конструювання моделей, які утворюють фундамент базових алго-
ритмів, що використовуються в практиці програмування. Також описано меха-
нізми породження та аналізу формальних мов: форми Бекуса-Наура, кон-
текстно-вільні та регулярні граматики. Представлено використання цих теоре-
тичних напрацювань у мові програмування Java. В посібнику наведено велику
кількість прикладів.
Для вивчення моделей обчислень описано використання програми
ModelComp. Програма має зручний інтерфейс та дозволяє будувати, викону-
вати й зберігати різні моделі обчислень.
Видання призначене для студентів вищих навчальних закладів та аспі-
рантів, які спеціалізуються за напрямом «Інженерія програмного забезпечення»
й «Комп’ютерні науки».

© Глибовець М. М., Кирієнко О. В.,


Проценко В. С., 2019
© Остапов О. Я., логотип серії, 2017
ISBN 978-966-2410-99-0 © НаУКМА, 2019
(Серія «Могилянський підручник») © Видавничий дім
ISBN 978-966-518-767-7 «Києво-Могилянська академія», 2019
Зміст
1. МОДЕЛІ 9
1.1. Вступ 9
1.1.1. Програмна інженерія 10
1.1.2. Моделі обчислень 11
1.1.3. Числові функції 12
1.2. Нормальні алгоритми Маркова 12
1.2.1. Алфавіт і слова 12
1.2.2. Нормальний алгоритм Маркова 13
1.2.3. Прості текстові перетворення 14
1.2.4. Текстові перетворення 16
1.2.5. Часткова функція, її обчислення 18
1.2.6. Множення і степінь 19
1.2.7. Задачі 22
1.3. Машини Тюрінґа 23
1.3.1. Машина Тюрінґа 23
1.3.2. Конфігурація, відображення 25
1.3.3. Приклад роботи машини Тюрінґа 27
1.3.4. Часткова функція, її обчислення 27
1.3.5. Діаграма машини Тюрінґа 29
1.3.6. З’єднання машин Тюрінґа 30
1.3.7. Базові машини Тюрінґа 32
1.3.8. Числові функції 33
1.3.9. Задачі 35
1.4. Машини з необмеженими регістрами 36
1.4.1. Програма та її виконання 36

3
1.4.2. Блок-схеми та програми 38
1.4.3. Обчислення числової функції 39
1.4.4. Перетворення програм 41
1.4.5. Приклади деяких програм 42
1.4.6. Задачі 43
1.5. Системи Поста 44
1.5.1. Правило виведення і безпосереднє виведення 44
1.5.2. Означення системи Поста 46
1.5.3. Функція, обчислювана за Постом 47
1.5.4. Прості числові функції 48
1.5.5. Розширений алфавіт у системах Поста 49
1.5.6. Задачі 51
1.6. Частково-рекурсивні функції 52
1.6.1. Вступ. Базові функції 52
1.6.2. Операція суперпозиції. Функції-константи 53
1.6.3. Операція примітивної рекурсії 55
1.6.4. Прості арифметичні функції 56
1.6.5. Тотальні функції віднімання 57
1.6.6. Операція мінімізації 58
1.6.7. Частково визначені функції. Означення 59
1.6.8. Задачі 60
1.7. Лямбда-числення 61
1.7.1. Лямбда-вирази 61
1.7.2. Вільні та зв’язані змінні 62
1.7.3. Застосування 63
1.7.4. Цілі числа 65

4
1.7.5. Додавання і множення 66
1.7.6. Логічні значення і операції 67
1.7.7. Перевірка умови 67
1.7.8. Пари і попереднє число 68
1.7.9. Рівність і нерівність 69
1.7.10. Рекурсія 69
1.7.11. Задачі 71
1.8. Програма ModelComp 72
1.8.1. Встановлення програми 72
1.8.2. Призначення програми 72
1.8.3. Робота з машиною з необмеженими регістрами 75
1.8.4. Робота з машиною Тюрінґа 76
1.8.5. Робота з нормальним алгоритмом Маркова 78
1.8.6. Робота з системою Поста 80
1.8.7. Робота з частково-рекурсивними функціями 82
1.8.8. Робота з лямбда-виразами 84
1.8.9. Представлення моделей у текстовому файлі 86
2. АНАЛІЗ 92
2.1. Граматики 92
2.1.1. Означення: граматика, мова 92
2.1.2. Типи граматик 93
2.1.3. Означення дерева 94
2.1.4. Дерево виведення 95
2.1.5. Приклади КВ-граматик 96
2.1.6. Задачі 97
2.2. Скінченні автомати 98

5
2.2.1. Детермінований скінченний автомат 98
2.2.2. Недетермінований скінченний автомат 100
2.2.3. Побудова еквівалентного
детермінованого автомата 102
2.2.4. Регулярні граматики 103
2.2.5. Задачі 104
2.3. Регулярні вирази 105
2.3.1. Алгебра Кліні 105
2.3.2. Регулярні вирази 106
2.3.3. Регулярні вирази й скінченні автомати 107
2.3.4. Задачі 110
2.4. Контекстно-вільні граматики 111
2.4.1. Контекстно-вільна граматика 111
2.4.2. Лівостороння і правостороння вивідності 112
2.4.3. Дерево виведення 112
2.4.4. Мова, неоднозначність 113
2.4.5. Задачі 114
2.5. Властивості КВ-граматик 116
2.5.1. Приведення КВ-граматик 116
2.5.2. Перетворення КВ-граматик 118
2.5.3. Схеми граматик 121
2.5.4. Задачі 122
2.6. Магазинні автомати 124
2.6.1. Магазинний автомат і конфігурація 124
2.6.2. Приклад МП-автомата 126
2.6.3. Властивості МП-автоматів 127

6
2.6.4. Задачі 129
2.7. Синтаксичний аналіз 129
2.7.1. Задача синтаксичного аналізу 129
2.7.2. Приклад синтаксичного аналізу 130
2.7.3. Синтаксичні аналізатори 131
2.7.4. Синтаксичний аналіз згори вниз 132
2.7.5. Синтаксичний аналіз знизу вгору 133
2.8. LL(1)-граматики 134
2.8.1. Означення LL(1)-граматики 134
2.8.2. LL(1)-аналізатор 135
2.8.3. Приклад LL(1)-аналізатора 136
2.8.4. Побудова керівної таблиці 138
2.8.5. Побудова функцій first і follow 139
3. JAVA 141
3.1. Регулярні вирази 141
3.1.1. Регулярні вирази Java 141
3.1.2. Методи класу String 142
3.1.3. Класи Pattern та Matcher 143
3.1.4. Робота з групами 145
3.1.5. Розпізнавання дійсного числа 147
3.1.6. Задачі 148
3.2. Робота з КВ-граматиками 150
3.2.1. Клас Grammar 150
3.2.2. Вивідність 152
3.2.3. Синтаксичне дерево 153
3.2.4. Перетворення граматик 155

7
3.3. Робота з LL(1)-граматиками 156
3.3.1. LL(1)-граматики 156
3.3.2. LL(1)-аналіз 159
3.3.3. Побудова fst і nxt 159
3.3.4. Реалізація first і follow 162
3.4. Рекурсивний спуск 163
3.4.1. Метод рекурсивного спуску 163
3.4.2. Прості арифметичні вирази 167
3.4.3. Ітераційні форми та синтаксичні діаграми 170
3.4.4. Регулярні вирази 174
3.4.5. Задачі 176
3.5. Аналіз мов 177
3.5.1. Лексичний аналіз 177
3.5.2. Синтаксичний аналіз 181
3.5.3. Різні джерела символів 183
3.5.4. Абстрактні синтаксичні дерева 185
3.6. Лямбда-вирази 189
3.6.1. Потоки даних 189
3.6.2. Функціональні інтерфейси 192
3.6.3. Обробка масиву 194
3.6.4. Числа Фібоначчі 195
3.6.5. Текстові файли і слова 196
3.6.6. Бібліотека 199
3.6.7. Множина Мандельброта 202
3.6.8. Задачі 208
Список літератури 209

8
Моделі обчислень у програмній інженерії

1. Моделі
1.1. Вступ
Мета курсу – навести приклади, як впливає теорія на програ-
мування в програмній інженерії. Показати, які окремі теоретичні
напрацювання використовуються в мові програмування Java.
Існує декілька відомих моделей обчислень (Розділ 1), які
були створені в середині минулого століття і які утворили фун-
дамент сучасних мов програмування:
 Нормальні алгоритми Маркова – найпростіша для ро-
зуміння модель обчислень;
 Машини Тюрінґа й машини з необмеженими регістра-
ми – процедурне програмування;
 Системи Поста – логічне програмування;
 Частково-рекурсивні функції й лямбда-числення –
функціональне програмування.
Усі моделі обчислень породжують один і той самий клас
ефективних обчислюваних функцій (тезис Черча). З погляду
навчання, кожна модель обчислень – це проста мова програ-
мування, в основі якої лежить конкретна парадигма програ-
мування. А процес вивчення моделей обчислень можна звести до
побудови (програмування) функцій певного класу (традиційно-
числові функції натурального аргументу). Звичайно, для цього
необхідно мати програму зі зручним інтерфейсом (ModelComp),
яка дозволяє будувати, виконувати та зберігати різні моделі об-
числень. Програму ModelComp можна завантажити з репози-
торію https://github.com/ProtsenkoVS/models_interpreter.git.
Інший тип моделей – механізми породження формальних
мов (Розділ 2). Найбільш відомий із них – граматики Хомського.
На практиці найчастіше використовуються контекстно-залежні
(типу 1), контекстно-вільні (типу 2) та регулярні (типу 3) грама-
тики. Найпотужніші граматики (граматики типу 0) породжують
той самий клас мов, що й системи Поста.

9
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Але головне завдання цих моделей інше – описати не процес


ефективного обчислення, а множину слів (формальну мову) в
такий спосіб, щоб мати можливість ефективно розв’язати задачу
синтаксичного аналізу. Задача синтаксичного аналізу – для зада-
ної формальної мови L і довільного слова w визначити, чи нале-
жить слово w мові L, w  L?
Як окремі теоретичні напрацювання використовуються в
мові програмування Java, розглядається в розділі 3. Повні тексти
Java-програм, наведених у ньому, можна знайти в репозиторії
https://github.com/ProtsenkoVS/models_java.git.
Регулярні граматики породжують найпростіший клас регу-
лярних мов, із якими зв’язані моделі, регулярні вирази та скін-
ченні автомати. Прикладний програмний інтерфейс Java API
використовує регулярні вирази (пакет java.util.regex).
Контекстно-вільні граматики використовуються для опису
синтаксису мов програмування, а окремі їхні класи допускають
ефективний синтаксичний аналіз за один прохід вхідного слова
(у випадку мови програмування – програми) без повернення.
Задача синтаксичного аналізу досить часто трапляється на
практиці (синтаксичні аналізатори). Цю задачу можна розв’я-
зати, використовуючи метод рекурсивного спуску, що базується
на використанні контекстно-вільних граматик, або, частіше,
їхніх різновидів – нотації Бекуса-Наура або діаграм Вірта.
У Java 8 введено потоки даних, в основі яких лежить лямбда-
числення.

1.1.1. Програмна інженерія


Програмна інженерія – розділ комп’ютерної науки, що ви-
вчає методи і засоби побудови комп’ютерних програм. Вона
містить багато різних підрозділів і напрямів. Лише перелік го-
ловних теоретичних і практичних знань, що використовуються в
програмній інженерії, становить великий документ SWEBOK, що
створювався міжнародною спільнотою декілька років і опуб-
лікований 2004 року.

10
Моделі обчислень у програмній інженерії

 SWEBOK – The Guide to the Software Engineering Body of


Knowledge (Керівництво до комплексу знань з програм-
ної інженерії)
Теоретичний базис програмної інженерії становлять такі ма-
тематичні дисципліни:
 теорія множин;
 теорія алгоритмів;
 математична логіка;
 теорія доведення;
 теорія формальних мов.
Якщо уявити програмну інженерію як велику будівлю, то в її
основі містяться моделі обчислень, сформовані теоретичним
базисом. Без моделей обчислень програмна інженерія як будівля
НЕ існує.

1.1.2. Моделі обчислень


1931 року Курт Ґедель (Австрія, США), опублікував дове-
дення теорем про неповноту формальних систем, зокрема ариф-
метики. Як наслідок, з’являється припущення про неможливість
алгоритмічного розв’язку багатьох математичних проблем, що,
своєю чергою, призводить до необхідності стандартизації по-
няття алгоритму.
Неформально алгоритм, або модель обчислення – це си-
стема правил для розв’язку задач певного класу. Ця система
правил повинна задовольняти цілу низку вимог: скінченність,
масовість, дискретність, елементарність, детермінованість та
результативність.
У 30–40 роках минулого століття з’явилася низка моделей
обчислень, створених різними вченими в різних країнах. Серед
них:
 Лямбда-числення (1932, Черч, США);
 Машина Тюрінґа (1936, Тюрінґ, Англія);
 Частково-рекурсивні функції (1936, Ґедель і Кліні,
США);

11
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Системи Поста (1943, Пост, США);


 Нормальні алгоритми Маркова (1951, Марков, СРСР);
 Машини з необмеженими регістрами (1963, Шепердсон і
Стерджіс, США).
Сьогодні відомо, що всі ці моделі обчислень задають один і
той самий клас обчислюваних функцій.

1.1.3. Числові функції


У моделях обчислень перш за все розглядають частково-
визначені функції натурального аргументу. Натуральні числа –
елементи множини N = {0,1, …, n, …}. Кожна функція натураль-
ного аргументу f має k (k>0) аргументів – натуральні числа та
результат її обчислення – теж натуральне число.
Позначаємо це так:
f: Nk–>N.
Область визначення функції позначаємо як Df:
Df⊆Nk.
Якщо (a1, …, an)∈Df і значення функції f у цій точці дорівнює
b, то f (a1, …, an) = b.
Якщо (a1, …, an)∉Df, то значення функції в цій точці не визна-
чено, що записуватимемо як f (a1, …, an) = невизначено.
 f (x, y) = x+y – всюди визначена.
 f (x, y) = x-y – частково визначена, лише для x ≥ y.

1.2. Нормальні алгоритми Маркова


1.2.1. Алфавіт і слова
Алфавіт T – скінченна множина символів. Слово в алфаві-
ті T – довільна скінченна послідовність символів T. Якщо слово
α, яке являє собою послідовність символів a1, a2, …, an, (a1, a2, …,
an ϵ T, можливо, з повтореннями) позначатимемо α = a1 a2 … an.
Довжина слова α = a1 a2 … an – кількість символів у ньому,
|α| = n. Порожнє слово – це слово, яке не має жодного символу,
позначається як ε, |ε| = 0. Множину всіх слів в алфавіті T
позначають T*.

12
Моделі обчислень у програмній інженерії

Означення 1. Слово α входить у слово β, якщо існують такі


слова α1 і α2, що β = α1 α α2.
Приклад 1. Нехай алфавіт T складається із двох символів T =
{a, b}.
 ε, a, aa, bbb, ab – приклади слів в алфавіті T.
 Слово ab входить у слово abbbab два рази: abbbab та
abbbab.
 Слово aa входить у слово aaabaaaa пять разів: aaabaaaa,
aaabaaaa, aaabaaaa, aaabaaaa та aaabaaaa.
 Слово ε (порожнє) входить у слово ab три рази: ab, ab
та ab.

1.2.2. Нормальний алгоритм Маркова


Означення 2. Нормальний алгоритм Маркова U в алфавіті T –
скінченна впорядкована послідовність правил виведення
(підстановок) α –> β або α –> .β, де α, β ϵ Т*, а символи –> і . не
належать до алфавіту Т.
U = < α1 –> β1, α2 –> β2, …, αm –> βm>
 Деякі (а можливо, жодна) підстановки мають вигляд αj
–> .βj. Підстановка α –> .β – заключна.
Кожен алгоритм U може обробити довільне слово x в алфа-
віті T, перетворюючи його на інше слово в алфавіті T, яке нази-
вають результатом обробки і позначають U(x). Вважають, що
алгоритм U застосовується до слова.
Означення 3. Обробка виконується послідовно, x = x0 => x1,
x1 => x2, …, xn-1 => xn, і може або закінчитися, або НЕ закін-
читися. На кожному кроці обробки xn ϵ Т* перебуває перша за
порядком підстановка αi –> βi або αi –> .βi така, що αi – підслово
слова xn.
 ЗНАЙШЛИ таку підстановку: саме ліве входження αi в xn
замінюється на βi, утворюючи наступне слово обробки
xn+1:
o Якщо підстановка заключна (αi –> .βi), то обробка
завершується і xn+1 – її результат; U(x) = xn+1.

13
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o Якщо підстановка НЕ заключна (αi –>βi), то


обробка продовжується.
 НЕ ЗНАЙШЛИ – обробка завершується і xn – її резуль-
тат; U(x) = xn.
 Якщо процес обробки НЕ завершується ніколи, то вва-
жається, що результат обробки U(x) – невизначено.
o Один крок виконання нормального алгоритму, що
перетворює слово x на слово y, позначатимемо
x => y, а декілька послідовних кроків – x =*> y.
Означення 4. Алфавіт T нормального алгоритму Маркова U
інколи ділять на два алфавіти T = Tm∪Ta, що не перетинаються:
Tm∩Ta = {}, Tm – основний і Ta – додатковий. У разі такого поділу
вважають, що алгоритм задає відображення Tm*⟶Tm*, а додатко-
вий алфавіт використовується лише в проміжних словах у про-
цесі обробки. Під час обробки слова x ∈ Tm*, якщо алгоритм
завершується, наприклад, у результаті виконання заключної
підстановки, і результатом обробки є слово xn∉Tm*, то
вважається, що U(x) – невизначено.

1.2.3. Прості текстові перетворення


Приклад 2. Побудувати нормальний алгоритм addBegin, який
дописує слово abb на початок вхідного слова в алфавіті T =
{a, b}.
Алгоритм addBegin має одну заключну підстановку:
addBegin = < ε –> . abb>.
Підстановка ε –> . abb замінює саме ліве входження порож-
нього слова (ліва частина підстановки) на слово abb. Тому
addBegin(aab) = abbaab, addBegin(ε) = abb.
Приклад 3. Побудувати нормальний алгоритм clearBeginOne,
який стирає перший символ непорожнього вхідного слова в ал-
фавіті T = {a, b}, тобто clearBeginOne (abb) = bb, а clearBeginOne
(ε) = невизначено.
Для побудови необхідно розширити алфавіт T = Tm∪Ta, увів-
ши додатковий символ с; основним алфавітом буде Tm = {a, b}, а

14
Моделі обчислень у програмній інженерії

додатковим – Ta = {c}. Надалі символи Tm і Ta не вживатимуться,


а в переліку символів алфавіту T указуватимуться лише символи
основного. Вважається, що всі інші символи, що трапляються в
підстановках алгоритму – це символи додаткового алфавіту.
clearBeginOne = <ca –>.ε, cb –>.ε, ε –>c>.
Надалі під час запису алгоритму всі підстановки послідовно
нумеруватимуться, а порожнє слово ε не вказуватиметься.
1. ca –>.
2. cb –>.
3. –> c
Якщо вхідне слово – непорожнє, то алгоритм виконується у
два кроки. Спочатку підстановка 3 вводить символ с перед пер-
шим символом слова, а потім заключна підстановка 1 або 2 ви-
лучає перший символ слова. У разі порожнього слова вживається
лише підстановка 3 і алгоритм ніколи не зупиняється.
 **abb => *ca*bb => bb
o clearBeginOne(abb) = bb
 **ba => *cb*a => a
o clearBeginOne(ba) = b
 ** => **c => **cc => ….
o clearBeginOne(ε) = невизначено
Показуючи, як алгоритм обробляє вхідне слово, вживаються
символи *, що не належать до основного й додаткового алфаві-
тів – метасимволи. На кожному кроці вони виділяють частину
слова – ліву частину підстановки, яка вживається. Ця частина
слова замінюється на праву частину підстановки.
Дуже схожий алгоритм clearBeginOne1 = <a –>.ε, b –>.ε> не
виконує необхідного перетворення:
 **abb => bb
o clearBeginOne1(abb) = bb
 b*a* => b
o clearBeginOne1(ba) = b
 Якщо на вході – порожнє слово, то до нього не можна за-
стосувати жодної підстановки і clearBeginOne1(ε) = ε.

15
Глибовець М. М., Кирієнко О. В., Проценко В. С.

1.2.4. Текстові перетворення


Приклад 4. Побудувати нормальний алгоритм addEnd, який
дописує слово abb у кінець вхідного слова в алфавіт T = {a,b}.
Оскільки під час виконання нормального алгоритму замі-
нюється саме ліве входження правої частини підстановки, то
основна проблема полягає в тому, як знайти кінець вхідного
слова, де потрібно дописати abb.
1. ca –> ac
2. cb –> bc
3. c –> .abb
4. –> c
Для цього використовується допоміжний символ с, який
з’являється на початку слова (підстановка 4), «пробігає» всі сим-
воли a (підстановка 1) і b (підстановка 2).
Після чого вживається заключна підстановка 3.
 **abb => *ca*bb => a*cb*b => ab*cb* => abb*c* =>
abbabb
o addEnd (abb) = abbabb
 ** => *c* => abb
o addEnd (ε) = abb
Приклад 5. Побудувати нормальний алгоритм moveOne-
ToEnd, який переносить перший символ непорожнього вхідного
слова в алфавіті T = {a,b} у кінець слова, тобто moveOneToEnd
(abb) = bba, а moveOneToEnd (ε) = невизначено.
На відміну від попереднього прикладу, у кінець вхідного
слова потрібне не фіксоване слово abb, а перший символ, яким
може бути або a, або b.
1. caa –> aca
2. cab –> bca
3. cba –> acb
4. cbb –> bcb
5. c –>.
6. –> c

16
Моделі обчислень у програмній інженерії

Знову підстановка 6 уводить допоміжний символ c перед


першим символом вхідного слова, а далі підстановки 1–2 (якщо
перший символ – a) або підстановки 3–4 (якщо перший символ –
b) «проштовхують» його на кінець вхідного слова. На остан-
ньому кроці символ с стирається (підстановка 5).
 **abb => *cab*b => b*cab* => bb*c*a => bba
o moveOneToEnd (abb) = bba
 **bba => *cbb*a => b*cba* => ba*c*b => bab
o moveOneToEnd (bba) = bab
 ** => **c => **cc => …
o moveOneToEnd (ε) = невизначено
Приклад 5. Побудувати нормальний алгоритм reverse, який
кожне слово x в алфавіті T = {a, b} переводить на слово xR –
дзеркальне відображення слова.
Розв’язок цього прикладу використовує ідею попереднього,
але оскільки потрібно перемістити не тільки перший символ, а й
інші, то символ с генерується декілька разів.
1. сс –> d
2. dc –> d
3. da –> ad
4. db –> bd
5. d –>.
6. caa –> aca
7. cab –> bca
8. cba –> acb
9. cbb –> bcb
10. –> c
Якщо вхідне слово непорожнє, довжини k>0, то алгоритм
на першій стадії роботи генерує (k+1)-символ (підстановка 10) і
переміщує всі символи вхідного слова (підстановки 6–9). Слово
x1 x2…xk перетворюється на слово ccxkcxk-1…cx2cx1, і залишається
лише вилучити символи с, що роблять на другій стадії роботи
підстановки 1–5.

17
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 **abb => *cab*b => b*cab* => **bbca => *cbb*ca =>
**bcbca => **cbcbca => *cc*bcbca => *db*cbca =>
b*dc*bca => b*db*ca => bb*dc*a => bb*da* => bba*d*
=> bba
o reverse(abb) = bba

1.2.5. Часткова функція, її обчислення


Означення 5. У системі Пеано довільне натуральне число
0,1, …, n, … представляється як послідовність символів |:
 Порожній рядок ε задає число 0.
 Рядок | – число 1, || – число 2, ||| – число 3, |||| – число 4
й так далі.
 Для зручності інколи замість ||| записуватимемо |3.
Використовуючи символ # як розподільник, список аргумен-
тів функції (a1, …, an) можна представити у вигляді рядка
|a1#|a2…#|an, що складається з (a1+a2+…+an) символів | і (n–1)
символів #.
Нехай f – часткова функція натурального аргументу з об-
ластю визначення Df⊆Nk:
f: Nk–>N.
Якщо (a1, …, an)∈Df і значення функції f – у цій точці b, то f
(a1, …, an) = b.
Якщо (a1, …, an)∉Df, то значення функції в цій точці не визна-
чено, що записуватимемо як f(a1, …, an) = невизначено.
Означення 6. Нормальний алгоритм Маркова U обчислює
часткову функцію f: Nk–>N, якщо:
 U(|a1#|a2…#|an) = |b тоді й тільки тоді, коли (a1, …, an) ∈ Df
і f(a1, …, an) = b.
 U(|a1#|a2…#|an) ) – невизначено (НЕ зупиняється) тоді й
тільки тоді, коли (a1, …, an)∉Df, тобто f (a1, …, an) = не-
визначено.
Функція f: Nk–>N обчислювана за Марковим, якщо існує
нормальний алгоритм Маркова U, що обчислює функцію f.

18
Моделі обчислень у програмній інженерії

 Основний алфавіт довільного нормального алгоритму


Маркова, що обчислює числову функцію T = {|} або T =
{|,#}.
Приклад 6. Розглянемо нормальні алгоритми Маркова, що
обчислюють прості числові функції:
 id(x) = x
id = <ε –>. ε>
o Алгоритм застосовується до довільного вхідного
слова |k і нічого в ньому не змінює.
 undefined(x) – усюди невизначена функція
undefined = <ε –>ε>
 const0(x) = 2 – функція-константа
 const0 = < | –>ε, ε –>. ||
o Під час виконання алгоритму спочатку застосо-
вується правило 1, яке стирає всі символи |, а
потім – один раз правило 2.
 addition(x,y) = x+y – функція додавання
addition = <# –>ε>
 substractionAbs(x,y) = |x-y| – віднімання за модулем
substractionAbs = <|#| –> #, # –>ε>
o |x#|y =*> |x-y# => |x-y, якщо x>y
o |x#|y =*> # =>, якщо x=y
o |x#|y =*> #|y-x => |y-x, якщо y>x

1.2.6. Множення і степінь


Приклад 6. Функцію multiply2(x) = 2*x обчислює нормальний
алгоритм Маркова multiply2 із наступними підстановками:
1. a| –> ||a
2. a –>.
3. –> a
Додатковий символ a, який уводить підстановка 3, проходить
через послідовність символів |, подвоюючи кожний із них (під-
становка 1), і стирається заключною підстановкою 2.

19
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Приклад 7. Нормальний алгоритм Маркова, що обчислює


функцію multiply(x,y) = x*y, використовує додаткові символи a,
b, c і має підстановки:
1. a| –> |ba
2. a –>
3. b| –> |b
4. |# –> #a
5. # –> c
6. c| –> c
7. cb –> |c
8. c –>.
Робота алгоритму складається з двох етапів. Спочатку вико-
нуються підстановки 1–4, що перетворюють слово |x#|y на слово
#|ybx*x. Підстановка 4 x разів утворює символ a, який, проходячи
через y символів | (підстановка 1), утворює y символів |b.
Підстановка 3 впорядковує послідовність |b|b...|b на ||…|bb…b.
На другому етапі (підстановки 5–8) виконується завершальна
робота: перетворення символу # на символ c, який, проходячи
рядком |ybx*xb, перетворює його на рядок |x*x (підстановки 6–7) і
на останньому кроці (підстановка 8) c стирається.
Приклад 8. Функція power2(x) = x2 обчислювана за Мар-
ковим.
Щоб побудувати нормальний алгоритм Маркова, який об-
числює функцію power2, можна використати попередній алго-
ритм множення. Для цього його необхідно розширити так, щоб
новий алгоритм спочатку перетворював слово |x на слово |x#|x, а
потім працював як алгоритм множення. Такий алгоритм отри-
муємо, якщо до підстановок 1–8 алгоритму множення додати
наступні підстановки 9–14, що використовують інші додаткові
символи d, e, f.
9. d| –> |ed
10. e| –> |e
11. d –> f
12. ef –> f|
13. f –> #

20
Моделі обчислень у програмній інженерії

14. –> d
У такому разі можна отримати алгоритм, що містить менше
підстановок і виконується швидше, якщо використати наступну
формулу:
1 + 1 + (2*1+1) + (2*2+1) + … + (2*(x-1)+1) =
= 2*(1+2+…+(x-1)) + x = x*(x-1) + x = x2.
Наступний алгоритм, що використовує додаткові символи a,
b, c, обчислює наведену формулу:
1. cb –> |c
2. c –>.
3. a| –> |bba
4. a –> b
5. b| –> |b
6. | –> a
7. –> c
Головну роботу виконують підстановки 3–6, які перетворю-
ють слово |x на слово bx*x. Інші підстановки 7 та 1–2 замінюють
усі символи b на символи |.
Приклад 9. Функція exponential3(x) = 3x обчислювана за
Марковим.
Нормальний алгоритм Маркова, який обчислює функцію
exponential3, використовує додаткові символи b, c, d і має
наступні підстановки:
1. b| –> |bbb
2. |b –> |c
3. |c –> c
4. c –> d
5. db –> |d
6. d –>.
7. b –>. |
8. –> b
Головну роботу виконують підстановки 8 (виконується один
x
раз) і 1, що слово |x перетворюють на слово |xb3 або,
використовуючи операцію ^ і дужки, – (|^x)(b^(3^x)). Підста-

21
Глибовець М. М., Кирієнко О. В., Проценко В. С.

новки 2–6 виконують заключну роботу: стирають усі символи | і


потім замінюють усі символи b на символи |. Підстановка 7 ви-
конується, тільки якщо аргумент функції x=0.
Використовуючи ту саму ідею, можна створити коротший
нормальний алгоритм Маркова із двома додатковими символами
b, t.
1. b| –> |bbb
2. t| –> t
3. tb –> |t
4. t –>.
5. –> tb
Знову головну роботу виконують остання підстановка (вико-
нується один раз) і перша, але для виконання заключної роботи
потрібно лише три підстановки (2–4).

1.2.7. Задачі
1. Побудувати нормальний алгоритм clearEndOne, який стирає
останній символ вхідного слова в алфавіті T = {a,b}.
2. Побудувати нормальний алгоритм addEndOne, який дописує
в кінець вхідного непорожнього слова x1x2…xn в алфавіті T =
{a,b} перший символ x1.
o невизначений для порожнього вхідного слова
3. Побудувати нормальний алгоритм transferIn10, який перево-
дить натуральні числа з одиничної системи числення (число
n кодується n-символами |) в десяткову систему.
4. Побудувати нормальний алгоритм copyWord, який кожне
слово x в алфавіті T = {a,b} переводить у слово xx.
5. Побудувати нормальний алгоритм, який обчислює функцію
 expression1(x,y) = |x-(y+2)|
6. Побудувати нормальний алгоритм, який обчислює функцію
 expression2(x,y,z) = x-(y+z)
o невизначена при x<y+z
7. Побудувати нормальний алгоритм, який обчислює функцію
 expression3(x,y) = (x+2)*(y+3)

22
Моделі обчислень у програмній інженерії

8. Побудувати нормальний алгоритм, який обчислює функцію


 expression4(x) = 3x+x
9. Побудувати нормальний алгоритм, який обчислює функцію
 divide3Quatient(x) = xdiv3
o xdiv3 – натуральне число – частка від ділення x
на 3
10. Побудувати нормальний алгоритм, який обчислює функцію
 dividedRemainder(x,y) = xmody
o xmody – натуральне число – залишок від ділення x
на y
o xmod0 = 0
11. Побудувати нормальний алгоритм, який обчислює функцію
gcd(x,y) = НСД(x,y)
 НСД(x,y) – найбільший спільний дільник натуральних чи-
сел x і y
o НСД(x,0) = 0
o НСД(0,y) = 0

1.3. Машини Тюрінґа


1.3.1. Машина
Тюрінґа
Машина Тюрінґа
складається з логічного
пристрою, голівки й
нескінченної в обидва
боки стрічки з комір-
ками (мал. 1). Логічний
пристрій машини може
перебувати в одному зі станів qϵQ, читаючи за допомогою
голівки символ, що міститься в одній із комірок стрічки. Головне
призначення машини Тюрінґа – обробити інформацію, що
міститься (записана) на стрічці.

23
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Програма управління послідовно змінює стан машини, керу-


ючи роботою голівки. На кожному кроці голівка може змінити
символ у комірці, яку вона читає, й, можливо, перейти на
сусідню комірку ліворуч або праворуч.
Означення 1. Формально машина Тюрінґа M = (Q, T, σ, q0, qs)
містить:
 Q = {q0, q1, …, qs} – скінченну множину станів;
 T = {a1, a2, …, am} – скінченний алфавіт;
 : Q×(T∪{‘ ‘})→Q×(T∪{‘ ‘} )×{.,<,>} – функцію пере-
ходів (програму управління);
 q0∈Q – початковий стан;
 qs∈Q – заключний стан.
Кожна комірка може містити один символ алфавіту T або
символ проміжку ‘ ‘, який інколи для зручності позначатиметься
як _.
Функція переходів σ задає програму управління машини. На
кожному кроці роботи машина Тюрінґа перебуває в одному зі
станів q ∈ Q і читає на стрічці символ a ∈ T∪{‘ ‘}.
 Якщо (q,a) ∈ Dσ і (q,a) = (p,b,m), то машина перейде в
стан p ∈ Q, запише на стрічці символ b ∈ T ∪ {‘ ‘} замість
символу a, якщо a≠b, і перемістить голівку машини m:
ліворуч – (m=’<’), праворуч – (m=’>’), або залишиться
на місці – (m=’.’ ).
 Якщо (q,a) ∉ Dσ, то машина виконає «холостий хід»: за-
лишиться в тому самому стані q, не змінить символу на
стрічці a й залишить голівку на місці.
o Таку поведінку машини можна описати явно,
якщо довизначити функцію переходів так:
(q,a) = (q,a,.).
У процесі роботи машини Тюрінґа символи алфавіту T пере-
буватимуть лише в обмеженій області нескінченної стрічки, по
обидва боки якої розміщуються комірки, що містять лише сим-
вол проміжку.

24
Моделі обчислень у програмній інженерії

1.3.2. Конфігурація, відображення


Основним призначенням машини Тюрінґа є обробка інфор-
мації, записаної на стрічці.
У початковий момент на стрічці в сусідніх комірках записане
деяке слово u в алфавіті T. Ліворуч і праворуч від слова на
стрічці перебувають лише символи ‘ ‘. Голівка машини Тюрінґа
читає перший символ слова u, машина перебуває в початковому
стані q0. Надалі поведінку машини повністю визначає функція
переходів.
Означення 2. У кожний момент роботи машини Тюрінґа
голівка
читає на
стрічці
один
символ a.
Послідов-
ність
символів на стрічці x1…xn утворює слово x, а послідовність
ay1…ym – слово y. Ліворуч від x і праворуч від y на стрічці розта-
шовані лише символи ‘ ‘. Слово xqy називається конфігурацією
машини Тюрінґа, а слово x1…xnqay1…ym – конфігурацією в
розширеному вигляді (мал. 2).
За один крок машина Тюрінґа переходить, що позначається
=>, з одної конфігурації до іншої залежно від команди 1–3:
1. якщо σ(q,a) = (p,b,.), то x1…xnqay1…ym=>x1…xnpby1…ym
2. якщо σ(q,a) = (p,b,>), то x1…xnqay1…ym=>x1…xnbpy1…ym
3. якщо σ(q,a) = (p,b,<), то x1…xnqay1…ym=>x1…pxnby1…ym
Якщо машина Тюрінґа повинна обробити слово u, то q0u – її
початкова конфігурація. Заключна конфігурація – xqsy. У за-
ключній конфігурації, тобто коли машина потрапляє в стан qs,
машина зупиняється, а результатом обробки слова u є слово xy.
Кожна машина Тюрінґа M задає деяке відображення T*⟶T*.
Машина M для слова u ∈ T* починає свою роботу з початкової
конфігурації q0u і:

25
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Ніколи НЕ зупиниться. У цьому разі вважається, що


M(u) – невизначено.
 Зупиниться в конфігурації xqsy. Якщо xy = ανβ, де
α,β∈{‘ ‘}* – порожні слова, перший і останній символи
слова ν не символ-проміжок, або ν = ε, то M(u)= ν.
Означення 3. Алфавіт T машини M часто ділять на два алфа-
віти T = Tm∪Ta, що не перетинаються: Tm∩Ta = {}, Tm – основний
і Ta – додатковий. У разі такого поділу вважають, що машина
задає відображення Tm*⟶Tm*, а додатковий алфавіт використо-
вується лише в процесі обробки. Машина M для слова u ∈ Tm*
починає роботу з початкової конфігурації q0u і:
 Ніколи НЕ зупиниться. У цьому разі вважається, що
M(u) – невизначено.
 Зупиниться в конфігурації xqsy i xy ∉ (Tm∪{‘ ‘})*. У цьому
разі вважається, що M(u) – невизначено.
 Зупиниться в конфігурації xqsy i xy ∈ (Tm∪{‘ ‘})*. Якщо
xy = ανβ, де α,β∈{‘ ‘}* – порожні слова, перший і останній
символи слова ν не символ-проміжок, або ν = ε, то
M(u) = ν.
Кожна машина Тюрінґа M, коли потрапляє в заключний стан
qs, зупиняється й більше не виконує ніякої роботи. Тому якщо
вважати, що функція переходів  машини не визначена в точках
(qs,a), то отримаємо машину Тюрінґа, яка поводиться так само,
як і початкова.
Означення 4. Така машина Тюрінґа, функція переходів якої
невизначена лише в заключному стані, називається стандарт-
ною.
Надалі вважаємо, що функція переходів (таблиця переходів)
σ визначена для всіх станів, окрім заключного. Якщо (q,a) ∉ Dσ,
для q ≠ qs, то вважаємо, що неявно вводиться довизначення
функції переходів σ(q,a) = (q,a,.).

26
Моделі обчислень у програмній інженерії

1.3.3. Приклад роботи машини Тюрінґа


Приклад 1. Розглянемо машину Тюрінґа example= ({q0, qs},
{a, b, c}, σ, q0, qs), у якої множина станів Q = {q0, qs}, вхідний
алфавіт T = {a, b, c} і функція переходів σ визначена так: σ(q0,
a) = (qs, b, .) ,σ(q0, b) = (q0, b, >) ,σ(q0, c) = (q0, c, >) ,σ(q0, _) =
(q0, _, >).
Машина example, зчитавши символ a, перетворює його на b і
зупиняється (переходить у заключний стан qs). Читаючи інший
символ – b, c або проміжок (_), – машина example лише пересу-
ває голівку праворуч. Простежимо роботу машини на словах
bcaa, ac і cb:
 u = bcaa
o Початкова конфігурація: q0bcaa
o Робота машини: q0bcaa =>bq0caa =>bcq0aa
=>bcqsba
o Відображення: example (bcaa) = bcba
 u = ac
o Початкова конфігурація: q0ac
o Робота машини: q0ac =>qsbc
o Відображення: example (ac) = bc
 u = bc
o Початкова конфігурація: q0bc
o Робота машини: q0bc =>bq0c =>bcq0_ =>bc_q0_
=>bc__q0_ => …
o Відображення: example (bc) = невизначено

1.3.4. Часткова функція, її обчислення


Нехай f – часткова функція натурального аргументу з об-
ластю визначення Df⊆Nk:
f: Nk–>N.
Якщо (a1, …, an) ∈ Df і значення функції f у цій точці b, то
f(a1, …, an) = b.
Якщо (a1, …, an) ∉ Df, то значення функції в цій точці не
визначено, що записуватимемо як f(a1, …, an) = невизначено.

27
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Означення 4. Машина Тюрінґа M обчислює часткову


функцію f: Nk–>N, якщо:
 M(|a1#|a2…#|an) = |b тоді й тільки тоді, коли (a1, …, an) ∈ Df
і f(a1, …, an) = b
 M(|a1#|a2…#|an) = невизначено (НЕ зупиняється) тоді й
тільки тоді, коли (a1, …, an) ∉ Df, тобто f(a1, …, an) =
невизначено.
Функція f: Nk–>N обчислювана за Тюрінґом, якщо існує ма-
шина Тюрінґа M, що обчислює функцію f.
Основний алфавіт у машини Тюрінґа, що обчислює часткову
функцію, завжди Tm = {|,#}. Поведінка такої машини Тюрінґа М
на вхідному слові, формат якого відмінний від |a1#|a2…#|an,
не розглядається.
Приклад 2. Машини Тюрінґа, що обчислюють прості функції
id(x) = x, і undefined(x):
 undefined(x) = ({q0, qs}, {|,#}, σ1, q0, qs) із функцією
переходів
σ1(q0, |) = (q0, |, .),σ1(q0, #) = (q0, #,.),σ1(q0, _) = (q0, _,.)
 id(x) = ({q0, qs}, {|,#}, σ1, q0, qs) із функцією переходів
σ2(q0, |) = (qs, |, .) ,σ2(q0, #) = (qs#, .) ,σ2(q0, _) = (qs_, .)
Приклад 3. Функція f(x,y) = x+y обчислювана за Тюрінґом.
Це робить наступна машина Тюрінґа addition = (Q, T, σ, q0, qs) з
алфавітом T = Tm = {|,#}, множиною станів Q = {q0, q1, qs} і
функцією переходів σ(q0, |) = (q1, _, >) ,σ(q0,#|) = (qs, _, .)
,σ(q1, |) = (q1,|, >) ,σ(q1, #) = (qs|, .).
Якщо на вході машини – слово, що починається з | (x>0), то
програма замінює перший символ | на проміжок ‘ ‘, знаходить
роздільний символ # і замінює його на |, створюючи
послідовність з (x+y)-символів. Якщо на вході – слово, що почи-
нається з # (x=0), то програма просто стирає (замінює на про-
міжок) роздільний символ #.

28
Моделі обчислень у програмній інженерії

1.3.5. Діаграма машини Тюрінґа


Означення 5. Діаграма машини Тюрінґа – це граф, вершини
якого – стани, а
дуги визнача-
ються таблицею
переходів. Над
дугою вказується
символ, що читає
машина, а під
дугою – дія, яку машина виконує. Елементу функції переходів
(q,a) = (p,b,m) відповідає дуга, що веде зі стану q у стан p, над
якою перебуває символ a, а під дугою – опис дії (b,m). Початко-
вий стан позначається написом назви машини, а заключний –
символом *.
З метою спрощення на діаграмі:
 Не вказують-
ся дії, коли
машина не
рухає
голівки (.).
 Не вказуються дії, коли голівка не змінює символу, що
читає.
 Декілька
(пара-
лельних,
що почи-
наються й закінчуються в однакових станах) дуг, із якими
пов’язана одна й та сама дія, замінюються однією дугою.
Під дугою вказується дія, а над дугою перераховуються
символи, які читає голівка.

29
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Якщо машина зі стану q переходить у стан p і виконує


одну й ту саму роботу незалежно від символу, який читає
голівка, то на діаграмі під дугою, що з’єднує вершини q і
p, лише вказується робота, яку виконує машина (b,m).
На малюнках показані повна (мал. 3) та спрощені діаграми
(мал. 4–5) машини Тюрінґа example та спрощена діаграма ма-
шини addition (мал. 6).
Головне призначення діаграм – наочно показати структуру
функції переходів машини Тюрінґа. На жаль, для виконання
навіть простих задач (додавання двох чисел) потрібно будувати
складні машини Тюрінґа. Бажано мати певний загальний метод,
який би дозволяв описувати будову («структуру»), і на її основі
«збирати» машини Тюрінґа.

1.3.6. З’єднання машин Тюрінґа


Означення 6. Існують дві стандартні машини Тюрінґа M1 =
(Q1, T, σ1, q01, qs1) і M2 = (Q2, T, σ2, q02, qs2), у яких спільний
алфавіт T і різні множини станів Q1 ∩ Q2 = {}. Машина M12 =
(Q1\{𝑞𝑠1} ∪Q2, T, σ12, q01, qs2), у якої σ12 визначається так:
 якщо (a,q) ∈ Dσ1 і σ1(a,q) = (p,b,m), p ≠ qs1, то σ12(a,q) =
(p,b,m) = σ1(a,q);
 якщо (a,q) ∈ Dσ1 і σ1(a,q) = (qs1,b,m), то σ12(a,q) = (q02,b,m);
 якщо (a,q) ∈ Dσ2 і σ2(a,q) = (p,b,m), то σ12(a,q) = (p,b,m) =
σ2(a,q);
називається з’єднанням машин M1 і M2. Позначається M12 =
M1∘M2.
Машина M12 послідовно виконує роботу машини M1 і ма-
шини M2, працюючи так:
 Починає в початковій конфігурації машини M1 і працює
згідно з функцією переходів (програми) σ1.
 Потрапивши в конфігурацію, що ввела у заключний стан
qs1, нова машина згідно з новою функцією переходів
σ12(a,q) = (q02,b,m) переходить у початковий стан ма-
шини M2.

30
Моделі обчислень у програмній інженерії

 Далі працює як машина M2, згідно з функцією переходів


(програми) σ2, доки не потрапить у заключний стан.
Обмеження Q1 ∩ Q2 = {} не критичне, тому що в разі потреби
можна просто перейменувати стани Q2, не змінюючи поведінки
машини M2 цілком.
У загальному випадку машина Тюрінґа, перебуваючи в
стані s і читаючи символи на стрічці, може водночас: змінити
стан t, записати новий символ b і перейти ліворуч – L або
праворуч – R. Для спрощення й можливості прозоро відобразити
дії машини Тюрінґа на діаграмах надалі будемо використовувати
лише можливість одночасно:
 Незалежно від символу на стрічці, змінити стан і запи-
сати символ b (машина b).
 Незалежно від символу на
стрічці, змінити стан і пе-
рейти ліворуч – L (маши-
на L).
 Незалежно від символу на
стрічці, змінити стан і пе-
рейти праворуч – R (ма-
шина R).
 Залежно від символу на стрічці, змінити стан, не
змінюючи символу на стрічці й залишаючи голівку на
місці.
Діаграми перших трьох машин наведені на мал. 7. Їхні про-
грами для алфавіту T (b ∈ T) мають вигляд:
 b = ({q0, qs}, T, σb, q0, qs), де σb(q0,X) = (qs,b,.) для довіль-
ного X ∈ T.
 L = ({q0, qs}, T, σL, q0, qs), де σL(q0,X) = (qs,X,<) для довіль-
ного X ∈ T.
 R = ({q0, qs}, T, σR, q0, qs), де σR(q0,X) = (qs,X,>) для
довільного X ∈ T.
Таке спрощення дає можливість зобразити поведінку ма-
шини Тюрінґа на діаграмі, що не має станів, відображаючи

31
Глибовець М. М., Кирієнко О. В., Проценко В. С.

спрощені дії або символом b, L, R, або розгалуженням, про-


ставивши над дугами, що виходять із нього, символи, які машина
читає на стрічці.

1.3.7. Базові машини Тюрінґа


Побудуємо базові машини Тюрінґа, використовуючи які
можна реалізувати основні числові функції: множення,
віднімання тощо. У машинах Тюрінґа рідко використовують
додаткові символи. Найчастіше інформація, що обробляється
машиною, представлятиме неперервні послідовності символів |,
що задають натуральні числа, поділені символом проміжку ‘_‘.
Алфавіт цих машин T={‘|’,’#’} і символ проміжку ‘_’.
Приклад 4. Машина Тюрінґа right (діаграму зображено на
мал. 8) переміщує голівку праворуч і переходить на перший
праворуч символ ‘_’, пропускаючи всі (можливо, жодного) сим-
воли ‘|’. Алфавіт T = {|}.
 right = ({q0,q1,qs}, {‘|’},
σ, q0, qs) і функція пе-
реходів σ(q0, _) = (q1,
_,>),σ(q0, |) = (q1,|,>),
σ(q1, |) = (q1,|,>),σ(q1, _) = (qs, _,.).
Приклад 5. Машина Тюрінґа left (діаграму зображено на
мал. 9) переміщує голівку ліво-
руч і переходить на перший
ліворуч символ ‘_’, пропуска-
ючи всі (можливо, жодного)
символи ‘|’. Алфавіт T = {|}.
 left = ({q0,q1,qs}, {‘|’}, σ, q0, qs) і функція переходів σ(q0,
_) = (q1, _,<),σ(q0, |) = (q1,|,<), σ(q1, |) = (q1,|,<),σ(q1, _) =
(qs, _,.).
Приклад 6. Машина Тюрінґа copy (діаграму зображено на
мал. 10) копіює послідовність з x символів ‘|’ за наступну за нею
послідовність з y символів ‘|’, отримуючи послідовність з (y+x)
символів ‘|’. Алфавіт T={‘|’,’_’}. Умовно роботу нової машини

32
Моделі обчислень у програмній інженерії

можна описати так (* позначає місце, де перебуває голівка ма-


шини):
*|.x.|_|.y.|_ =*> *|.x.|_|.y+x.|_ (x>=0,y>=0)
Машина використовує дві машини Тюрінґа, що отримуються
в результаті з’єднання раніше написаних машин:

 ‘_’ ∘right∘right∘ ‘|’ ∘left∘left∘‘|’∘ R (8 машин)


 left∘R (2 машини)
Приклад 7. Машина
Тюрінґа delete вилучає
праворуч усі символи
‘|’ до першого символу
‘_’.
Алфавіт T={‘|’}.
Діаграму машини наве-
дено на мал. 11. Маши-
на delete отримується в
результаті з’єднання
двох найпростіших
машин (машини, що записує символ проміжку ‘_’, і машини, що
переміщує голівку праворуч – R)‘_’∘R.
Приклад 7. Машина initial (діаграму зображено на мал. 12)
перетворює початкове слово машини Тюрінґа, що обчислює
числову функцію, замінюючи розділові символи ‘#’ на символи
‘_’. Алфавіт T={‘|’,’#’}.

1.3.8. Числові функції


Приклад 8. Машина Тюрінґа const2 обчислює функцію
const2(x) = 2. Її діаграму наведено на мал. 13. Машина (результат
з’єднання п’яти раніше побудованих машин) спочатку стирає всі
символи |, а потім

33
Глибовець М. М., Кирієнко О. В., Проценко В. С.

записує два нових символи |.


Приклад 9. Допоміжна машина multiply з діаграмою,
зображеною на мал. 14, формує послідовність символів ‘|’
довжини x*y безпосередньо за двома послідовностями символів
‘|’ довжини x>0 і y>0 відповідно. Алфавіт T={‘|’}.

Умовно роботу цієї машини можна описати так (* позначає


місце, де перебуває голівка машини):
*|.x.|_|.y.|_ =*>*|.x.|_|.y.|_|.x*y.| (x>0,y>0)
Приклад 10. Використовуючи попередню машину як основу,
можна побудувати машину Тюрінґа, що обчислює функцію
multiplication(x,y) = x*y з діаграмою (мал. 15):

Приклад 11. В основі машин Тюрінґа, що обчислюють різні


варіанти функцій з операцією віднімання, лежить проста до-
поміжна машина raise1.
Машина Тюрінґа raise1 (мал. 16) замінює крайні символи ‘|’
у двох сусідніх послідовностях, що містять принаймні один сим-
вол ‘|’, на символи ‘_’. Алфавіт T={‘|’,’_’}.

На мал. 17 зображено діаграму машини Тюрінґа substraction-


Abs, яка обчислює всюди визначену функцію віднімання за мо-
дулем substractionAbs (x,y) = |x-y|. Основу цієї машини утворює
допоміжна машина raise1.

34
Моделі обчислень у програмній інженерії

З’єднання машини substractionAbs із дев’ятьма простими


машинами утворює машину Тюрінґа, що обчислює функцію
substract4Abs (x) = |x-4| (мал. 18).

1.3.9. Задачі
1. Побудувати машину Тюрінґа clearEndOne, яка стирає остан-
ній символ вхідного слова в алфавіті T = {a,b}.
2. Побудувати машину Тюрінґа addEndOne, яка дописує в
кінець вхідного непорожнього слова x1x2…xn в алфавіті T =
{a,b} перший символ x1.
 результат невизначено для порожнього вхідного слова
3. Побудувати машину Тюрінґа transferIn10, яка переводить
натуральні числа з одиничної системи числення (число n ко-
дується n-символами |) в десяткову систему.
4. Побудувати машину Тюрінґа copyWord, яка кожне слово x в
алфавіті T = {a, b} переводить у слово xx.
5. Побудувати машину Тюрінґа, яка обчислює функцію
 expression1(x,y) = |y-(x+2)|
6. Побудувати машину Тюрінґа, яка обчислює функцію
 expression2(x,y,z) = z-(x+y)
o невизначена при z<x+y
7. Побудувати машину Тюрінґа, яка обчислює функцію
 exponential2(x) = 2x
8. Побудувати машину Тюрінґа, яка обчислює функцію
 expression4(x) = 3x+x
9. Побудувати машину Тюрінґа, яка обчислює функцію

35
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 divide3Remainder(x) = xmod3
o xmod3 – натуральне число – залишок від ділення
x на 3
10. Побудувати машину Тюрінґа, яка обчислює функцію
 dividedQuatient(x,y) = xdivy
o xdivy – натуральне число – частка від ділення x
на y
o xdiv0 = 0
12. Побудувати машину Тюрінґа, яка обчислює функцію
gcd(x,y) = НСД(x,y)
 НСД(x,y) – найбільший спільний дільник натуральних чи-
сел x та y
o НСД(x,0) = 0
o НСД(0,y) = 0

1.4. Машини з необмеженими регістрами


1.4.1. Програма та її виконання
Машина з необмеженими регістрами – простий ідеалізова-
ний комп’ютер, що має нескінченну кількість регістрів та про-
граму, яку він може виконати. Кожний регістр може містити
довільне натуральне число. Регістри позначаються R1, R2, …, а
їхній вміст – r1, r2, … відповідно. Машина послідовно виконує
команди програми, змінюючи зміст регістрів.
Означення 1. Програма машини (МР-програма) – це впо-
рядкований список команд P = <I1, …, Ik>. Кожна команда в
програмі має власний номер – порядок її розташування. Є ко-
манди лише чотирьох видів:
 Z(n) – замінює вміст регістра з номером nRn на 0, позна-
чається rn := 0. Зміст інших регістрів не змінюється.
 S(n) – збільшує вміст регістра з номером nRn на 1, позна-
чається rn := rn+1. Зміст інших регістрів не змінюється.
 T(m,n) – замінює вміст регістра з номером nRn на число,
що міститься в регістрі з номером mRm, позначається rn :=
rm. Змінює зміст лише регістру Rn.

36
Моделі обчислень у програмній інженерії

 J(m,n,q) – команда умовного переходу, яка може змінити


послідовне виконання команд програми. Якщо вміст
регістрів Rn і Rm збігаються, то наступною виконувати-
меться команда з номером q. Зміст усіх регістрів не
змінюється.
Означення 2. Конфігурація машини (r1, r2, …) – це по-
слідовність, що містить вміст усіх регістрів машини в деякий
момент її роботи.
Щоб виконати обчислення, машині з необмеженими регіст-
рами (МНР) потрібно надати програму P і початкову конфігура-
цію – послідовність натуральних чисел (a1, a2, …), що вказує
значення в регістрах R1, R2, … перед початком обчислення.
Програма P = <I1, …, Ik> виконується послідовно, почина-
ючи з команди з номером 1. Змінити послідовне виконання ко-
манд програми може лише команда J(m,n,q). Якщо номер цієї
команди p і вміст регістрів Rn і Rm збігається, то далі виконува-
тиметься команда з номером q, в іншому разі наступною вико-
нуватиметься команда з номером (p+1). Виконання програми P
зупиниться, коли необхідно буде виконати команду з номером
q>k. Це можливо у випадках:
 Виконується команда J(m,n,q), rn = rm, наступною по-
трібно виконати команду з номером q > k.
 Виконується остання команда програми Ik, і далі по-
трібно виконати команду з номером (k+1) > k.
Значення в регістрах R1, R2, … – послідовність натуральних
чисел (b1, b2, … ) у разі зупинки програми – заключна конфігу-
рація.Таке виконання обчислення позначають:
P(a1, a2, … )↑(b1, b2, …).
У деяких випадках машина, почавши виконувати програму P
в початковій конфігурації (a1, a2, …), ніколи не зупиниться. Це
позначають:
P(a1, a2, … ) ↑.
У кожній програмі P завжди використовується скінченна
кількість регістрів ρ(P). Тому більшість регістрів (номери яких

37
Глибовець М. М., Кирієнко О. В., Проценко В. С.

більше ρ(P)) завжди матиме значення, рівне 0, а всі конфігура-


ції – це скінченні списки.

1.4.2. Блок-схеми та програми


У багатьох випадках перед написанням тексту програми для
зручності спочатку будують її блок-схему.
Означення 4. Блок-схема – це орієнтований граф, вершинами
якого є елементи:
 Початок – овал із назвою програми та її аргументами,
біля якого інколи розміщують коментар, пов’язаний із
початком пунктиром. Із початку завжди виходить одна
дуга й не входить жодної. Описує початок обчислення.
 Кінець – перекреслене коло, у яке входить одна дуга й не
виходить жодної. Завершення обчислення.
 Об’єднання – коло, у яке входить декілька дуг, а вихо-
дить одна. Об’єднує декілька шляхів обчислення в один.
 Обробка – прямокутник, у який входить одна дуга і з
якого виходить одна дуга. Вказує, як змінюється зміст
регістрів, наприклад:
o rn := 0 – встановлення в нуль;
o rn := rn+1 – збільшення на одиницю;
o rn := rm – пересилання.
 Рішення – ромб, у середи-
ні якого вказана умова. У
ромб входить одна дуга і з
ромба виходить дві дуги з
позначками «Так» і «Ні».
За блок-схемою зручно за-
писувати готову програму.
Приклад 1. Побудуємо МР-
програму, яка обчислює функцію
notSignum(x) = nsg(x).
 Для натуральних чисел
nsg(x) = 1 при x = 0 і

38
Моделі обчислень у програмній інженерії

nsg(x) = 0 при x > 0.


Блок-схему програми наведено на мал. 1, а саму програму – в
таблиці 1.
Таблиця 1
Програма
№ Команда Коментар заносить у
1 Z(2) r2 := 0 регістр 2 R2
2 J(1,2,5) if r1=r2 then goto 5 число 0,
3 Z(1) r1 := 0 необхідне
4 J(1,1,6) goto 6 – безумовний перехід для
5 S(1) r1 := r1+1 порівняння
з аргументом x (регістр R1) і встановлює результат у регістрі 1 R1
(залежно від результатів порівняння).

1.4.3. Обчислення числової функції


Розглядається частково-визначена функція натурального ар-
гументу f, що має k (k>0) аргументів f: Nk–>N з областю визна-
чення Df⊆Nk.
Означення 3. МР-програма P обчислює функцію f, якщо:
 P(a1, a2, …, ak )↑(b, …) тоді й тільки тоді, коли (a1, …,
an) ∈ Df і f(a1, …, an) = b.
 P(a1, a2, …, ak)↑ тоді й тільки
тоді, коли (a1, …, an) ∉ Df, i f(a1,
…, an) = невизначено.
Функція f обчислювана, якщо існує
МР-програма P, що її обчислює.
Приклад 2. Побудувати МР-
програму, що обчислює функцію
addition(x,y) = x+y.
Ідея обчислення: порахувати, скіль-
ки одиниць входить в y, і додати їх до x.
Для цього використовується регістр R3.
На початку в нього заноситься 0, збіль-
шуючи на 1 на кожному кроці циклу

39
Глибовець М. М., Кирієнко О. В., Проценко В. С.

регістри R1 і R3, отримуємо в R3 – y, а в R1 – x+y. Фактично


регістр R3 являє собою лічильник кількості одиниць у числі y.
 Початкова конфігурація: (x, y, 0).
 Заключна конфігурація: (x+y, y, y).
Таблиця 2

№ Команда Коментар
1 Z(3) r3 := 0
2 J(3,2,6) ifr3=r2 then goto 6
3 S(1) r1 := r1+1
4 S(3) r3 := r3+1
5 J(1,1,2) goto2 – безумовний перехід
Блок-схему програми наведено на мал. 2, а саму програму з
коментарями до кожної команди – в таблиці 2.
Послідовність команд 2, 3, 4 і 5, що може виконуватися ба-
гато разів, називають циклом:
 команда 2 – перевірка умови циклу;
 команди 3–5 – тіло циклу.
Під час виконання цієї програми для довільної початкової
конфігурації машина завжди зупиниться:
 Якщо y=0, то умова ко-
манди 2 виконається від-
разу й машина спробує зро-
бити перехід на команду 6,
що призведе до зупинки.
 Якщо y>0, то умова коман-
ди 2 виконається після y
кроків тіла циклу.
Приклад 3. Частково-визначена
функція substraction(x,y) = x-y, ви-
значена лише для аргументів x≥y,
обчислювана.
Блок-схема потрібної МР-про-
грами має структуру, схожу на по-

40
Моделі обчислень у програмній інженерії

передню, але тепер у регістрі R3 підраховується, скількох


одиниць не вистачає числу y, щоб зрівнятися з числом x. Тому
на кожному кроці циклу збільшуються регістри R2 і R3.
 Якщо x≥y, то на якомусь кроці циклу значення регістрів
R1 і R2 збіжаться, виконання програми зупиниться.
 Якщо x<y, відразу r2>r1, і виконання операції r2 := r2+1
лише збільшуватиме значення в регістрі R2. Виконання
про-
Таблиця 3
грами
№ Команда Коментар
ніколи
1 Z(3) r3 := 0
НЕ
2 J(1,2,6) ifr1=r2 then goto 6
зупи-
3 S(2) r2 := r2+1
ниться.
4 S(3) r3 := r3+1
5 J(1,1,2) goto2 – безумовний перехід
6 T(3,1) r1 := r3
1.4.4. Перетворення програм
Означення 4. P1 і P2 – еквівалентні програми, якщо під час
роботи на однакових конфігураціях
вони:
 обидві зупиняться на однакових
заключних конфігураціях;
 обидві НЕ зупиняться.
Означення 5. Програма P, що
містить |P| команд із довжиною |P|, –
стандартна, якщо для довільної ко-
манди переходу J(m,n,q) виконується
умова q≤|P|.
Теорема 1. Для кожної програми P
існує еквівалентна стандартна про-
грама P1.
Доведення. Якщо програма P = <I1,
I2, …, Ik> не є стандартною, то побудуємо програму P1 = <C1, C2,
…, Ck, Ck+1>, у якої:

41
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Ck+1 = T(1,1).
 Ct = It, t<k+1, якщо команда It – Z, S, T.
 Ct = J(m,n, q1), t<k+1, якщо It = J(m,n, q) і q1 = min(q,k+1).
Нескладно з’єднати дві стандарні програми P = <I1, I2, …, Ik>
і Q = <V1, V2, …, Vt> так, щоб нова програма W спочатку обчис-
лювалася як програма P, а потім продовжила обчислення як про-
грама V, початковою конфігурацією якої є заключна конфігура-
ція програми програма P. Така програма фактично реалізує по-
слідовне виконання програм P і V.
Програма W = <I1, I2, …, Ik, V11, V12, …, V1t>, де:
 V1i = Vi, якщо команда Vi є Z, S, T.
 V1i = J(m,n,q+k), якщо Vi = J(m,n, q).
Використовуючи з’єднання програм і програму, що об-
числює addition, можна легко побудувати програми, що об-
числюють наступні функції:
 addition3(x,y,z) = x+y+z
 mult3(x) = 3*x
На мал. 4 і 5 наведено блок-схеми цих програм, на кожній із
них програми, що обчислюють addition, позначаються прямо-
кутником із подвійними бічними сторонами.
Можна виконати складнішу операцію: вставити стандартну
програму Q = <V1, V2, …, Vt> у середину програми P = <I1, I2, …,
Ik> після команди з номером m<k і отримати нову програму W =
<I11, I12, …, I1m, V11, V12, …, V1t, I1m+1, …, I1k>, у якої:
 I1i = Ii, якщо команда Ii є Z, S, T.
 I1i = J(m1,n1,q), якщо Ii = J(m1,n1, q) і q≤m.
 I1i = J(m1,n1,q+t), якщо Ii = J(m1,n1, q) і q>m.
 V1i = Vi, якщо команда Vi є Z, S, T.
 V1i = J(m1,n1,q+m), якщо Vi = J(m1,n1, q).

1.4.5. Приклади деяких програм


Приклад 4. Функція multiplication(x,y) = x*y обчислювана.
За означенням x*y = x+x+…+x (y разів), тому для отримання
бажаної програми можна скористатися програмою addition(x,y).

42
Моделі обчислень у програмній інженерії

На мал. 6 наведено блок-схему програми, у якій використо-


вується цикл (із лічильником у регістрі 5), що виконується y
разів.
Оскільки регістр 2 використовується під час виконання
програми addition, то початкове значення y (регістр 2) на почат-
ку виконання програми зберігається в регістрі 4.
Приклад 5. Функція substract1(x) = x÷1, у якої x÷1 = 0, коли
x = 0, і x÷1 = x-1, коли x>0, обчислювана.
Блок-схему програми наведено на мал. 7. Значення x перепи-
сується в регістр 3 і порівнюється з нулем. Якщо значення x
більше 0, то в регістрах 1 і 2 розташовуються два послідовні
числа 0 і 1, які одночасно збільшуються на одиницю, допоки
значення в регістрі 2 не стане рівним x.

1.4.6. Задачі
Побудувати МР-програму, яка обчислює функцію:
1. signum(x) = sg(x)
 для натуральних чисел sg(x) = 0 при x=0 і sg(x) =
= 1 при x>0
2. expresion1(x,y,z) = z-(x+y)

43
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 невизначена при z<(x+y)


3. less(x,y)
 набуває значення 1 при x<y та 0 при x≥y
4. lessEqual(x,y)
 набуває значення 1 при x≤y та 0 при x>y
5. maximum(x,y) = max(x, y)
6. expression2(x,y) = (x + 1)* y
7. factorial(x) = x!
 0!=1, x! = 1*2*…*x, якщо x>0
8. expresion3(x,y) = 3x+2*y
9. expresion4(x,y) = (x÷y)div3
 (x÷y)div3 – натуральне число – частка від ділення
(x÷y) на 3
10. dividedQuotient(x,y) = xdivy
 xdivy – натуральне число – залишок від ділення
x на y
 xdiv0 = 0
11. gcd(x,y) = НСД(x,y)
 НСД(x,y) – найбільший спільний дільник нату-
ральних чисел x та y
 НСД(x,0) = 0
 НСД(0,y) = 0

1.5. Системи Поста


1.5.1. Правило виведення і безпосереднє виведення
Означення 1. Правило виведення (продукція) в алфавіті T є
v –> u, де:
 слово v = α0S1α1…αm-1Smαm;
 слово u = β0Sk0β1…βn-1Sknβn;
 S1, …, Sm – метасимволи, що не входять до алфавіту T, {S1,
…, Sm} ∩ T = {};
 Sk0, …, Skn – усі належать множині {S1, …, Sm};
 α0, α1, …, αm-1, αm, β0, β1, …, βn-1, βn – слова в алфавіті T.

44
Моделі обчислень у програмній інженерії

Означення 2. Правило виведення v –> u, або α0S1α1…αm-1Smαm


–> β0Sk0β1…βn-1Sknβn, можна безпосередньо застосувати до слова σ
в алфавіті T, якщо існують такі слова φ1, …, φm в алфавіті T, що
σ = α0φ1α1…αm-1φmαm. Результатом безпосереднього застосування
його до слова σ є слово τ = β0φk0β1…βn-1φknβn, що позначається
σ=>τ.
Зауважимо, що якщо у слові σ слова φ1, …, φm замінити на
метасимволи S1, …, Sm, то отримаємо праву частину правила
виведення: слово v = α0S1α1…αm-1Smαm. Безпосереднє виведення
σ=>τ пов’язує із кожним метасимволом слово в алфавіті T: S1-φ1,
…, Sm–φm. Слово τ – результат заміни у правій частині правила
виведення v–>u кожного метасимволу S1, …, Sm на відповідне
слово φ1, …, φm.
Приклад 1. Якщо алфавіт T = {a, b} і правило виведення є
aS1bS2 → S2aS2a, то:
 до слова aba це правило можна безпосередньо застосу-
вати лише одним способом (S1 = ε, S2 = a) aba => aaaa;
 до слова abba це правило можна безпосередньо застосу-
вати двома способами (S1 = ε, S2 = ba) abba => baabaa
і (S1 = b, S2 = a) abba => aaaa;
 до слова ba це правило не можна безпосередньо застосу-
вати жодним способом.
Приклад 2. Алфавіт T = {a, b} і правилом виведення є
S1bS2aaS3b → S3abS1. До слова babaabbaab це правило виведення
можна застосувати шістьма різними способами. (Підслова, що
зв’язуються з метасимволами S1, S2, S3 виділяються у слові
babaabbaab символом |.)
1. ||b|abaabb|aa||b (S1 = ε, S2 = abaabb, S3 = ε)
2. |ba|b|aabb|aa||b (S1 = ba, S2 = aabb, S3 = ε)
3. |babaa|b|b|aa||b (S1 = babaa, S2 = b, S3 = ε)
4. |babaab|b||aa||b (S1 = babaab, S2 = ε, S3 = ε)
5. ||b|ab|aa|bbaa|b (S1 = ε, S2 = ab, S3 = bbaa)
6. |ba|b||aa|bbaa|b (S1 = ba, S2 = ε, S3 = bbaa)

45
Глибовець М. М., Кирієнко О. В., Проценко В. С.

1.5.2. Означення системи Поста


Означення 3. (Канонічна) система Поста P = (T, A, Q), де:
 T – скінченний алфавіт;
 A – скінченна множина слів в алфавіті T – аксіоми P;
 Q – скінченна множина правил виведення (продукцій).
Множина Q правил виведення системи P задає на множині T*
відношення безпосереднього виведення =>. σ =>τ, якщо в мно-
жині Q існує правило виведення, безпосереднє застосування
якого до слова σ дозволяє отримати слово τ.
Позначатимемо σ *= >τ (слово τ виводиться зі слова σ у си-
стемі P), якщо слово τ отримується зі слова σ за допомогою скін-
ченної послідовності застосувань правил виведення із Q. Форма-
льно σ *= >τ тоді й тільки тоді, коли:
 σ = τ,
 або існує послідовність слів σ0 = σ, σ1, …, σn = τ в алфа-
віті T таких, що σ0 => σ1, σ1=>σ2, …, σn-1 => σn.
Слово τ породжується системою Поста P, якщо α *=>τ для
певної аксіоми α  A.
Означення 3. Множина, породжена системою Поста, або
множина теорем системи P – це множина Th(P) = {τЄT* | існує
α  A, α*= >τ}.
Приклад 3. Система Поста palindrome, яка породжує всі
паліндроми в алфавіті T = {a, b} є palindrome = (T, A, Q), де T =
{a, b}, A = {a, b, ε}, Q = {S–>aSa, S–>bSb}.
Паліндром – це слово, яке зліва направо і справа наліво чи-
тається однаково. Приклади виведення окремих паліндромів у
системі palindrome:
 З аксіоми a можна безпосередньо вивести або слово aaa,
застосувавши перше правило виведення S–>aSa, або
слово bab, застосувавши друге: S–>bSb. Застосовуючи й
далі одне з цих правил, можна вивести довільний па-
ліндром непарної довжини із символом посередині.
o a =>aaa =>baaab =>abaaaba

46
Моделі обчислень у програмній інженерії

 З аксіоми ε можна вивести всі паліндроми парної дов-


жини.
o ε =>aa =>baab =>abaaba

1.5.3. Функція, обчислювана за Постом


Означення 4. Нехай частково-визначена функція f: Nm–>N
із областю визначення Df – підмножин а Nm, тоді графік функції
f – це множина Gf, що є підмножиною Nm+1,
Cf = {(x1, x2, …, xm, y) | (x1, x2, …, xm)  Df і
y = f(x1, x2, …, xm)}.
m
Функція f: N –>N обчислювана за Постом, якщо її графік –
множина
Cf = {(x1, x2, …, xm, y) | (x1, x2, …, xm)  Df і
y = f(x1, x2, …, xm)}
породжується деякою системою Поста.
Приклад 4. Функція id(x) = x обчислювана за Постом.
Графік цієї функції – множина Gf = {#, |#|, |2#|2, |3#|3, …,
|n#|n, …}, яку породжує наступна система Поста id = ({|, #}, {#},
{S1#S2 –> |S1#|S2}). Одна аксіома й одне правило виведення
послідовно виводять усі слова графіка функції
 # => |#| => ||#|| => |||#||| => …
Приклад 5. Усюди невизначена функція undefined(x) об-
числювана за Постом.
Графік усюди невизначеної функції – порожня множина
Gf = {}, яку породжує довільна система Поста із порожньою
множиною аксіом. Наприклад, undefined = ({|, #}, {}, {}).
Приклад 6. Функція const2(x) = 2 обчислювана за Постом.
Графік цієї функції – множина Gf = {#||, |#||, |2#||, |3#||, …,
|n#||, …}, яку породжує проста система Поста id = ({|, #}, {#||},
{S1#||–> |S1#||}). Знову одна аксіома й одне правило виведення
послідовно виводять усі слова графіка функції:
 #|| => |#|| => ||#|| => |||#|| => …

47
Глибовець М. М., Кирієнко О. В., Проценко В. С.

1.5.4. Прості числові функції


Приклад 7. Функція signum(x) = sg(x) обчислювана за
Постом.
Графік функції Gf = {#, |#|, |2#|, |3#|, …, |n#|, …} і система, що
породжує цю множину signum = ({|, #}, {#, |#|}, {S1#|–> |S1#|}).
Правило виведення може бути застосоване лише до другої
аксіоми та слів, що з неї одержано, виведенням на кожному
кроці зі слова |i#| слово |i+1#|.
Приклад 8. Частково визначена функція divide2Part(x) = x/2,
яка не визначена при непарних натуральних значеннях x, об-
числювана за Постом.
Графік функції Gf = {#, ||#|, |4#|2, |6#|3, …, |2n#|n, …} і він поро-
джується наступною системою divide2Part = ({|, #}, {#}, {S1#S2
–>||S1#S2|}).
Приклад 9. Функція divided2Quotient(x) = xdiv2 обчислювана
за Постом.
Графік функції Gf = {#, |#, ||#|, |3#|, |4#|2, |5#|2, |6#|3, |7#|3, …,
|2n#|n, |2n+1#|n, …} будує схожу на попередню систему із двома
аксіомами divided2Quotient = ({|, #}, {#, |#}, {S1#S2–>||S1#S2|}).
Приклад 10. Функція addition(x,y) = x+y обчислювана за
Постом.
Графік функції Gf = {##, |##|, #|#|, |2##|2, |#|#|2, #|2#|2, |3##|3,
|#|2#|3, #|3#|3, …, |x#|y#|x+y, …} будує система Поста addition =
= ({|, #}, {##}, {S1#S2#S3 –>S1|#S2#S3|, S1#S2#S3 –>S1#S2|#S3|}).
Правила виведення цієї системи відображають наступні дві
прості властивості операції додавання:
 якщо x+y = s, то (x+1) + y = s+1
 якщо x+y = s, то x + (y+1) = s+1
у правилах виведення x відповідає S1, y – S2, а s – S3. Зауважимо,
що більшість слів із графіка можна породити, використовуючи
різні послідовності застосувань правил виведення. Наприклад,
слово ||#||#|||| можна вивести з аксіоми ##, якщо:

48
Моделі обчислень у програмній інженерії

 застосувати спочатку двічі перше правило виведення


S1#S2#S3 –>S1|#S2#S3|, а потім двічі друге правило S1#S2#S3
–>S1#S2|#S3|
o ## => |##| => ||##|| => ||#|#||| => ||#||#||||
 застосувати спочатку двічі друге правило виведення
S1#S2#S3 –>S1|#S2#S3|, а потім двічі перше правило
S1#S2#S3 –>S1#S2|#S3|
o ## => #|#| => #||#|| => |#||#||| => ||#||#||||
Приклад 11. Функція substractionPart(x,y) = x-y, яка не визна-
чена при x<y, обчислювана за Постом.
Графік функції Gf = {##, |#|#, |2#|2#, …, |y#|y#, …, |##|, |2##|2,
|2#|#|, …, |x#|y#|x-y, …} будує система substractionPart = ({|, #},
{##}, {S1#S2#→S1|#S2|#, S1#S2#S3→S1|#S2#S3|}). Для випадку x≥y
слово |x#|y#|x-y відповідає рівності ((x-y) + y) - y = (x-y). Його мо-
жна отримати з аксіоми ##, застосовуючи y разів правило
S1#S2#→S1|#S2|# та (x-y) разів правило S1#S2#S3→S1|#S2#S3| у до-
вільній послідовності.

1.5.5. Розширений алфавіт у системах Поста


Приклад 12. Функція subtraction(x,y) = x÷y, де x÷y = x-y, коли
x≥y, і x÷y = 0 в іншому разі, обчислювана за Постом.
Графік цієї функції Gx÷y включає усі точки графіка поперед-
ньої та додатково наступні {#|#, #|2#, |#|2#, …, |x#|x+k#, …}.
На перший погляд, досить додати лише одне правило
S1#S2#→S1#S2|# для того, щоб отримати необхідну систему Поста
substraction = ({|, #}, {##}, {S1#S2#→S1|#S2|#, S1#S2#S3→S1|#S2#S3|,
S1#S2#→S1#S2|#}).
Але виведення ## => (3 правило) #|# => (2 правило) |#|#| по-
роджує слово |#|#|, яке не належить графіку й відповідає рівності
1÷1 = 1.
Наведена система породжує необхідну множину слів, якщо
заборонити вживати правило 2 після правила 3 й навпаки.
Тобто, якщо правила вживати:

49
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 (правило 1) декілька разів, потім (правило 2) декілька


разів,
 (правило 1) декілька разів, потім (правило 3) декілька
разів,
у цьому разі отримуватимемо лише необхідні слова. З такою
метою в системах Поста використовується розширений алфавіт.
Означення 5. Множина слів в алфавіті T породжувана за
Постом, якщо існує розширений алфавіт T1, який включає в себе
алфавіт T, і [якщо існує] система Поста P = (T1, A, Q) такі, що
Th(P) ∩ T* = X.
Приклад 12 (продовження). Система Поста subtraction = (T1,
{##}, Q) із розширеним алфавітом T1 = {|, #, a} і множиною Q,
що включає сім правил виведення:
1. S1#aS2#→S1|#aS2|#
2. S1#aS2#→S1#bS2#
3. S1#bS2#S3→S1|#bS2#S3|
4. S1bS2→S1S2
5. S1#aS2#→S1#cS2#
6. S1#cS2#S3→S1#cS2|#S3
7. S1cS2→S1S2
породжує в алфавіті T= {|, #} графік функції x÷y. Тобто
Th(subtraction) ∩ T* = Gx÷y.
Множина, породжена системою Поста subtraction, склада-
ється із п’яти множин, що не перетинаються, Th(subtraction) =
M1 U M2 U M3 U M4 U M5. Кожну із множин отримано в резуль-
таті використання одного правила виведення:
 M1 = {#a#, |#a|#, …, |x#a|x#, …} – використання правила 1;
 M2 = {#b#, |#b|#, …, |x#b|x#, …} – використання правила 2;
 M3 = {|y#b#|y, |y+1#b|#|y+1, …, |x+y#b|x#|y, …} – використання
правила 3;
 M4 = {|y##|y, |y+1#|#|y+1, …, |x+y#|x#|y, …} – використання
правила 4;
 M5 = {#c#, |#c|#, …, |x#c|x#, …} – використання правила 5;

50
Моделі обчислень у програмній інженерії

 M6 = {#c|y#, |#c|y+1#, …, |x#c|x+y#, …} – використання пра-


вила 6;
 M7 = {#|y#, |#|y+1#, …, |x#|x+y#, …} – використання пра-
вила 7.
Множина слів в алфавіті T Th(subtraction) ∩ T* = M4 UM7.
Приклад 13. Функція subtractionAbs(x,y) = |x–y| обчислювана
за Постом.
Використавши розширений алфавіт T1 = {|, #, a, b, c}, отри-
муємо:
subtractionAbs = ({|, #, a, b, c}, {#a#},
{S1#aS2#→S1|#aS2|#, S1aS2→S1bS2, S1aS2→S1cS2,
S1#bS2#S3→S1|#bS2#S3|, S1bS2→S1S2,
S1#cS2#S3→S1#cS2|#S3|, S1cS2→S1S2})
Приклад 14. Побудувати систему Поста duplicate, яка пород-
жує усі слова xx, де x – довільне слово в алфавіті T = {a, b}.
Необхідна система Поста використовує розширений алфавіт T1 =
{a, b, c}:
duplicate = ({a, b, c}, {c}, {S1cS2→aS1caS2,
S1cS2→bS1cbS2, S1cS2→S1S2}
Система породжує наступну множину слів Th(duplicate), що
складається із двох множин слів, що не перетинаються, –
M1 і M2:
 M1 = {xcx | x – довільне слово в алфавіті {a, b}}. Слова
множини M1 отримуються з аксіоми c у результаті за-
стосування правил S1cS2→aS1caS2 та S1cS2→bS1cbS2.
 M2 = {xx | x – довільне слово в алфавіті {a, b}}. Слова
множини M2 отримуються зі слів множини M1 у резуль-
таті одного застосування правила S1cS2→S1S2.
Th(duplicate) ∩ {a, b}* = M2

1.5.6. Задачі
Довести, що наступні функції обчислювані за Постом:
1. notSignum(x) = nsg(x)

51
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 для натуральних чисел nsg(x) = 1 при x=0 і


nsg(x) = 0 при x>0
2. divide3Quatient(x) = xdiv3
 xdiv3 – натуральне число – частка від ділення x
на 3
3. expression1(x,y,y) = x+y+z
4. expression2(x,y) = x* (y+1)
5. expression3(x) =3x+x
6. gtyz(x,y,z)
 набуває значення 1 при y+z>x і 0 при y+z≤x

1.6. Частково-рекурсивні функції


1.6.1. Вступ. Базові функції
Першим поняття частково-рекурсивних функцій у науковий
обіг увів Алонзо Черч. Пізніше подібні класи функцій були опи-
сані Куртом Ґедельом та Стівеном Коулом Кліні.
Розглядаються частково-визначені функції натурального ар-
гументу. Кожна така функція f має k (k>0) аргументів – нату-
ральні числа 0,1, …, N, … і результат її обчислення – теж нату-
ральне число – f: Nk–>N. Область визначення функції Df:Df⊆Nk.
Запис f(x1, x2, …, xk) = y показує, що функція f застосовується
до аргументів – натуральних чисел x1, x2, …, xk і її результатом є
натуральне число y.
Означення 1. Всюди визначена функція називається тоталь-
ною функцією.
Загальний метод породження (побудови) обчислювальних
функцій можна описати так:
 Фіксується скінченна множина базових функцій, кожна із
яких легко обчислюється.
 Фіксується скінченна множина конструктивних операцій,
кожна із яких будує нову функцію із раніше побудованих
обчислювальних функцій.

52
Моделі обчислень у програмній інженерії

o З кожною операцією зв’язується метод обчис-


лення значення нової функції через значення
функцій, із яких вона побудована.
 Інших обчислювальних функцій не існує.
Означення 2. Є три базові функції від одного аргументу:
z1(x1) = 0, a1(x1) = x1+1, i11 (x1) = x1
які позначаються у виразах (операторних термах) як z1, a1, 𝑖11 .
Для кожного n (n>1) є n базових функцій селекторів:
i1n (x1,x2,…,xn) = x1, i2n (x1,x2,…,xn) = x2, …, inn (x1,x2,…,xn) = xn
які позначаються у виразах (операторних термах) як 𝑖𝑛1 , 𝑖𝑛2 , …, 𝑖𝑛𝑛 .
Властивість 1. Усі базові функції усюди визначені (тотальні)
й обчислювані.

1.6.2. Операція суперпозиції. Функції-константи


Означення 3. Задано функцію g(x1, …, xn) від n-аргументів та
n-функцій g1(x1, …, xm), …, gn(x1, …, xm) від m-аргументів. Опе-
рація суперпозиції S(g, g1, …, gn) будує з цих m+1-функцій нову
функцію f(x1, …, xm) від m-аргументів.
Значення функції f у точці (a1, …, am) обчислюється так:
 Вираховуються значення b1 = g1(a1, …, am), …, bn = gn(a1,
…, am) функцій g1, …, gn у точці (a1, …, am).
 Вираховується значення c = g(b1, …, bn).
 Покладається f(x1, …, xm) = c.
Випадок суперпозиції, коли n=1 і m=1, у математиці назива-
ють композицією функцій. Використовуючи традиційне позна-
чення композиції функцій, можна записати:
f(x1, …, xm) = g(g1(x1, …, xm), …, gn(x1, …, xm)).
Нехай функції g, g1, …, gn позначаються (задаються) вира-
зами rg, rg1, …, rgn, кожний із яких будується з позначень базових
функцій і функцій, визначених раніше. Тоді наступний вираз
S(rg, rg1, …, rgn) позначає функцію f. У логіці такі вирази часто
називають операторними термами.

53
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Властивість 2. Якщо функції g(x1, …, xn), g1(x1, …, xm), …,


gn(x1, …, xm) – обчислювані й тотальні, то функція f – обчислю-
вана й тотальна.
Приклад 1. Покажемо, як із використанням суперпозиції бу-
дуються функції-константи.
 const0v2(x1,x2) = 0 – функція-константа 0 від двох аргу-
ментів. Цю функцію можна отримати як результат супер-
позиції функції селектора від двох аргументів 𝑖21 (x1,x2)
у функцію z1(x1).
o Функцію const0v2(x1,x2) можна обчислити, вико-
ристовуючи формулу
const0v2(x1,x2) = z1 (i12 (x1,x2)).
o Вираз (операторний терм) S(𝑖21 , z1) позначає цю
функцію.
 const0v3(x1,x2,x3) = 0 – схожа функція, але від трьох аргу-
ментів. Отримується в результаті суперпозиції функції
𝑖31 (x1,x2,x3) у функцію z1(x1) і позначається S(𝑖31 , z1).
 const2(x1) = 2. Ця функція – результат двох суперпозицій.
Спочатку функції z1(x1) у функцію a1(x1). Отримано
функцію a1 (z1 (x1)), її можна позначити як S(a1,z1), по-
вторно підставляється у функцію a1(x1).
o Функція const2(x1) позначається як S(a1,S(a1, z1)),
і її можна обчислити, використовуючи формулу
const1v2(x1,x2) = a1 (a1(z1(x1))).
 const1v2(x1,x2) = 1. Функцію можна отримати в результаті
суперпозиції функції g1(x1,x2) = z1(𝑖21 (x1,x2)) у функцію
g(x1) = a1(x1).
o Функцію const1v2(x1,x2) можна обчислити,
використовуючи формулу const1v2(x1,x2) =
a1(z1(𝑖21 (x1,x2))), і її позначає вираз S(a1,S(z1,𝑖21 )).
 Зауважимо, що функція-константа 0 від одного аргу-
менту – це z1.

54
Моделі обчислень у програмній інженерії

1.6.3. Операція примітивної рекурсії


Означення 4. Операція примітивної рекурсії R(g,h) за двома
функціями g(x1, …, xn) та h(x1, …, xn, xn+1, xn+2), що мають,
відповідно, n і (n+2) аргументи, будує нову функцію f(x1, …, xn,
xn+1), що має (n+1) аргумент.
Для обчислення значення функції f у точці (a1, …, an, an+1)
послідовно вирахувати наступні значення функції f:
 f(a1, …, an, 0) = g(a1, …, an)
f(a1, …, an, 1) = h(a1, …, an, 0, f(a1, …, an, 0))
……………………………………………………….
f(a1, …, an, an+1) = h(a1, …, an, an+1 – 1, f(a1, …, an, an+1 – 1))
Вважають, що функція f(x1, …, xn, xn+1) обчислюється за
наступною рекурсивною схемою (процедурою):
 f(x1, …, xn, 0) = g(x1, …, xn)
 f(x1, …, xn, y+1) = h(x1, …, xn, y, f(x1, …, xn, y))
Якщо функції g і h позначаються (задаються) виразами rg і rh,
то вираз R(rg, rh) позначає функцію f, яку отримуємо в результаті
операції примітивної рекурсії.
Властивість 3. Якщо функції g(x1, …, xn) і h(x1, …, xn, xn+1,
xn+2) – обчислювані й тотальні, то f(x1, …, xn, xn+1) – обчислювана
й тотальна.
На практиці операція примітивної рекурсії найчастіше засто-
совується для визначення функцій одного аргументу f(x1) (n = 0)
або двох аргументів f(x1,x2) (n = 1). У цих випадках функція f
обраховується за схемами:
 Якщо n=0, то g(x1) = a – функція-константа від одного
аргументу, і h(x1,x2)
o f(0) = a – константа
o f(y+1) = h(y,f(y))
 Якщо n=1, то g(x1) – довільна функція від одного аргу-
менту, і h(x1,x2,x3)
o f(x1,0) = g(x1)
o f(x1,y+1) = h(x1,y,f(x1,y))

55
Глибовець М. М., Кирієнко О. В., Проценко В. С.

1.6.4. Прості арифметичні функції


Розглянемо декілька прикладів побудови найпростіших
арифметичних функцій.
Приклад 2. notSignum(x1) = nsg(x1)
Функція nsg(x1) = 1 при x1=0 і nsg(x1) = 0 при x1>0. Цю
функцію можна обчислити за схемою:
 nsg(0) = 1
 nsg(y+1) = 0
Якщо покласти g(x1) = 1 та h(x1,x2) = 0, то функцію nsg
можна отримати в результаті операції примітивної рекурсії
R(g,h). Функція g(x1) – функція-константа 1 від одного аргу-
менту, яку можна отримати в результаті суперпозиції функції
z1(x1) у функцію a1(x1)S(a1, z1). Функція h(x1,x2) =0 – функція-
константа 0 від двох аргументів, яку раніше визначали як
const0v2. Оскільки функції g(x1) = 1 та h(x1,x2) = 0 можна позна-
чити виразами S(a1, z1) і S(𝑖21 , z1), то функція notSignum позна-
чається виразом R(S(a1, z1), S(𝑖21 , z1)).
Приклад 3. addition(x1,x2) = x1+x2
Функцію addition(x1,x2) можна обчислити за схемою:
 f(x1,0) = x1
 f(x1,y+1) = x1+y+1 = (x1+y)+1 = a1 (x1+y) = a1(f(x1,y))
Якщо покласти g(x1) = x1 і h(x1,x2,x3) = x3+1, то функцію
addition можна отримати в результаті операції примітивної
рекурсії R(g,h). Оскільки g(x1) – це функція-селектор, яка
позначається 𝑖11 , а функція h(x1,x2,x3) – результат суперпозиції
функції-селектора 𝑖33 у базову функцію a1(x1), яку можна
позначити виразом S(a1, 𝑖33 ), то функція addition позначається
виразом R(𝑖11 ,S(a1, 𝑖33 )).
Приклад 4. multiplication(x1,x2) = x1*x2
Функцію можна обчислити за схемою:
 f(x1,0) = 0
 f(x1,y+1) = x1*(y+1) = (x1*y)+x1 = f(x1,y)+x

56
Моделі обчислень у програмній інженерії

Функція multiplication(x1,x2) – результат операції примітивної


рекурсії до функцій g(x1) = z1(x1) і h(x1,x2,x3) = x3+ x1. Функцію
можна позначити виразом
multiplication = R(z1,S(addition, i33 , i13 )) =
= R(z1,S(R(i11 ,S(a1, i33 )),i33 , i13 )).

1.6.5. Тотальні функції віднімання


Приклад 5. subtract1(x1) = x1÷1
Всюди визначена функція x1÷1 = x1–1, якщо x1>0, і 0, якщо
x1=0. Її можна обчислити за схемою:
 f(0) = 0
 f(y+1) = (y+1)÷1 = y
Якщо покласти g(x1) = z1(x1) і h(x1,x2) = x1, то subtract1(x1) –
результат застосування операції примітивної рекурсії до
константи g(x1) і функції h(x1,x2). Функцію можна позначити
виразом subtract1 = R(z1,𝑖21 ).
Приклад 6. Використання попередньої функції дозволяє по-
будувати функцію subtraction(x1,x2) = x1÷x2, у якої x1÷x2 = x1-x2,
якщо x1≥x2, і 0, якщо x1<x2. Функцію можна обчислити за схе-
мою:
 f(x1,0) = x1÷0 = x1
 f(x1,y+1) = x1÷(y+1) = (x1÷y)÷1 = f(x1,y)÷1
Якщо покласти g(x1) = x1 і h(x1,x2,x3) = x3÷1, то subtrac-
tion(x1,x2) – результат застосування операції примітивної рекурсії
до константи g(x1) і функції h(x1,x2,x3), її можна позначити вира-
зом
subtraction = R(i11 ,S(subtract1, i33 )) = R(i11 ,S(R(z1,i12 ), i33 )).
Використовуючи просту підстановку, легко побудувати си-
метричну функцію subtractionRev(x1,x2) = x2÷x1. subtraction-
Rev(x1,x2) = S(subtraction,x2,x1), і позначення цієї функції subtrac-
tionRev = S(subtraction,𝑖22 , 𝑖21 ).
Функцію subtractionAbs(x1,x2) = |x1–x2| легко побудувати, ви-
користавши формулу subtractionAbs(x1,x2) = |x1–x2| = (x1÷x2) +
(x2÷x1).

57
Глибовець М. М., Кирієнко О. В., Проценко В. С.

subtractionAbs(x1,x2) = R(x1+x2, x1÷x2,x2÷x1)

1.6.6. Операція мінімізації


Означення 5. Операція мінімізації M за функцією g(x1, …, xn,
xn+1) із (n+1)-аргументами будує функцію f(x1, …, xn) із n-аргу-
ментами, яка задається співвідношенням f(x1, …, xn) = μy(g(x1, …,
xn, y) = 0), що використовує μ-оператор.
Обчислення μ-оператора – μy(g(x1, …, xn, y) = 0) у точці (x1,
…, xn):
 Знаходить найменше y≥0 таке, що g(x1, …, xn, z)
визначена для усіх z≤y і g(x1, …, xn, y) = 0. Значення μ-
оператора – y.
 Якщо таке y знайти неможливо, вважається, що значення
μ-оператора – невизначено.
Означення 6. Значення функції f = M(g) у точці (a1, …, an) об-
числюється так:
 Послідовно обчислюються значення g(a1, …, an,y) для
y=0, 1, 2, …
o Результат – перше y, коли g(a1, …, an,y)=0.
o Для всіх t ≤ y g(a1, …, an,t) – визначено і g(a1, …,
an,t)>0.
 f (a1, …, an) – невизначено, якщо виконується одна з умов:
o Для довільного y≥0 g(a1, …, an,y) – визначено й
g(a1, …, an,y)>0.
o Для довільного y<t g(a1, …, an,y) – визначено й
g(a1, …, an,y)>0, але g(a1, …, an,t) – невизначено.
o g(a1, …, an,0) – невизначено.
Якщо rg – вирази, що позначають функцію g, то М(rg) – ви-
раз, що позначає функцію f, яка отримується в результаті опера-
ції мінімізації.
Властивість 4. Функція g(x1, …, xn, xn+1) може бути обчис-
лювана й усюди визначена (тотальна), а результат операції
мінімізації – функція, що обчислювана, але не всюди визначена.

58
Моделі обчислень у програмній інженерії

Функція g(x1, x2) = x1 + x2 + 1 – обчислювана й тотальна.


Функція від одної змінної f = M(g) – всюди невизначена. Вона
задається співвідношенням
f(x1) = μy(x1 + y + 1 = 0)
Для довільного y≥0 значення g(a,y) = a+y+1>0, тому для
довільного a≥0 значення f(a) – невизначено.

1.6.7. Частково визначені функції. Означення


Приклад 7. Функція substractionAbs3(x1,x2,x3) = |x1 – (x2+x3)|
отримується в результаті суперпозиції функції-селектора
𝑖31 (x1,x2,x3) = x1 та функції g(x1,x2,x3) = addition(x2,x3) у функцію
subtractionAbs:
substractionAbs3 = S(substractionAbs,i13 ,g) =
= S(substractionAbs,i13 ,S(addition,i23 ,i33 )).
Частково визначену функцію віднімання subtractionPart(x1,
x2) = x1–x2 можна побудувати, застосувавши операцію мінімізації
до функції substractionAbs3.
 subtractionPart(x1,x2) = x1–x2, якщо x1≥x2 і невизначена
в інших випадках
 subtractionPart = M(substractionAbs3)
Функція задається співвідношенням subtractionPart(x1,
x2) = μy(|x1 – (x2+y)|= 0). Приклади її обчислення:
 Обчислимо subtractionPart(3,1)= 2
o substractionAbs3(3,1,0) = |3 – (1+0)|= 2
o substractionAbs3(3,1,1) = |3 – (1+1)|= 1
o substractionAbs3(3,1,2) = |3 – (1+2)|= 0
 Обчислимо subtractionPart(1,3)= невизначено
o substractionAbs3(1,3,0) = |1 – (3+0)|= 2
o substractionAbs3(1,3,1) = |1 – (3+1)|= 3
o substractionAbs3(1,3,2) = |1 – (3+2)|= 4
o і так далі.
Означення 7. Функція, що отримується із базових функцій
скінченним застосуванням операцій суперпозиції та примітивної
рекурсії, називається примітивно рекурсивною функцією.

59
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Функція, що отримується із базових функцій скінченним


застосуванням операцій суперпозиції, примітивної рекурсії та
мінімізації, називається частково-рекурсивною функцією.
Усюди визначена частково-рекурсивна функція називається
рекурсивною функцією.
Означення 8. Використовуючи позначення базових функцій
z1, a1, 𝑖11 , 𝑖𝑛1 , 𝑖𝑛2 , …, 𝑖𝑛𝑛 (n>1) та вирази, що задають (позначають)
операції суперпозиції S(rg, rg1, …, rgn), примітивної рекурсії R(rg,
rh) і мінімізації М(rg), кожну частково-рекурсивну функцію
можна представити (задати) виразом, який називають оператор-
ним термом.

1.6.8. Задачі
Довести, що наступні функції примітивно-рекурсивні.
1. signum(x) = sg(x)
 для натуральних чисел sg(x) = 0 при x=0 і sg(x) = 1
при x>0
2. expression1(x,y,z) = |x–(y+z)|
3. expresion2(x,y) = (x +1)* y
4. less(x,y)
 набуває значення 1 при x<y та 0 при x≥y
5. lessEqual(x,y)
 набуває значення 1 при x≤y та 0 при x>y
6. maximum(x,y) = max(x, y)
7. factorial(x) = x! = 1*2* …*(x–1)* x
8. expression3(x,y) = 3x + y
9. expression4(x) = (x+1)div3
 (x+1)div3 – натуральне число – частка від ділення
(x+1) на 3
10. dividedQuotient(x,y) = xdivy
 xdivy – натуральне число – частка від ділення x
на y
11. gcd(x,y) = НСД(x,y)

60
Моделі обчислень у програмній інженерії

 НСД(x,y) – найбільший спільний дільник нату-


ральних чисел x і y
 НСД(x,0) = 0
 НСД(0,y) = 0

1.7. Лямбда-числення
1.7.1. Лямбда-вирази
Лямбда-числення (-числення) можна назвати найменшою
універсальною мовою програмування у світі. Основне поняття в
лямбда-численні – вираз. Лямбда-числення містить лише одне
правило перетворень (підстановка змінної) й лише одну схему
визначення функції.
Поняття «лямбда-числення» було введене в науковий обіг у
1930-х роках Алонзо Черчем як один із способів формалізації
поняття ефективного обчислення. Лямда-числення універсальне
тому, що в його межах можна побудувати й обрахувати довільну
обчислювану функцію. Воно еквівалентне машинам Тюрінґа.
Але лямбда-числення використовує правила перетворення
(підстановки), не турбуючись про те, як їх реалізує комп’ютер у
реальності. Цей підхід більше пов’язаний із програмним забез-
печенням (software), ніж із організацією комп’ютера (hardware).
Означення 1. Вираз expr – це:
 Змінна – довільний ідентифікатор: id
 Функція – λ id . expr
 Застосування – expr1 expr2
Використовуються лише два ключові слова (символи): λ і.
Для наочності вираз можна взяти в круглі дужки. Тобто якщо
expr – вираз, то (expr) – той самий вираз.
Щоб не вживати багато дужок, приймається угода, що опе-
рація застосування – асоціативна ліворуч. Тобто вираз expr1 expr2
expr3 … exprN вираховується так само, як наступний вираз:
(…((expr1 expr2) expr3) … exprN).
Приклад 1. Згідно з означенням лямбда-виразу, окремий
ідентифікатор – лямбда-вираз.

61
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Найпростішою функцією є
λx.x
Цей вираз визначає тотожну функцію (identity function). Ім’я
(ідентифікатор) після λ – ідентифікатор аргументу цієї функції.
Вираз після крапки (в цьому разі лише x) називають «тілом»
визначення.
Означення 2. Функції можна застосовувати до виразів. На-
приклад:
(λ x . x) y
Тотожна функція застосовується до y. Дужки використову-
ють для наочності, щоб позбутися неоднозначності.
Застосування функції обчислюється підстановкою замість
аргументу x значення (у нашому прикладі – y) в тіло визначення
функції, тобто
(λ x . x) y = [y / x] x = y
У цьому перетворенні позначення [y / x] використовують,
щоб відзначити, що всі входження аргументу x замінюються на
значення y у виразі праворуч – x. Підстановку часто називають
редукцією.
Імена аргументів у визначеннях функцій не мають ніякого
змістовного навантаження. Це просто позначення розміщення,
тобто вони використовуються, щоб показати, де розташовані
аргументи функції, коли вона обчислюється. Тому
(λ z . z) ≡ (λ y .y) ≡ (λ t .t) ≡ (λ u . u)
тощо. Символ ≡ уживається, аби показати, що коли A ≡ B, то A –
просто синонім B.

1.7.2. Вільні та зв’язані змінні


У лямбда-численні всі імена аргументів локалізуються в
означеннях. Вважають, що x у функції λ x . x зв’язана тому, що
її входженню в тіло визначення передує λ x. Ім’я, перед яким
немає λ, називають вільною змінною. У виразі
(λ x . x y)
змінна x – зв’язана, а y – вільна. У виразі

62
Моделі обчислень у програмній інженерії

(λ x . x) (λ y . y x)
х у тілі першого виразу, що ліворуч, – зв’язана першим λ. У тілі
другого виразу y – зв’язана другим λ, а x – вільна. Зауважимо,
що x у другому виразі повністю незалежна від x у першому
виразі.
Означення 3. Змінна x вільна у виразі, якщо маємо один із
наступних трьох випадків:
 x – вільна у виразі x
 x – вільна в λ x1.expr, якщо ідентифікатор x1 ≠ x і x –
вільна в expr
 x – вільна в expr1 expr2, якщо x – вільна в expr1, або вона
вільна в expr2
Означення 4. Змінна x зв’язана, якщо маємо один із наступ-
них двох випадків:
 x – зв’язана в λ x1.expr, якщо ідентифікатор x1= x або x –
зв’язана в expr
 x – зв’язана в expr1 expr2, якщо x – зв’язана в expr1, або
вона зв’язана в expr2
Варто підкреслити, що один і той самий ідентифікатор може
бути водночас вільним і зв’язаним в одному й тому самому ви-
разі. У виразі
(λ x . x y) (λ y . y)
перша змінна y – вільна у лівому підвиразі в дужках. Але друга
змінна y – зв’язана в другому підвиразі праворуч. Отже, іденти-
фікатор y у цілому виразі водночас вільний і зв’язаний.

1.7.3. Застосування
Найбільше здивування під час знайомства з лямбда-числен-
ням викликає той факт, що немає потреби давати імена
функціям. Кожний раз, коли необхідно застосувати функцію,
записується все означення функції й лише потім виконується її
обчислення. Щоб спростити позначення, ми використовувати-
мемо ідентифікатори й числа як синоніми для деяких означень

63
Глибовець М. М., Кирієнко О. В., Проценко В. С.

функцій. Тотожну функцію, наприклад, можна позначити іден-


тифікатором id, який буде синонімом для виразу (λ x . x).
Тотожна функція, застосована сама до себе, – це застосу-
вання
id id ≡ (λ x . x) (λ x . x)
У цьому виразі перша x у тілі першого виразу в дужках – не-
залежна від x у тілі другого виразу. Цей вираз насправді можна
переписати так:
id id ≡ (λ x . x) (λ z . z)
Тотожна функція, застосована сама до себе,
id id ≡ (λ x . x) (λ z . z)
обчислить наступне:
[λ z . z / x]x = λ z . z = id
Тобто знову тотожна функція.
Виконуючи застосування, потрібно бути уважним, щоб уник-
нути змішання вільних уходжень змінної зі зв’язаними входжен-
нями. У виразі
(λ x . (λ y . x y)) y
функція ліворуч містить зв’язану y, в той самий час y право-
руч – вільна. Некоректна підстановка (застосування) змішає дві
змінні, що призведе до помилкового результату
(λ y . y y)
Просто замінивши ім’я зв’язаної змінної y на t, отримаємо
(λ x . (λ t . x t)) y = (λ t . y t) –
зовсім інший, але правильний результат.
Тобто, якщо функція λ x .expr застосовується до expr1, то за-
мінюються всі вільні входження x в expr на expr1 (підставляється
expr1 замість усіх вільних x). Якщо підстановка вносить вільну
змінну виразу expr1 у вираз, де ця змінна зв’язана, то до вико-
нання підстановки ім’я зв’язаної змінної замінюють на інше.
Наприклад, у виразі
(λ x . (λ y .( x (λ x . x y))) y
аргумент x асоціюється з y. У тілі
(λ y .( x (λ x . x y)))

64
Моделі обчислень у програмній інженерії

лише перша x – вільна, тож може бути заміненою. До виконання


підстановки також потрібно перейменувати змінну y, щоб уник-
нути змішання її зв’язаного входження з вільним:
[y / x] (λ t .( x (λ x . x t))) =(λ t .( y (λ x . x t)))
Означення 5. Під час виконання обчислення часто виникає
ситуація, коли вираз має декілька редексів, тобто можна вико-
нати застосування (підстановку) в різних частинах виразу. У разі
нормального порядку обчислення (normal order) із усіх мож-
ливих операцій застосування завжди застосовується (скоро-
чується) найбільш лівий, найбільш зовнішній вираз (редекс).
Обчислення продовжується, допоки це можливо.

1.7.4. Цілі числа


Мова програмування повинна вміти виконувати арифметичні
обчислення над натуральними числами. Натуральні числа можна
задавати так: починаючи з нуля (zero), записати suc(zero), щоб
представити 1, suc(suc(zero)), щоб представити 2 й так далі.
У лямбда-численні можна лише визначати нові функції, тому
числа будуть визначені як функції із використанням наступного
підходу: нуль можна визначити як
λ s . (λ z . z).
Це функція від двох аргументів s і z. Такі вирази, що містять
більше одного аргументу, скорочено записують як λ s z . z. У
цьому записі зрозуміло, що s – перший аргумент, який підстав-
лятиметься в процесі обчислення, і z – другий.
Означення 6. Використовуючи таку нотацію, перші нату-
ральні числа можна визначити так:
0≡λsz.z
1 ≡ λ s z .s (z)
2 ≡ λ s z .s (s (z))
3 ≡ λ s z . s ( s ( s (z)))
Приклад 2. Перш за все побудуємо функцію наступне число
(successor function). Її можна визначити так:
succ ≡ λ w y x . y (w y x)

65
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Застосувавши цю функцію до представлення числа нуль,


отримаємо:
succ 0 ≡ (λ w y x . y (w y x)) (λ s z . z)
У тілі першого виразу, замінюючи всі входження w на
(λ s z . z), отримаємо:
λ y x . y ((λ s z . z) y x) = λ y x . y ((λ z . z) x) =
λ y x . y (x) ≡ λ s z .s (z) ≡ 1
Тобто отримуємо представлення числа 1 (згадаємо, що імена
зв’язаних змінних – порожні).
Функція succ, застосована до 1, отримає:
succ 1≡ (λ w y x . y (w y x)) (λ s z . s (z)) =
(λ y x . y ((λ s z . s (z)) y x)) = (λ y x . y (y (x))) ≡ 2
Зауважимо, що єдина мета застосування (λ s z . s (z)) до аргу-
ментів y і x – це перейменування змінних, що використовуються
в означенні нашого числа.

1.7.5. Додавання і множення


Приклад 3. Функцію додавання можна побудувати швидко,
якщо врахувати, що тіло s z у визначенні числа 1 можна інтер-
претувати як застосування функції s (наступне число) до z (нуль).
Для того щоб додати 2 і 3, досить застосувати функцію succ
(наступне число) два рази до 3:
addition ≡ λ x y . x succ y
Спробуємо виконати наступне, щоб вирахувати 2+3:
2 succ 3 ≡ (λ s z .s (s z)) (λ w y x . y (w y x)) (λ u v . u ( u (u v)))
Перший вираз праворуч – це 2, другий – функція succ і
третій – 3 (імена перейменовані для прозорості). Цей вираз
зводиться (після двох підстановок) до
(λ w y x . y (w y x)) (λ w y x . y (w y x))
(λ u v . u ( u (u v))) ≡ succ succ 3
Можна перевірити , що succ succ 3 зводиться до succ 4 = 5.
Приклад 4. Множення двох чисел x і y можна обчислити, ви-
користовуючи наступну функцію:
multiplication ≡ λ x y z . x (y z)

66
Моделі обчислень у програмній інженерії

Добуток 2 на 2 – це
(λ x y z . x (y z)) 2 2
що зводиться до
(λ z . 2 (2 z))
Можна перевірити, що в разі подальших застосувань (ре-
дукцій) отримаємо очікуваний результат 4.

1.7.6. Логічні значення і операції


Означення 7. Вводяться наступні дві функції, які позначають
значення true і false:
true ≡ λ x y . x
false ≡ λ x y . y
Обидві функції мають два аргументи. true повертає перший,
а false повертає другий із двох аргументів.
Використовуючи ці представлення логічних значень, можна
дати визначення логічних операцій.
Приклад 5. Функцію and від двох аргументів можна визна-
чити так:
and ≡ λ x y . x y (λ u v .v) ≡ λ x y . x y false
Функцію or від двох аргументів можна визначити так:
or ≡ λ x y . x (λ u v .u) y ≡ λ x y . x true y
Функцію заперечення not від одного аргументу можна визна-
чити так:
not ≡ λ x . x (λ u v .v) (λ a b .a) ≡ λ x y . x false true
Якщо функцію заперечення not застосувати до значення
true, отримаємо:
not true ≡ (λ x . x (λ u v .v) (λ a b .a)) (λ c d . c)
що зводиться до
true false true ≡ (λ c d . c) (λ u v .v) (λ a b .a) ≡ (λ u v . u) ≡ false
тобто до логічного значення false.

1.7.7. Перевірка умови


Приклад 6. Дуже зручно мати у мові програмування
функцію, яка повертає значення true, коли її аргументом є число

67
Глибовець М. М., Кирієнко О. В., Проценко В. С.

0, і false – в іншому разі. Наступна функція відповідає цим вимо-


гам:
isZero ≡ λ x . x false not false
Щоб зрозуміти, як ця функція працює, відзначимо дві вла-
стивості:
 Довільна функція f, застосована 0 разів до аргументу a,
вирахує a.Тобто
0 f a ≡ (λ s z . z) f a = a
 Функція false, застосована до довільного аргументу,
отримає в результаті тотожну функцію
false a ≡ (λ x y . y) a = λ y . y = id
Зараз можна перевірити, що функція isZero працює коректно.
Якщо функцію застосувати до 0, то отримаємо:
isZero 0 ≡ (λ x . x false not false) 0 = 0 false not false =
= not false = true
Тому що false, застосоване 0 разів до not, обчислить not.
Функція isZero, застосована до іншого числа N, вирахує:
isZero N ≡ (λ x . x false not false) N = N false not false
Функція false застосовується N разів до not. Але false, засто-
соване до будь-чого, поверне тотожну функцію, тому якщо у
виразі, що вище, виконати застосування для будь-якого числа N,
що більше нуля, то буде отримано:
id false = false.

1.7.8. Пари і попереднє число


Означення 8. Для представлення пари (a,b) в лямбда-численні
можна скористатися функцією
(λ z . z a b)
Можна добути з цього виразу перший елемент пари, застосу-
вавши цю функцію до true:
(λ z . z a b) true = true a b = a
і другий, застосувавши функцію до false:
(λ z . z a b) false = false a b = b

68
Моделі обчислень у програмній інженерії

Приклад 6. Використовуючи пари чисел, визначимо функцію


попереднє число (predecessor function). Для знаходження попе-
редника числа n можна скористатися наступною стратегією:
створити пару чисел (n, n–1) і потім повернути другий елемент
пари як результат.
Наступна функція генерує з пари (n, n–1) (яка є аргументом p
у функції) нову пару (n+1, n):
predSS ≡ (λp z . z (suc (p true)) (p true))
Попередник (попереднє число) числа n отримується із засто-
суванням n разів функції predSS до пари (λ z. z 0 0) і потім із ви-
тяганням другого елемента з підсумкової пари:
pred ≡ (λ n .n predSS (λ z. z 0 0)) false
Зауважимо, що, застосувавши функцію pred до нуля, отри-
маємо нуль. Ця властивість корисна, тож вона використовується
під час означення інших функцій.

1.7.9. Рівність і нерівність


Приклад 7. Використовуючи функцію pred як базовий блок,
можна визначити функцію, що перевіряє, чи число x більше або
рівне числу y:
greatEqual ≡ (λ x y .isZero (x pred y))
Якщо в разі застосування x разів функції pred до числа y от-
римано нуль, то це можливо лише при x ≥ y.
Якщо x ≥ y і y ≥ x, то x = y. Це зумовлює наступне визначення
функції equal, що перевіряє на рівність два числа:
equal ≡ (λ x y. and (isZero (x pred y))) (isZero (y pred x))))
Подібно можна визначити функції, що перевіряють x > y,
x < y або x ≠ y.

1.7.10. Рекурсія
Рекурсивні функції можна визначити в лямбда-численні, ви-
користовуючи функції, які, викликавши іншу функцію f, потім
регенерують самі себе. Це краще зрозуміти, якщо розглянути
одну з таких функцій fix:

69
Глибовець М. М., Кирієнко О. В., Проценко В. С.

fix ≡ (λ y.(λ x. y (x x)) (λ x. y (x x)))


Ця функція, застосована до функціі rec, вирахує:
fix rec = (λ x. rec (x x)) (λ x. rec (x x)))
яка після подальшого застосування (підстановки) вираховує:
rec ((λ x. rec (x x)) (λ x. rec (x x))))
але це означає, що fix rec = rec (fix rec). Тобто функція rec об-
числюється, якщо використати рекурсивний виклик fix rec як
перший аргумент.
Приклад 8. Визначимо функцію recSum ≡ fix rec, яка додає
перші натуральні числа, використовуючи наступне рекурсивне
визначення:
[n + (n-1) + ... + 1 + 0] = n + [(n-1) + ... + 1 + 0]
Для отримання підсумкової функції використаємо наступне
визначення для rec:
rec ≡ (λ r n .isZero n 0 (n succ (r (pred n))))
У визначенні перевіряється число n: якщо воно нуль, то під-
сумкова сума – нуль. Якщо n не нуль, то функція succ (наступ-
ний) застосовується n разів до рекурсивного виклику (аргу-
мент r) функції, яка застосовується до попередника n (pred n).
Як дізнатися, що r у виразі вище – рекурсивний виклик rec,
адже функції в лямбда-численні не мають імен? Це невідомо, і це
одна з причин використання рекурсивного оператора fix.
Припустимо, необхідно додати числа від 0 до 3. Необхідні
операції будуть виконані під час виклику
fix rec 3 = rec (fix rec) 3 = isZero 3 0 (3 succ (fix rec (pred 3))))
Оскільки 3 не дорівнює 0, обчислення зведеться до
3 succ (fix rec 2)
Тобто сума чисел від 0 до 3 дорівнює:
3 додати суму чисел від 0 до 2.
Наступні рекурсивні обчислення fix rec зумовлять правиль-
ний кінцевий результат.
Зауважимо, що у функції, визначеній вище, рекурсія обір-
веться, коли аргумент стане рівним 0. Заключний результат буде
таким:

70
Моделі обчислень у програмній інженерії

3 succ 2 succ 1 succ 0


що є числом 6.

1.7.11. Задачі
Побудувати лямбда-вирази, що обчислюють наступні
функції:
1. expression1(x,y,z) = x+y+z
2. signum (x) = sg(x)
 для натуральних чисел sg(x) = 0 при x=0 і sg(x) =
= 1 при x>0
3. expresion2(x,y) = (x +1)* y
4. less(x,y)
 приймає значення True при x<y та False при x≥y
 функція повертає значення 0 (False) або True
5. lessEqual(x,y)
 приймає значення True при x≤y та False при x>y
 функція повертає значення 0 (False) або True
6. maximum(x,y) = max(x, y)
7. factorial(x) = x! = 1*2* …*(x–1)* x
8. expression3(x,y) = 3x + y
9. divide3Quatient(x) = xdiv3
 xdiv3 – натуральне число – частка від ділення
x на 3
10. dividedQuotient(x,y) = xdivy
 xdivy – натуральне число – частка від ділення
x на y
11. gcd(x,y) = НСД(x,y)
 НСД(x,y) – найбільший спільний дільник нату-
ральних чисел x і y
 НСД(x,0) = 0
 НСД(0,y) = 0

71
Глибовець М. М., Кирієнко О. В., Проценко В. С.

1.8. Програма ModelComp


1.8.1. Встановлення програми
Усі файли, необхідні для роботи з програмою, можна заван-
тажити з репозиторію https://github.com/ProtsenkoVS/models_-
interpreter.git.
Для роботи з програмою ModelComp необхідно, щоб на ма-
шині була встановлена версія Java не нижче 8.
Потрібно в одному каталозі розмістити наступні файли:
 ModelComp.jar – містить необхідну Java-програму.
 Model.db – база даних, у якій зберігаються створені мо-
делі обчислень.
Запуск програми:
 Із командного рядка – ввести в каталозі, де розміщена
програма ModelComp.jar, наступну команду:
o java -jar ModelComp.jar
 Із провідника (файлового менеджера) – подвійна фікса-
ція (дворазове натискання кнопки мишки) на файлі
ModelComp.jar.

1.8.2. Призначення програми


Головне призначення Java-програми ModelComp – підтри-
мувати роботу з наступними моделями обчислення (пункт меню
Модель обчислень): машини з необмеженими регістрами (Модель
обчислень / Машини з необмеженими регістрами);
 Машини Тюрінґа (Модель обчислень / Машини Тюрінґа);
 Нормальні алгоритми Маркова (Модель обчислень / Нор-
мальні алгоритми Маркова);
 Системи Поста (Модель обчислень / Системи Поста);
 Рекурсивні функції (Модель обчислень / Частково-рекур-
сивні функції);
 Лямбда-вирази (Модель обчислень / Лямбда-вирази).
Роботу з кожним типом моделі ведуть через графічний
інтерфейс, який включає:
 Опис моделі.

72
Моделі обчислень у програмній інженерії

 Програму (елементи) моделі.


 Кнопки, що дозволяють обрати конкретну команду (еле-
мент) програми:
o Напис k:n указує номер обраної команди (еле-
мента) моделі k і загальну кількість команд (еле-
ментів) n у моделі.
o Якщо команди (елемента) ще НЕ вибрано, то
напис буде 0:n.
 Кнопки для роботи з окремою командою (елементом)
моделі.
 Кнопки, що дозволяють обрати модель із бази даних. Усі
моделі одного виду впорядковані за своїми іменами:
o Напис l:m указує номер вибраної l і загальну
кількість моделей m цього виду в базі даних.
o Якщо моделі ще НЕ вибрано, то напис буде 0:m, а
області, що містять інформацію про модель та її
програму, – порожні.
 Кнопки для роботи з моделлю загалом.
Програма дозволяє:
 Створювати, редагувати й зберігати в базі даних моделі.
 Тестувати вибрану модель у режимі інтерпретації.
 Працювати з текстовим форматом моделі – введення з
текстового файлу / виведення в текстовий файл.
Для кожної моделі обчислень програма дозволяє:
 Переглянути всі моделі, що містяться в базі даних (Вибір
моделі).
 Створити нову модель (Нова).
o Обрати конкретну модель із бази даних і на її
основі створити нову модель, ідентичну обраній,
за винятком імені, яке встановлюється за замов-
чуванням і може бути змінене (Нова на основі).
 Ввести модель із текстового файлу (Ввести модель із
файлу).

73
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Обрати конкретну модель із бази даних і виконати з нею


наступні дії:
o Вилучити модель (Вилучити).
o Вивести модель в окремий текстовий файл (Виве-
сти модель у файл).
o Відредагувати модель (Опис моделі, Програма
(елементи) моделі).
o Виконати модель у режимі інтерпретації (Робота
з моделлю).
Програма дозволяє виведення/введення декількох моделей
(можливо, різного виду) в один текстовий файл / з одного тек-
стового файлу (Пункти меню Системні дії / Введення моделей із
файлу та Системні дії / Виведення моделей у файл).

74
Моделі обчислень у програмній інженерії

1.8.3. Робота з машиною з необмеженими


регістрами
Машина з необмеженими регістрами має програму, яка задає
числову функцію з декількома аргументами (арність функції).
У вибраній машині можна редагувати назву, кількість аргу-
ментів (арність), опис машини та працювати з її програмою.
Для модифікації програми доступні наступні дії:
 Нова – відкриває форму Command, що містить усі компо-
ненти нової команди, які можна редагувати.
o Усі компоненти нової команди збігаються з
компонентами виділеної, а номер є на оди-
ницю більше.
o Дозволяється редагувати номер команди.
 Редагувати – відкриває форму Command, що містить усі
компоненти виділеної команди, які можна редагувати,
окрім номера команди.
 Вилучити – вилучає виділену команду.
 Перемістити вверх / Перемістити вниз – міняє виділену
команду місцями із сусідньою згори (номер команди на 1
менше) / знизу (номер команди на 1 більше).
 Вставити програму – вставляє усі команди програми за
виділеною командою, коригуючи команди переходу обох
програм.
Команди переходу програми обраної машини можуть кори-
гуватися під час виконання наступних дій:
 Вилучити команду.
 Додати нову команду за командою, яка не є останньою.
 Вставити програму за командою, яка не є останньою.
Всі інші дії з програмою не змінюють команд переходу.
Для інтерпретації (виконання) обраної програми необхідно
перейти на форму Work (кнопка Робота з машиною).
 Для виконання необхідно ввести аргументи й натиснути
кнопку Виконати.
 Виконання програми зупиняється, коли:

75
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o Необхідно виконати команду, номер якої не вхо-


дить у програму; після завершення виводиться
результат – зміст регістра 1.
o Виконано команд (кроків) більше, ніж указано: у
цьому разі вважається, що результат не визначе-
ний, і виводиться Невизначено.

 Кнопка Переглянути виводить послідовність конфігу-


рацій машини під час виконання програми на введених
аргументах.

1.8.4. Робота з машиною Тюрінґа


У вибраній машині Тюрінґа можна редагувати назву, почат-
ковий і заключний стан, вказівку на те, що машина Тюрінґа об-
числює числову функцію, основний і додатковий алфавіти, опис
машини та програму машини (таблицю переходів), кожний еле-

76
Моделі обчислень у програмній інженерії

мент (рядок) якої описує поведінку машини Тюрінґа в одному зі


станів.
Зауваження. 1. Більшість елементів графічного інтерфейсу,
що описують модель, являють собою текстові поля. Напри-
клад, для машини Тюрінґа це назва, початковий і заключний
стан, основний і додатковий алфавіт, опис машини. Резуль-
тат редагування тестового поля обробляється програмою
(включно зі змінами в базі даних) лише ПІСЛЯ введення
символу Enter у текстовому полі. 2. У текстових полях опису
моделі й коментаря команди не можна використовувати
символ ’.
Для модифікації таблиці переходів доступні наступні дії:
 Перевірка – перевіряє коректність таблиці переходів
машин, у разі помилки видаючи відповідне повідом-
лення.
 Новий – дозволяє створити новий стан, якщо відкрити
форму Command, що містить усі компоненти нового
стану, які можна редагувати.
o Усі переходи нового стану збігаються з пере-
ходами виділеного.
o Ім’я нового стану, яке НЕ збігається із жодним
іменем стану таблиці переходів, модна реда-
гувати.
 Редагувати – дозволяє відредагувати переходи виді-
леного стану, якщо відкрити форму Command.
 Вилучити – вилучає виділений стан із таблиці переходів.
 Перейменувати – змінює назву визначеного стану, пере-
ходи якого виділені, на іншу в усіх елементах таблиці
переходів.
 Вставити програму – вставляє програму іншої машини
Тюрінґа, перейменовуючи всі її стани.
o Імена станів таблиці переходів, яка вставляється,
перейменовуються так, щоб вони були більшими
за імена всіх станів, переходи яких уже задано.

77
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o Отже, програма, що вставляється, розміщується в


кінці таблиці переходів.
Для інтерпретації (виконання) обраної машини Тюрінґа по-
трібно перейти на форму Work (кнопка Робота з машиною).
 Для виконання необхідно ввести початкове слово або
аргументи, якщо машина обчислює числову функцію, і
натиснути кнопку Виконати.
 Машина закінчить виконання програми й зупиниться,
якщо:
o Машина потрапить у заключний стан, і в цьому
разі виводить результат – заключне слово.
o Машина потрапить у стан @tt, якого немає в таб-
лиці переходів; результат вважається невизначе-
ним і виводиться повідомлення Невизначений
стан:@tt.
o Машина потрапить у конфігурацію (стан @tti
символ S), для якої не описано поведінки машини
(в таблиці переходів є стан @tt, але для символу S
не визначено дії). Результат вважається невизна-
ченим і виводиться повідомлення Невизначений
перехід: @tt+’S’.
o Машина виконає більше, ніж указано, кроків і не
потрапить у заключний стан. Вважається, що ре-
зультат невизначений і виводиться Невизначено
(вичерпано кількість переходів).
 Послідовність конфігурацій машини, що виникла під час
виконання програми, можна переглянути, натиснувши
кнопку Переглянути.

1.8.5. Робота з нормальним алгоритмом Маркова


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

78
Моделі обчислень у програмній інженерії

Для роботи зі списком підстановок доступні наступні дії:


 Перевірка – перевіряє коректність списку підстановок, у
разі помилки видаючи відповідне повідомлення.
 Нова – дозволяє створити нову підстановку, якщо
відкрити форму Command, що містить усі компоненти
нової підстановки, які можна редагувати.
o Компоненти нової підстановки збігаються з
компонентами виділеної.
o Дозволяється редагувати номер підстановки.
 Редагувати – дозволяє відредагувати компоненти виді-
леної підстановки, якщо відкрити форму Command.
 Вилучити – вилучає виділену підстановку.
 Перемістити вгору / Перемістити вниз – міняє виділену
підстановку місцями з сусідньою згори/знизу.
Для інтерпретації (виконання) вибраного алгоритму необ-
хідно перейти на форму Work (кнопка Робота з алгоритмом).
 Для виконання необхідно ввести початкове слово або
аргументи, якщо алгоритм обчислює числову функцію, і
натиснути кнопку Виконати.

79
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Після закінчення виконання виводиться результат.


o Якщо алгоритм виконав більше підстановок
(кроків), ніж указано, й не зупинився, то вва-
жається, що результат невизначений і виводиться
Невизначено.
 Послідовність підстановок, які виконав алгоритм, можна
переглянути, натиснувши кнопку Переглянути.

1.8.6. Робота з системою Поста


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

80
Моделі обчислень у програмній інженерії

Для модифікації списку аксіом і правил виведення системи


доступні наступні дії:
 Перевірка – перевіряє коректність списку аксіом і правил
виведення, у разі помилки видаючи відповідне пові-
домлення.
 Нове – відкриває форму Command, що містить усі
компоненти нового правила виведення чи аксіоми, які
можна редагувати.
o Усі компоненти нового правила виведення
збігаються з компонентами виділеного, а номер є
на одиницю більше.
o Дозволяється редагувати номер аксіоми / правила
виведення.
 Редагувати – дозволяє відредагувати компоненти виді-
леної аксіоми / правила виведення, якщо відкрити форму
Command.
 Вилучити – вилучає виділену аксіому / правило виве-
дення.
 Перемістити вверх / Перемістити вниз – міняє виділену
аксіому / правило виведення місцями із сусіднім згори/
знизу.
Для тестування (виконання) обраної системи необхідно пе-
рейти на форму Work (кнопка Робота з системою).
 Програма дозволяє побудувати (сформувати) частину
слів (теорем), які можна вивести за певну кількість
кроків, згідно з аксіомами й правилами виведення си-
стеми. Для цього необхідно вказати кількість кроків k і
натиснути кнопку Формувати дані.
o Програма формує усі слова, які можна вивести з
аксіом системи, застосувавши не більше, ніж k
разів, правила виведення системи.
 Кнопка Перегляти дані надає можливість переглянути
усі сформовані дані певної категорії:

81
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o Теореми – показує УСІ виведені з аксіом теореми.


o Усі вивідні – показує УСІ виведені з аксіом слова.
o Покроково вивідні – показує виведення з аксіом
УСІХ слів покроково.
o Якщо деяке слово (теорема) має декілька виве-
день, то в останньому перегляді будуть показані
УСІ виведення, на відміну від перших двох, де це
слово (теорема) трапиться лише один раз.
 Якщо система задає числову функцію, то можна отри-
мати значення функції для конкретних аргументів
(кнопка Показати результат).
o Якщо за вказану кількість кроків НЕ сформовано
слова, яке описує значення функції на вказаних
аргументах, то результат – Невизначено.

1.8.7. Робота з частково-рекурсивними


функціями
Кожна модель частково-рекурсивних функцій – це набір
(множина) примітивно-рекурсивних та частково-рекурсивних

82
Моделі обчислень у програмній інженерії

функцій, визначених користувачем. Якщо в означенні функції f2


використовується функція f1, яка не є базовою, то ці функції
повинні входити в один набір.
В обраному наборі можна редагувати назву, опис набору й
працювати з усіма функціями набору, виконуючи наступні дії:
 Перевірка – перевіряє набір функцій на коректність і
в разі наявності помилок видає про них повідомлення.
 Нова – відкриває форму Command, що містить назву, тіло
й коментар нової функції, які можна редагувати.
o Початкові значення тіла й коментаря нової
функції збігаються з тілом і коментарем виді-
леної, а початкове значення імені – name01, якщо
ім’я виділеної функції – name.
o Кнопка Тестувати – перевіряє коректність
функції, введеної в текстовому полі Тіло функції.
o Кнопка Структура – показує структуру коректно
введеної функції у вигляді дерева.

83
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Редагувати – відкриває форму Command, яка дозволяє


відредагувати тіло й коментар виділеної функції.
 Вилучити функцію – вилучає виділену функцію.
 Виконати – відкриває форму Work, яка дозволяє об-
числити значення обраної функції для конкретних
аргументів.
o Необхідно ввести аргументи й натиснути кнопку
Обчислити. Виводиться результат обчислення
функції або невизначене значення – Невизначено
(вичерпано загальну кількість кроків).
o Кнопка Переглянути дозволяє переглянути по-
слідовність обчислення функції у вигляді дерева
обчислення.

1.8.8. Робота з лямбда-виразами


Кожна модель лямбда-виразів – це набір (множина) лямбда-
виразів, визначених користувачем. Якщо в означенні лямбда-
виразу e2 використовується лямбда-вираз e1, то ці вирази по-
винні входити в один набір. Зауважимо, що означення виразу e1
повинне розміщуватися раніше від означення лямбда-виразу e2,
який його використовує. Кожний вираз набору можна поміняти
місцями з виразом, розташованим над ним / під ним, використо-
вуючи кнопки Перемістити вгору або Перемістити вниз.
В обраному наборі можна редагувати назву, опис набору й
працювати з усіма лямбда-виразами набору, виконуючи наступні
дії:
 Перевірка – перевіряє набір виразів на коректність і в разі
наявності помилок видає про них повідомлення.
 Новий – відкриває форму Command, що містить назву,
тіло й коментар нового лямбда-виразу, які можна ре-
дагувати.
o Початкові значення тіла й коментаря нового ви-
разу збігаються з тілом і коментарем виділеного, а

84
Моделі обчислень у програмній інженерії

початкове значення імені – name01, якщо ім’я


виділеного виразу – name.
o Кнопка Тестувати – перевіряє коректність
лямбда-виразу, введеного в текстовому полі Тіло
виразу.

 Редагувати – відкриває форму Command, яка дозволяє


відредагувати тіло й коментар виділеного лямбда-виразу.
 Вилучити – вилучає виділений лямбда-вираз.
 Вставити вираз – дозволяє обрати довільний вираз із
довільного набору виразів.
o Якщо в наборі вже є вираз із таким самим іменем,
то ім’я виразу, що вставляється, замінюється на
name01, name02…
o Вставний вираз додається нижче за виділений
вираз.
 Робота з набором – відкриває форму Work, яка дозволяє
працювати з довільними лямбда-виразами, що викори-
стовують вирази набору.

85
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o Необхідно ввести вираз у полі Вираз і натиснути


кнопку Виконати. Виводиться результат об-
числення виразу або невизначене значення –
Невизначено (вичерпано загальну кількість
кроків).
o Кнопка Переглянути дозволяє переглянути по-
слідовність обчислення виразу – послідовність
виконаних редукцій.
o Послідовність виконаних редукцій формується,
якщо встановлено Тестовий режим, і стає до-
ступною кнопка Переглянути.

1.8.9. Представлення моделей у текстовому файлі


Кожну модель можна описати в текстовому файлі, формат
якого залежить від виду моделі. У наступному описі текстових
форматів моделей вживатимуться терміни:
 Ім’я (Ідентифікатор) моделі або функції – послідовність
цифр і букв латинського алфавіту, що починається з
букви. Позначається Id.
 Коментар – послідовність символів, що починається
символом ‘ і закінчується кінцем рядка (символ \n).
Послідовність не містить у собі символів ‘ та “, позна-
чається Comm.
 Рядок – послідовність символів, яка з обох кінців обме-
жена символом “ і не містить його в середині, позна-
чається St, St1, Al, … Наприклад: “” – рядок без жодного
символу (порожній рядок), “a” – рядок, що складається з
одного символу a.
 Число – послідовність цифр, позначається n, r, r1, …
 Виділені слова (наприклад: Machine, end, …) –
обов’язкові (службові) слова.
 Під час опису формату інколи вживаються символи [, ] –
це метасимволи. Конструкція, яка перебуває між ними,
може бути відсутня.

86
Моделі обчислень у програмній інженерії

Нормальний алгоритм Маркова


[Comm]
Algorithm Id
Alphabet Al1, Al2; [Numerical n;]
RuleAlg1
……
RuleAlgk
end Id
 Comm – загальний коментар моделі.
 Id – ім’я моделі.
 Al1, Al2 – рядки, які задають, відповідно, основний і
додатковий алфавіти.
 RuleAlg1…RuleAlgk – список правил алгоритму,
кожний із яких розташований в окремому рядку й має
вигляд:
o St1 –> St2; [Comm]
o St1 –>. St2; [Comm]
 Якщо присутня конструкція Numerical n;, то алго-
ритм обчислює часткову функцію з n-аргументами й
основний алфавіт має вигляд Al1 = “|#”.

Машина з необмеженими регістрами


[Comm]
Computer Id : t;
Command1
……
Commandk
end Id
 Comm – загальний коментар програми.
 Id – ім’я програми.
 t – число, що задає кількість вхідних даних.
 Command1…Commandk – список команд, які утворю-
ють програму. Кожна команда розташована в окремо-
му рядку й має вигляд:
o i: Z(n) [Comm]

87
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o i: S(n) [Comm]
o i: T(n,m) [Comm]
o i: J(n,m,q) [Comm]
o i, n, m, q – числа, що задають номер команди
(i) і її аргументи – (n,m,q).

Машина Тюрінґа
[Comm]
Machine Id
Alphabet Al1, Al2; [Numerical n;]
Initial ini; Final fin;
MoveState1
……
MoveStatek
end Id
 Comm – загальний коментар моделі.
 Id – ім’я моделі.
 Al1, Al2 – рядки, які задають, відповідно, основний і
додатковий алфавіти.
 ini, fin – рядки, які задають, відповідно, початковий і
заключний стан машини.
o Стан машини Тюрінґа – це рядок “@XY”, X,
Y – довільні символи.
 MoveState1, …, MoveStatek – список правил машини,
які задають таблицю переходів. Кожне з правил опи-
сує поведінку машини в одному зі станів, розташо-
вується в окремому рядку й має вигляд:
o State: St1 –>Move1: : Stn –> Moven; [Comm]
o State – рядок, що задає стан машини.
o St1, …, Stn – рядки, кожний із яких є симво-
лом алфавіту (основного або додаткового).
o Move1, …, Moven – рядки “@XYSM”, що
описують поведінку машини у стані State із
символом St1, …, Stn:
 @XY – наступний стан машини;

88
Моделі обчислень у програмній інженерії

 S – символ основного або додаткового


алфавіту;
 M – символ <,> або ., що описує рух
головки машини.
o Comm – можливий коментар поведінки ма-
шини у стані State.
 Якщо присутня конструкція Numerical n;, то машина
обчислює часткову функцію з n-аргументами й
основний алфавіт – Al1 = “|#”.

Система Поста
[Comm]
System Id
Alphabet Al1, Al2; [Numerical n;]
RuleSyst1
……
RuleSystk
end Id
 Comm – загальний коментар моделі.
 Id – ім’я моделі.
 Al1, Al2 – рядки, які задають, відповідно, основний і
додатковий алфавіти.
 RuleSyst1, …, RuleSystk – список аксіом і правил виве-
дення системи, кожний із яких розташований в окре-
мому рядку й має вигляд:
o St1 ; [Comm]
 аксіома
o St1 –> St2; [Comm]
 правило виведення
o St1, St2 – рядки, у яких можуть вживатися
символи основного й додаткового алфавіту та
змінні. Кожна змінна – це пара символів @R,
@S, @T, @U, @V, @W, @X, @Y або @Z.

89
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Якщо присутня конструкція Numerical n;, то система


породжує часткову функцію з n-аргументами й
основним алфавітом є Al1 = “|#”.

Набір частково-рекурсивних функцій


[Comm]
Recursive Is;
Function 1
……
Function k
end Is
 Модель з іменем Is і можливим коментарем Comm
складається із множини функцій Function1, …,
Functionk.
 Кожна функція описується в одному рядку й має
вигляд:
o Id : n = Expr; [Comm]
o Id – імя функції.
o n – число, кількість аргументів функції.
o Expr – вираз, який задає тіло функції (визна-
чає, як вирахувати функцію):
 a1 – базова функція s(x).
 z1 – базова функція o(x).
 iNM (NM – десяткові цифри) – базова
функція-селектор M-аргументу з N-
аргументів.
 @S(e0,[e1,…,en]) – операція супер-
позиції в n-арну функцію, яка за-
дається виразом e0, n-функцій, що за-
даються виразами e1, …, en.
 @R(eg,eh) – операція примітивної ре-
курсії за функціями, що задаються ви-
разами eg і eh.
 @M(eg) – операція мінімізації за
функцією, що задається виразом eg.

90
Моделі обчислень у програмній інженерії

 Id1 – ідентифікатор раніше визначеної


функції.
o Comm – коментар функції.

Набір лямбда-виразів
[Comm]
CalculusLid;
Lambda1
……
Lambdak
end Lid
 Модель з іменем Lid і можливим коментарем Comm
складається з послідовності лямбда-виразів з іменами
Lambda1, …, Lambdak.
 Кожний вираз описується або двома рядками
o ‘ Comment lambda
o Idl = \ x1 x2 … xk . Expr
 або одним рядком (вираз без коментаря)
o Idl = \ x1 x2 … xk . Expr
 Кожний вираз описується в одному рядку й має
вигляд:
o Idl = \ x1 x2 … xk . Expr
o Idl – ім’я лямбда-виразу.
o x1, x2, …, xk – ідентифікатори, що задають
змінні лямбда-виразу.
o Expr – вираз, який має вигляд:
 xi – змінна;
 idP – ідентифікатор раніше визначе-
ного лямбда-виразу;
 n – десятковий запис цілого числа без
знака;
 (e) – лямбда-вираз e в дужках;
 efea – операція застосування – два
лямбда-вирази ef та ea;

91
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 \y.eb – операція абстракції: y – змінна-


ідентифікатор, а – лямбда-вираз.

2. Аналіз
2.1. Граматики
2.1.1. Означення: граматика, мова
Алфавіт V – це довільна скінченна множина. Елементи цієї
множини – це символи алфавіту. Слово α – довільна скінченна
послідовність символів V. Через V* позначаються всі слова в
алфавіті V. Якщо α – слово, то |α| – довжина слова α. Порожнє
слово (порожня послідовність символів) позначається ε та |ε| = 0.
Означення 1. Мова L в алфавіті V – це довільна підмножина
множини V*. Тобто L  V*.
Означення 2. Граматика G = (VN, VT, P, S), де:
 VN і VT – алфавіти, відповідно, нетерміналів і терміналів,
VN  VT = {}.
 V = VN  VT.
 P – скінченна множина правил α–>β, де α = α1Nα2, α1  V*,
N  VN, α2  V* і β  V*.
 S – початковий нетермінал, S VN.
Означення 3. Якщо α–>β  Pіγ, δ  V*, то в граматиці G зі
слова γαδ безпосередньо виводиться слово γβδ, що позначається
γαδ=>γβδ.
 u=>v, якщо існують γ, δ  V* і α–>β P такі, що u=γαδ і
v=γβδ.
 u=>v – відношення безпосереднього виведення на мно-
жині V*.
Означення 4. Якщо α1, α2, …, αn  V*, α1=>α2, …, αn-1=> αn, то
в граматиці G зі слова α1 виводиться слово αn, що позначається
α1*=>αn.
 Послідовність α1=> α2=> … => αn називається виведен-
ням слова αn зі слова α1.
 u*=>v – відношення виведення на множині V*.

92
Моделі обчислень у програмній інженерії

Властивість 1. Відношення *=> є рефлексивним і транзи-


тивним замиканням відношення =>.
Означення 5. Множина L(G) = {w | w  VT*, S *=>w} – мова,
породжувана граматикою G.
Якщо α V* і S *=>α, то α – сентенційна форма граматики G.

2.1.2. Типи граматик


Мова, яку породжує граматика, повністю залежить від виду її
правил виведення. Є чотири типи граматик.
Означення 6. Нехай G = (VN, VT, P, S), якщо її правила виве-
дення:
 тільки виду A–>aB, A–>a або A–>ε, де A, B VN, a  VT,
то G – граматика типу 3, або регулярна граматика;
 тільки виду A–>β, де A VN, β  V*, то G – граматика
типу 2, або контекстно-вільна граматика (КВ-граматика);
 тільки виду γAδ–>γβδ, де γ, β, δ  V*, A  VN, β≠ε, то G –
граматика типу 1, або контекстно-залежна граматика
(КЗ-граматика);
 довільні, то G – граматика типу 0.
Властивість 2. Якщо в граматиці G = (VN, VT, P, S) усі пра-
вила виду α–>β, де |β| ≥ |α|, то граматика – контекстно-залежна.
Приклад 1. У граматиці G1 = (VN, VT, P, S) VN = {S,B,C}, VT =
= {a,b,c}, P = {S –>aBC, S –>aSBC, CB –>BC, aB –>ab, bB –>bb,
bC –>bc, cC –>cc}.
 G1 – граматика типу 1 (контекстно-залежна)
 L(G1) = {anbncn | n ≥ 1}
 Виведення слова aaabbbccc має вигляд:
o S => aSBC => aaSBCBC => aaaBCBCBC =>
aaaBBCCBC => aaaBBCBCC => aaaBBBCCC =>
aaabBBCCC => aaabbBCCC => aaabbbCCC =>
aaabbbcCC => aaabbbccC => aaabbbccc
Приклад 2. У граматиці G2 = (VN, VT, P, S) VN = {S}, VT =
= {a,b}, P = {S –>aSb, S –>ε}
 G2 – граматика типу 2 (контекстно-вільна)

93
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 L(G2) = {anbn | n ≥ 0}
 Виведення слова aabb має вигляд:
o S =>aSb =>aaSbb =>aabb
Приклад 3. У граматиці G3 = (VN, VT, P, S) VN = {S,A}, VT =
= {a,b}, P = {S –>aS, S –>aA, A –>bA, A –>ε}
 G3 – граматика типу 3 (регулярна)
 L(G3) = {anbm | n>0, m ≥ 0}
 Виведення слова aaab має вигляд:
o S =>aS =>aaS =>aaaA => aaabA => aaab
Властивість 3. Якщо AL0, AL1, AL2, AL3 – усі мови типу 0,
1, 2 і 3 відповідно, то маємо співвідношення
AL3  AL2  AL1  AL0
Властивість 4. Системи Поста породжують усі мови типу 0 і
тільки їх.

2.1.3. Означення дерева


Означення 6. Дерево T – скінченна
множина вузлів (мал. 1), у якої:
 Виокремлюється один вузол n, що
називається коренем дерева.
 Всі інші вузли розбиваються на
список підмножин (піддерев) T1,
…, Tk, які попарно не перети-
наються.
o Дерево позначається як T = (n,<T1, …, Tk>)
o У графічному зображенні дерева зв’язок між вуз-
лами відображають дуги.
Приклад 4. Непорожню множину
вузлів {n1, n2, n3, n4, n5}, що утворює
дерево T1 = (n1,<(n2, <>), (n3, <(n4,
<>), (n4, <>)>)>), зображено на мал. 2.
Означення 7. Якщо дерево T =
(n,<T1, …, Tk>) має корінь n і піддерева
T1, …, Tk, то:

94
Моделі обчислень у програмній інженерії

 Всі інші вузли дерева T – нащадки кореня.


 Прямі нащадки – корені піддерев T1, …, Tk – це сини
кореня n.
 Вузол, що не має синів, називається листком.
o Листок – це дерево, що має порожній список
піддерев.
o n2, n4, n5 – листки дерева T1.
 Крона дерева – це всі листки дерева в порядку зліва
направо.
o Крона дерева T1 – <n2, n5, n4>.
Часто кожному вузлу дерева ni додають
його мітку ni:m (деякий символ або слово).
Такі дерева називають поміченими дере-
вами. Мітки, на відміну від вузлів, можуть
повторюватися в одному дереві. На прак-
тиці в разі використання дерев важливішу
роль відіграють мітки, які помічають вузли.
Приклад 5. Помічене дерево T2 =
(n1:S,<(n2:a, <>), (n3:A, <(n4:a, <>), (n4:B, <>)>)>), графічне
зображення якого відображає не вузли дерева {n1, n2, n3, n4, n5},
а їх мітки, наведене на мал. 3.

2.1.4. Дерево виведення


Для графічного зображення виведення ліворуч у КВ-грама-
тиках використовується спеціальне дерево – дерево виведення
(дерево розбору, синтаксичне дерево).
Означення 8. Нехай G = (VN, VT, P, S) – КВ-граматика. Дерево
виведення для G відповідає умовам:
 Кожен вузол має мітку – символ
із V або ε.
 Мітка кореня S.
 Якщо вузол має хоч одного на-
щадка, то його мітка – нетер-
мінал.

95
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Якщо вузли n1, n2, …, nk – прямі нащадки вузла n,


перераховані зліва направо, із мітками A1, A2, …, Ak, а
мітка вузла n – A, то A–>A1A2…Ak  P.
 ε може бути міткою тільки листка.
Приклад 6. У граматиці G4 = (VN, VT, P, S) VN = {S,A}, VT =
= {a,b}, P = {S –>aAS, S –>a, A –>SbA, A –>ba, S –>ε}
 Виведення слова aabba має вигляд:
o S =>aAS =>aSbAS =>aabAS =>aabbaS => aabba
 На мал. 4 показано дерево виведення для граматики G4,
що відповідає наведеному виведенню слова aabba.
 Крона дерева – мітки листків зліва направо – утворює
результат виведення – слово aabba.
Властивість 5. Нехай G = (VN, VT, P, S) – КВ-граматика.
Вивід S *=>α, α V* існує тоді й тільки тоді, коли існує дерево
виведення в граматиці G із кроною (результатом) α.
 Кожному слову, що виводиться в граматиці, може від-
повідати декілька виведень.
 Кожному дереву виведення може відповідати декілька
виведень.

2.1.5. Приклади КВ-граматик


Приклад 7. Граматика G5 = (VN, VT, P, S), що має алфавіти
VN = {S}, VT = {a,b} і правила виведення P = {S –>aSa, S –>bSb,
S –>ε}, породжує всі паліндроми парної довжини в алфавіті
{a,b}.
 Наприклад, паліндром abbbba можна вивести так:
o S => aSa => abSba => abbSbba => abbbba
Приклад 8. Граматика G6 = (VN, VT, P, S) з алфавітами VN =
= {S,A,B}, VT = {a,b} та правилами виведення P = {S –>aB, S –>
bA, A –>a, A –>aS, A –>bAA, B –>b, B –>bS, B –>aBB} породжує
всі непорожні слова, у яких кількість символів a і b однакова.
Щоб упевнитися в цьому, досить довести наступні властивості:

96
Моделі обчислень у програмній інженерії

 S *=>α  VT* тоді й тільки тоді, коли в α однаково сим-


волів a і b.
 A *=>α  VT* тоді й тільки тоді, коли в α на один символ a
більше, ніж символів b.
 B *=>α  VT* тоді й тільки тоді, коли в α на один символ b
більше, ніж символів a.
o Кожна з цих властивостей доводиться індукцією
за довжиною слова α (фактично всі три власти-
вості доводяться водночас).

2.1.6. Задачі
1. Чи можна вивести слово cabbaac у граматиці: G = (VN,
VT, P, S), VN = {S, A, B}, VT = {a, b, c}, P = {S –>SS, S –>a,
S –>c, S –>A, B –>bB, B –>b, aAa –>aBa}? Побудувати
виведення цього слова.
2. Чи належить рядок (()())() мові, що породжується КВ-
граматикою G = (VN, VT, P, S), VN = {S, A}, VT= {(, )}, P =
= {S –>SA, S –> A, A –>(S), A –> ( ) }? Побудувати виве-
дення цього слова.
3. Чи належить слово 00011011 мові, що породжується КВ-
граматикою G = (VN, VT, P, S), VN = {S, A}, VT = {0, 1}, P =
= {S –>SS, S –> A, A –>0A1, A –> S, A –> 01}? Побудуйте
виведення цього слова.
4. До якого типу належить граматика G = (VN, VT, P, S), VN =
= {S, A, B}, VT = {a, b, c, s}, P = {S –>AcBs, A –>AcA, A –>
B, B –>a, B –>b}? Яку мову вона породжує? Чи можна
породити цю мову граматикою більшого типу?
5. До якого типу належить граматика G = (VN, VT, P, S), VN =
= {S, A, B}, VT = {a, b, c}? Яку мову вона породжує?
a. P = { S –>A, S –>B, A –>aAb, A –>a, B –>aBbb,
B –>b}
b. P = { S –>aA, S –>bS, A –>aA, A –>bB, B –>aB,
B –>bB, B –>a}

97
Глибовець М. М., Кирієнко О. В., Проценко В. С.

6. Побудувати виведення декількох слів мови, що задається


граматикою G = (VN, VT, P, S), VN = {S, R}, VT = {0, 1}, P =
= {S –>SS, S –>RS, R –>RR, R –>0, RS –>SR, 0S0 –>010}.
Яку мову породжує ця граматика?
7. Задано граматику G = (VN, VT, P, S), VN = {S, B, C}, VT = =
{a, b, c}, P = {S –>aSBC, S –>abC, CB –>BC, bB –>bb, bC
–>bc, cC –>cc}. Побудувати виведення слова aaabbbccc.
8. До якого типу належить граматика G = (VN, VT, P, S), VN =
= {S, A, B}, VT = {a, b, c}? Яку мову вона породжує?
a. P = {S –>aA, S –>aB, B –>bB, B –>b, A –>bA,
A –>a}
b. P = {S –>bA, A –>aA, A –>ba, A –>bB, B–>bB,
B –>b}
c. P = {S –>aAb, S –>ab, S –>bBa, B –>bBa, B –>ba,
aA –>aaAb, A –>ab}
d. P = {S –>AB, AB –>BA, A –>a, B –>b}

2.2. Скінченні автомати


2.2.1. Детермінований скінченний автомат
Означення 1. Скінченний детермінований автомат M = (Q, T,
σ, q0, F), де:
 Q – скінченна множина станів, q0∈Q, F⊆Q;
 T – скінченний вхідний алфавіт;
 σ – функція переходу, σ: Q×T−>Q;
 q0 – початковий
стан;
 F – множина за-
ключних станів.
Скінченний авто-
мат – це логічний при-
стрій, що має стрічку із
вхідним словом x і на
кожному кроці роботи
перебуває в одному зі станів із множини Q. У процесі роботи

98
Моделі обчислень у програмній інженерії

автомат послідовно читає вхідне слово, переходячи з одного


стану в інший згідно із функцією переходів σ.
 Словоx = a1a2…an містить лише символи вхідного алфа-
віту T.
 На кожному кроці автомат читає один символ.
 Автомат починає свою роботу в початковому стані q0.
 Автомат послідовно переходить зі стану в стан:
o q0 a1−> q1 –a1−>q2 – …−>qn-1 –an−> qn
o qi = σ(qi-1, ai) для 0≤i<n
 Після завершення автомат перевіряє, чи належить остан-
ній стан qn множині заключних станів F.
Означення 2. Конфігурація скінченного автомата M – це кор-
теж (q, x), де q∈Q і x∈T*. На кожному кроці автомат виконує
безпосередній перехід (q,ax)=>(p, x) із конфігурації (q,ax) у
конфігурацію (p, x), читаючи на вході символ a.
 (q,ax) => (p, x), якщо p = σ(q, a)
Означення 3. Автомат M виконує перехід (q,x) *=> (p,y) із
конфігурації (q, x) у конфігурацію (p, y), якщо:
 (q, x) = (p, y), або
 існують конфігурації (q1,x1), …, (qn,xn) такі, що:
o (q1, x1) = (q, x)
o (qn, xn) = (p, y)
o (qi, xi) => (qi+1, xi+1) для 1≤i<n
Означення 4. Автомат M розпізнає слово x ∈ T*, якщо (q0,x)
*=> (p,ε) для деякого p ∈ F.
Означення 5. Мова L(M), що
розпізнається автоматом M, – це
множина слів в алфавіті T, L(M) =
= {x ∈ T* | (q0,x) *=> (p, ε), p ∈ F}.
Приклад 1. Автомат M0 = (Q, T, σ,
q0, F) має Q = {q0, q1, q2, q3}, T =
= {a, b}, F = {q0, q1, q2} і функцію
переходів: σ(q0, a) = q1, σ(q0, b) =
= q3, σ(q1, a) = q1, σ(q1, b) = q3,

99
Глибовець М. М., Кирієнко О. В., Проценко В. С.

σ(q2, a) = q3, σ(q2, b) = q2, σ(q3, a) = q3, σ(q3, b) = q3.


 Автомат розпізнає мову L(M0) = {an | n ≥ 0}U{bm | m ≥ 0}.
Для роботи часто зручніше скористатися графічним зобра-
женням автомата, яке інколи називають діаграмою переходів. На
графічному зображенні початковий стан указується стрілочкою,
а заключні стани виділяють товщою лінією.

2.2.2. Недетермінований скінченний автомат


Означення 6. Скінченний недетермінований автомат M = (Q,
T, σ, q0, F), де:
 Q – скінченна множина станів, q0∈Q, F⊆Q;
 T – скінченний вхідний алфавіт;
 σ – функція переходу, σ: Q×(TU {ε})−>2Q;
 q0 – початковий стан;
 F – множина заключних станів.
Недетермінований автомат у стані q має можливість:
 Прочитати на вході символ a і перейти в один зі станів
q1, …, qi, якщо σ(q, a) = {q1, ..., qi}.
 Не читаючи на вході жодного символу, перейти в один зі
станів q1, …, qi, якщо σ(q, ε) = {q1, …, qi}.
 Не виконувати жодної дії, якщо на вході символ a і σ(q,
a) = {}.
Означення 7. Автомат виконує безпосередній перехід (q,x)
=> (p,y) із конфігурації (q,x) у конфігурацію (p, y), якщо:
 x=y і p ∈ σ(q, ε) – не читаючи на вході жодного символу,
 або існує a ∈ T такий, що x=ay і p∈σ(q, a) – читаючи на
вході символ a.
Означення 8. Автомат M розпізнає
слово x ∈ T*, якщо ІСНУЄ перехід
(q0,x) *=> (p,ε) для деякого p ∈ F.
Приклад 2. Наступний недетер-
мінований автомат M1 = (Q, T, σ, q0, F)
має Q = {q0, q1, q2, q3}, T = {a, b}, F =
= {q3} і функцію переходів: σ(q0, ε) =

100
Моделі обчислень у програмній інженерії

= {q1,q2}, σ(q0, a) = {}, σ(q0, b) = {}, σ(q1, ε) = {q3}, σ(q1, a) =


= {q1}, σ(q1, b) = {}, σ(q2, ε) = {q3}, σ(q2, a) = {}, σ(q2, b) = {q2},
σ(q0, ε) = {}, σ(q3, a) = {}, σ(q3, b) = {}.
Як і попередній, цей автомат розпізнає мову L(M0) = {an | n ≥
0 }U{bm | m ≥ 0}, але завдяки недетермінізму має простішу струк-
туру.
Графічне зображення цього недетермінованого скінченного
автомата наведено на мал. 3.
Теорема 1. Для довільного недетермінованого скінченного
автомата M існує еквівалентний скінченний детермінований
автомат Md такий, що
L(M) = L(Md).
Доведення. Нехай M = (Q, T, σ, q0, F) – скінченний не-
детермінований автомат. Побудову детермінованого автомата
Md, який розпізнає мову L(M), можна виконати у два етапи.
На першому етапі вилучаються всі порожні переходи. Бу-
дується недетермінований автомат Mn = (Qn, T, σn, q0, Fn) такий,
що L(M) = L(Mn) і для довільного q ∈ Qnσn(q, ε) = {}.
 Відберемо у множину станів Qn – усі досяжні стани із Q,
тобто ті стани, у які може перейти автомат M, прочи-
тавши якесь слово w ∈ Σ*.
Qn = {q0} U {p ∈ Q | w ∈ Σ*, (q0,w) *=> (p,)}
 Позначатимемо через Cl(q) = {p | (q,) *=> (p,)} – всі
стани, у які може перейти автомат M зі стану q порож-
німи переходами (не читаючи жодного символу на
вході).
 q ∈ Qn, a ∈ T, σn(q, a) = ⋃𝑝𝐶𝑙(𝑞) 𝜎(𝑝, 𝑎)
 Fn = {p | Cl(p) ∩ F ≠ {} }
Автомат Mn допускає ті самі слова, що й автомат M, але на
кожному кроці читає один символ вхідного слова.
На другому етапі за автоматом Mn будується підсумковий де-
термінований автомат Md = (Qd, T, σd, q0d, Fd). Стани автомата
Md – це всі можливі підмножини множини станів Qn, а його
робота моделює всі можливі варіанти роботи автомата Mn.

101
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Qd = 2Qn = {S | S⊆Qn} – УСІ підмножини множини Qn


 q0d = {q0} – множина з одного елемента q0
 S ⊆ Qn i a ∈ Σ, σd(S, a) = ⋃𝑝  𝑆 𝜎𝑛 (𝑝, 𝑎)
 Fd = {S | S ∩ Fn ≠ {} }
Очевидно, що L(M) = L(Mn) = L(Md).

2.2.3. Побудова еквівалентного детермінованого


автомата
Виконаємо побудову детермінованого скінченного автомата
для недетермінованого автомата M1 = (Q, Σ, σ, q0, F), описаного
вище.
На першому етапі будується еквівалентний недетермінова-
ний автомат без порожніх переходів M1n = (Qn, T, σn, q0, Fn).
 Qn = Q
 Cl(q0) = {q0, q1, q2, q3},
Cl(q1) = {q1, q3}, Cl(q2) = {q2,
q3}, Cl(q3) = {q3}
 σn(q0, a) = {q1}, σn(q0, b) =
= {q2},
σn(q1, a) = {q1}, σn(q1, b) = {},
σn(q2, a) = {}, σn(q2, b) = {q2},
σn(q3, a) = {}, σn(q3, b) = {}
 Fn = {q0, q1, q2, q3} = Q
На мал. 4 наведено графічне зображення недетермінованого
автомата M1n.
На другому етапі будується заключний еквівалентний де-
термінований автомат M1d = (Qd, T,
σd, q0d, Fd). Згідно з теоремою 1,
кількість станів цього автомата 24 =
= 16, Qd = {{}, {q0}, {q1}, {q2}, {q3},
{q0, q1}, …, {q0, q1, q2, q3}}. Але
більшість із них недосяжна з по-
чаткового стану {q0}.

102
Моделі обчислень у програмній інженерії

Щоб не використовувати непотрібних недосяжних станів,


функцію переходів σd і множину станів Qd будують послідовно,
починаючи з початкового стану {q0}.
Наступна таблиця містить побудовані функцію переходів і
множину станів:
a b
{q0} {q1} {q2}
{q1} {q1} {}
{q2} {} {q2}
{} {} {}
Усі стани, крім {}, заключні.
12 інших станів із Qd – недосяжні з початкового стану {q0}.
На мал. 5 наведено графічне зображення детермінованого авто-
мата M1d.

2.2.4. Регулярні граматики


Граматика G = (VN, VT, P, S), кожне правило виведення якої є
A–>aB, A–>a або A–>ε, де A, B  VN, a  VT, називається грамати-
кою типу 3, або регулярною граматикою.
Означення 6. Мова, що породжується регулярною грамати-
кою, називається регулярною мовою.
Теорема 2. Якщо G – граматика типу 3, то існує недетерміно-
ваний скінченний автомат M такий, що L(G) = L(M).
Доведення. Нехай G = (VN, VT, P, S) – регулярна граматика й
символ D  VN  V. Недетермінований автомат M = (VN  {D}, VT,
σ, S, {D}), у якого функція переходу δ визначається
 B ∈ VN, a ∈ VT: σ(B,a) = {C | B –> aC ∈ P}  {D | B –>
a ∈ P}
 B ∈ VN: σ(B,ε) = {C | B –> C ∈ P}  {D | B –> ε ∈ P}
 a ∈ VT: σ(D,a) = {}
 σ(D,ε) = {}
моделює виведення слів регулярної граматики G й розпізнає
мову L(G).

103
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Теорема 3. Якщо M – недетермінований скінченний автомат,


то існує G-граматика типу 3 така, що L(M) = L(G).
Доведення. Нехай M = (Q, T, σ, q0, F) – недетермінований
скінченний автомат. Наступна регулярна граматика G = (Q, T, P,
q0) із правилами виведення
P = {q –>ap | p∈σ(q,a)}  {q –>p | p∈ σ(q,ε)}  {q –>ε | q∈F}
породжує мову L(G) = L(M).

2.2.5. Задачі
1. Побудувати детермінований скінченний автомат, що
розпізнає мову, задану регулярним виразом a*(a+b)*a.
2. Побудувати недетермінований скінченний автомат, що
розпізнає усі слова в алфавіті {a,b,c}, за винятком слів
мови, заданої регулярним виразом c*(a+b)*c.
3. Побудувати скінченний недетермінований автомат, що
розпізнає мову в алфавіті T = {a,b}, яка складається зі
слів:
a. у яких друга літера від кінця – b;
b. що починаються й закінчуються різними літе-
рами;
c. що містять не менше, ніж два входження сим-
волу a.
4. Побудувати скінченний детермінований і недетермінова-
ний автомати, що розпізнають мову в алфавіті T = {a,b},
яка складається зі слів:
a. що закінчуються ланцюжком ааbba;
b. у яких за кожним символом а безпосередньо
йде b.
5. Побудувати скінченний детермінований автомат, який
допускає над алфавітом T = {a,b}:
a. порожню мову;
b. мову, що містить лише одне слово – порожній
ланцюжок;
c. усі слова, що включають ланцюжок аba;

104
Моделі обчислень у програмній інженерії

d. усі слова із не менше, ніж одним входженням


символу а, і точно двома входженнями b;
e. усі слова з парним входженням символу a й
непарним входженням символу b.
 Зауваження. Інколи простіше спочатку
побудувати недетермінований автомат із
відповідними властивостями, а потім його
детермінізувати.
6. Мова L в алфавіті T = {a,b} складається зі слів, що:
a. містять точно одне входження символу b;
b. містять не більше, ніж два входження символу a;
c. починаються й закінчуються однаковою літерою.
Побудувати:
a. скінченний недетермінований автомат, що
розпізнає мову L;
b. скінченний детермінований автомат, що розпізнає
мову L.

2.3. Регулярні вирази


2.3.1. Алгебра Кліні
Нехай T – скінченний алфавіті T* – множина УСІХ слів в
алфавіті T.
Довільна множина L⊆T* – мова в алфавіті T.
Наступні операції, застосовані до мов L, L1, L2 в алфавіті T
обчислюють (формують) нову мову в алфавіті T:
 L1  L2 – об’єднання мов;
 L1 L2 = {xy | x ∈ L1, y ∈ L2} – конкатенація мов;
0 1 2 3
 L* = ⋃∞ 𝑖
𝑖=0 𝐿 , де L = {ε}, L = L, L = L∙L, L = L∙L∙L, …, –
ітерація.
Означення 1. Алгебра Кліні в алфавіті T – це ВСІ мови, які
можна отримати зі скінченних мов в алфавіті T за допомогою
скінченної кількості операцій об’єднання, конкатенації та іте-
рації.
Приклад 1. Приклади мов в алфавіті T = {a,b}.

105
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 L1 = {a} – мова містить одне слово довжини 1


 L1* = L10  L11  L12U… = {ε, a, aa, aaa, …} = {an | n ≥ 0}
 L2 = {b} – мова містить одне слово довжини 1
 L2* = L20  L21  L22U… = {ε, b, bb, bbb, …} = {bn | n ≥ 0}
 L = L1  L2 = {an | n ≥ 0}U{bm | m ≥ 0}
У програмуванні ці мови часто позначають наступними
(регулярними) виразами:
 L1 – a
 L∗1 – a*
 L2 – b
 L∗2 – b*
 L – (a*) | (b*)

2.3.2. Регулярні вирази


Припустимо, що символи ‘0’, ‘1’, ‘(‘, ‘)’ і ‘*’ НЕ належать
алфавіту T.
{‘0’, ‘1’, ‘(‘, ‘)’, ‘*’}∩T = {}
Означення 2. Регулярний вираз в алфавіті T – це слово пев-
ного виду в алфавіті {‘0’, ‘1’, ‘(‘, ‘)’, ‘*’}  T.
0, 1 і a, a ∈ T, – прості регулярні вирази.
Якщо p і q – регулярні вирази, то (p), p|q, pq, p* – теж
регулярні вирази.
Означення 3. Кожний регулярний вираз r задає мову L(r) в
алфавіті T – елемент алгебри Кліні.
 0 задає L(0) = {} – порожню мову.
 1 задає L(1) = {ε} – мову, яка містить одне слово –
порожнє слово ε.
 Якщо вирази p і q задають мови L(p) і L(q) відповідно, то:
o (p) задає L((p)) = L(p);
o p|q задає L(p|q) = L(p)UL(q);
o pq задає L(pq) = L(p)L(q);
o p* задає L(p*) = (L(p))*.

106
Моделі обчислень у програмній інженерії

Круглі дужки ( і ) використовуються для встановлення по-


рядку виконання операцій.
 Регулярний вираз a|bc залежно від порядку виконання
операцій об’єднання | і конкатенації може задавати дві
різні мови:
o (a|b)c – {ab, ac}
o a|(bc) – {a, bc}
 Аналогічно a|b*:
o a|(b*) – {a, ε, b, bb, bbb, …}
o (a|b)* – {ε, a, b, aa, ab, ba, bb, …}
У разі відсутності дужок використовується пріоритет опера-
цій: ітерація *, конкатенація, об’єднання |. Наступні пари регу-
лярних виразів задають одну мову:
 a|bc і a|(bc)
 ab* і a(b*)

2.3.3. Регулярні вирази й скінченні автомати


Теорема 1. Мова L(r), що задається регулярним виразом r,
розпізнається деяким скінченним недетермінованим автома-
том M:
L(r) = L(M).
Доведення. Для доведення досить показати, як за регулярним
виразом побудувати скінченний недетермінований автомат, що
розпізнає мову, задану регулярним виразом.
Побудуємо скінченні автомати R1, R2, R3 для простих регу-
лярних виразів 0, 1, a, де a  T.
 0 , L(0) = {},R1 = ({q0,q1}, T, σ, q0, {q1}), де σ – всюди не-
визначена функція переходів q {q0,q1}a T, σ(q, a) =
= {} і σ(q, ε) = {} (мал. 1).
 1 , L(1) = {ε},R2 = ({q0,q1}, T, σ, q0, {q1}), де σ(q0,ε) =
= {q1}, σ(q1,ε) = {},q {q0,q1}a Tσ(q,a) = {} (мал. 2).

107
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 a, L(a) = {a},R3 = ({q0,q1}, T, σ, q0, {q1}), де σ(q0,a) =


= {q1}, σ(q1,a) = {},q{q0,q1}b T\{a}σ(q,b) = {}iσ(q,
ε) = {} (мал. 3).
Нехай p і q – регулярні вирази, що задають мови, які розпі-
знають скінченні автомати M1 = (Q1, T, σ1, q1, F1) i M2 = (Q2, T,
σ2, q2, F2) такі, що мають різні стани Q1∩Q2 = {} та L(p) = L(M1)
i L(q) = L(M2).
Тоді мови L(pq), L(p|q), L(p*) виразів pq, p|q, p* розпізнають
скінченні автомати:
R4 = (Q1  Q2, T, σ4, q1, F2),
σ4 = σ1  σ2  {σ4(q,ε) = {q2}| qF1}
R5 = (Q1  Q2  {q0,f0}, T, σ5, q0, {f0}),
σ5 = σ1  σ2  {σ5(q0,ε) = {q1,q2}}  {σ5(q,ε) = {f0}| qF1  F2}
R6 = (Q1  {q0,f0}, T, σ6, q0, {f0}),
σ5 = σ1  {σ5(q0,ε) = {q1,f0}}  {σ5(q,ε) = {q1,f0}| qF1}
структуру яких зображено на мал. 4, мал. 5 та мал. 6 відповідно.

Теорема 2. Якщо M – скінчений детермінований автомат, то


існує регулярний вираз r, що задає мову, яку розпізнає авто-
мат M, тобто
L(r) = L(M).

108
Моделі обчислень у програмній інженерії

Доведення. Нехай M = (Q, T, σ, q0, F) – скінченний де-


термінований автомат. Вважатимемо, що його стани впорядко-
вані й послідовно перенумеровані. Якщо це не так, то їх можна
попередньо перенумерувати.
Q = {p1, …, pn}, q0  Q, F  Q
Побудуємо базові регулярні вирази Rij для 0 ≤ i,j ≤ n:
0

якщо не існує a  T такого, що σ(pi, a) = pj, то Rij


0
 0

R
0
 якщо {ai1, ai2, …, aik} = {a | σ(pi, a) = pj}, то ij = ai1 | ai2 |
… | aik
Перехід (s,w) *=> (t,u) із конфігурації (s,w) в конфігурацію
(t,u) називається переходом через стани p1, …, pk і позначається
(s,w) *=1...k => (t,u) тоді й тільки тоді, коли існують конфігу-
рації (q1,x1) = (s,w), (q2,x2), …, (qn,xn) = (t,u) такi, що (qi,xi) =>
(qi+1,xi+1), i=1,…,n–1; q2,…, qn-1{p1,…, pk}; q1 = s; qn = t. У про-
цесі такого переходу в якості проміжних вершин використову-
ються лише вершини з {p1,…, pk}.
Покажемо, як побудувати регулярний вираз Rij , що задає
k

мову (множину)
{w | (pi,w) *= 1…k => (pj,ε)}
k 1 *
Rij = Rij | Rik ( Rkk ) Rkj
k k 1 k 1 k 1

R
k
 Вираз ij будується (задається) із використанням виразів
R
k 1
tq .
R
n
Регулярний вираз ij задає мову (множину) {w | (qi,w) *=>
(qj,ε)}.
Якщо F = {pi1, …, pim}, то вираз
Rq 0 pi1 | Rqopi2 | … | R
n n n
q0 pim

задає мову L(M).

109
Глибовець М. М., Кирієнко О. В., Проценко В. С.

2.3.4. Задачі
1. Побудувати регулярний вираз, що задає мову L в алфавіті
T = {a,b}, яка складається зі слів, у яких друга літера від
кінця – b.
2. Побудувати регулярний вираз, що задає мову L в алфавіті
T = {a,b}, яка складається зі слів, що починаються й
закінчуються різними літерами.
3. Задайте мову над алфавітом {0, 1}, усі слова якої почина-
ються з символу 0 і містять, як мінімум, два символи 1, за
допомогою:
a. регулярної граматики;
b. регулярного виразу;
c. скінченного недетермінованого автомата;
d. скінченного детермінованого автомата.
4. Задайте мову над алфавітом {0, 1}, усі слова якої не
містять двох одиниць поспіль, за допомогою:
a. регулярної граматики;
b. регулярного виразу;
c. скінченного недетермінованого автомата;
d. скінченного детермінованого автомата.
5. Задайте всі слова десяткових цифр, які представляють
число, що ділиться на 5:
a. регулярною граматикою;
b. регулярним виразом;
c. скінченним недетермінованим автоматом;
d. скінченним детермінованим автоматом.
6. Мова L в алфавіті T = {a,b} складається зі слів, що мі-
стять точно один символ b. Побудувати:
a. скінченний недетермінований автомат, що роз-
пізнає мову L;
b. граматику типу 3, що породжує мову L;
c. регулярний вираз, що задає мову L;
d. скінченний детермінований автомат, що розпізнає
мову L.

110
Моделі обчислень у програмній інженерії

7. Регулярний вираз (a|b)*(a*) задає мову L в алфавіті T =


= {a,b}. Побудувати:
a. скінченний недетермінований автомат, що роз-
пізнає мову L;
b. граматику типу 3, що породжує мову L.
8. Задано граматику типу 3 G = (VN, VT, P, S), VN = {S, B},
VT = {a, b, c}, P = {S –> aS, S –> aB, S –> cS, S –> c, B –>
bB, B –> aB, B –> a}. Побудувати:
a. скінченний недетермінований автомат, що
розпізнає мову, яку породжує ця грамматика;
b. регулярний вираз, що задає мову, яку породжує
ця грамматика.

2.4. Контекстно-вільні граматики


2.4.1. Контекстно-вільна граматика
Означення 1. Контекстно-вільна граматика (КВ-граматика)
G = (VN, VT, P, S) має:
 Алфавіти нетерміналів VN і терміналів VT, що не
перетинаються: VN  VT = {}.
 Початковий нетермінал S  VN.
 Об’єднаний алфавіт, що позначається V = VN  VT.
 Скінченну множину правил A–>α  P, де A VN, α V*.
Означення 2. Нехай α, β  V*, то β безпосередньо виводиться з
α в граматиці G (α=>β), якщо існують A–>γ  P, α1  V*, α2  V*
такі, що α = α1Aα2, β = α1γα2.
Означення 3. Нехай α, β  V*, то β виводиться з α в граматиці
G (α*=>β), якщо:
 α =β або
 α0 = α, α1, α2, …, αn = β такі, що α0, α1, α2, …, αn  V*,
αi=>αi+1 для 0≤i<n.
Послідовність α0 = α =>α1 =>α2 => … =>αn = β – виведення
слова β зі слова α.
Означення 4. Множина L(G) = {w | w  VT*, S *=>w} – мова,
породжувана граматикою G.

111
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Означення 5. Граматики G1 і G2 – еквівалентні, якщо L(G1) =


L(G2).
Означення 6. Мова M називається контекстно-вільною мовою
(КВ-мова), якщо існує КВ-граматика G така, що L(G) = M.

2.4.2. Лівостороння і правостороння вивідності


Означення 7. Виведення α0 = α =>α1 =>α2=> … =>αn у гра-
матиці G називається лівостороннім, якщо для 1≤i<n, αi. = xiAiβi,
xi  VT*, Ai  VN, βi  V*, Ai –>γi  P, αi+1. = xiγiβi. Лівостороння без-
посередня вивідність позначається =lm>, а лівостороння вивід-
ність – *=lm> (lm – скорочення від leftmost).
Означення 8. Виведення α0 = α =>α1 => α2 => … => αn у
граматиці G називається правостороннім, якщо для 1≤i<n, αi. =
= βiAixi,xi  VT*, Ai  VN, βi  V*, Ai –>γi  P, αi+1. = βiγixi. Правосто-
роння безпосередня вивідність позначається =rm>, а правосто-
роння вивідність – *=rm> (rm – скорочення від rightmost).
Приклад 1. У граматиці G = (VN, VT, P, S) VN = {S,A}, VT =
= {a,b}, P = {S –>aAS, S –>a, A –>SbA, A –>ba, S –>ε}.
 Одне із виведень слова aabba має вигляд:
o S =>aAS =>aA =>aSbA =>aabA =>aabba
 Лівостороннє виведення слова aabba має вигляд:
o S=lm>aAS =>aSbAS =lm>aabAS =lm> aabbaS=lm>
aabba
 Правостороннє виведення слова aabba має вигляд:
o S =rm>aAS =rm>aA =rm>aSbA =rm>aSbba=rm>
aabba
Властивість 1. Якщо G = (VN, VT, P, S) – КВ-граматика, у
якої S *=>w, w  VT*, то існує лівостороннє виведення S *=lm>w
і правостороннє виведення S *=rm>w.

2.4.3. Дерево виведення


Означення 9. Нехай G = (VN, VT, P, S) – КВ-граматика. Дерево
виведення (дерево розбору, синтаксичне дерево) для G – це
помічене дерево, що відповідає таким умовам:

112
Моделі обчислень у програмній інженерії

 Кожен вузол має мітку – символ із V або ε.


 Мітка кореня – S.
 Якщо вузол має хоч одного сина, то його мітка –
нетермінал.
 Якщо вузли n1, n2, …,
nk – сини вузла n,
перераховані зліва
направо, з мітками A1,
A2, …, Ak, а мітка вузла
n – A, то A –>A1A2…
Ak  P.
 ε може бути міткою
тільки листка.
На мал. 1 наведено приклад
дерева виведення для попередньої граматики G. Це дерево
можна побудувати за довільним виведенням слова aabba. За
деревом можна побудувати лише одне лівостороннє (правосто-
роннє) виведення слова aabba. Крона дерева – це результат виве-
дення.
Властивість 2. Нехай G = (VN, VT, P, S) – КВ-граматика. Ви-
ведення S *=>α, α  V* існує тоді й тільки тоді, коли існує дерево
виведення в граматиці G із кроною (результатом) α.
Кожному слову, що виводиться в граматиці, може відповідати
декілька виведень.
Кожному дереву виведення може відповідати декілька ви-
ведень.
Властивість 3. Кожному дереву виведення у КВ-граматиці G
відповідає лише одне лівостороннє й одне правостороннє виве-
дення.

2.4.4. Мова, неоднозначність


Теорема 1. Існує алгоритм для визначення, чи є мова, пород-
жувана КВ-граматикою, порожньою.

113
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Доведення. Нехай КВ-граматика G = (VN, VT, P, S) така, що


L(G) ≠ {} і має m різних нетерміналів
(m = |VN|). Існує x  VT*, S *=>x. Вико-
риставши одне із виведень, будуємо
дерево виведення.
Якщо в цьому дереві виведення
існує гілка завдовжки більше m, то на
цій гілці трапляється два однакових
нетермінали A. У цьому разі за цим деревом шляхом простого
перетворення (мал. 2) можна побудувати дерево виведення з
коротшою гілкою. Це дерево теж задає виведення термінального
слова.
Виконавши декілька таких перетворень, отримаємо дерево
виведення термінального слова, яке не має жодної гілки
завдовжки більше m.
Для перевірки того, що КВ-граматика породжує порожню
мову, досить побудувати УСІ дерева виведення з гілками не
довше m (їхня скінченна множина) й перевірити, чи є серед них
термінальний вивід.
Означення 10. КВ-граматика G – неоднозначна, якщо існує
w  VT* таке, що w  L(G) й існує принаймні два дерева виведення
із кроною w. Тобто для такого слова w існує більше одного
правостороннього (лівостороннього) виведення.
Означення 11. КВ-мова M – неоднозначна, якщо не існує
однозначної КВ-граматики, що породжує цю мову.
Властивість 4. Мова M = {anbncm | n,m≥0}  { anbmcm |
n,m≥0} – неоднозначна КВ-мова.

2.4.5. Задачі
1. Задано граматику G = (VN, VT, P, S), VN = {S, D, L}, VT =
{0, 1, a, b}, P = {S –>LDL, L –>La, L –>Lb, L –> a, D –>
D0, D –> D1, D –> 0, D –> 1}.
a. Побудувати дерева виведення наступних слів:
ab10aa, abba0a, a1a.

114
Моделі обчислень у програмній інженерії

b. Довести, що слова ab10, b1a, abba не виводяться в


цій граматиці.
c. Описати мову, що породжується цією граматикою.
2. Побудувати граматику, що породжує мову
L = {αcβcγc | α, β, γ – довільні слова паліндроми з a і b}
3. Побудувати лівостороннє й правостороннє виведення
слова abbbb у граматиці G = (VN, VT, P, S), VN = {S, A, B},
VT = {a, b}, P = {S –>aAB, A –>bBb, B –>A, B –>ε}.
4. Задано граматику G = (VN, VT, P, S), VN = {S, A}, VT =
= {0,1}, P = {S –> SS, S –> A, A –> 0A1, A –> S, A –> 01}.
Побудувати три різні дерева виведення для слова
00011011.
5. Задано граматику G = (VN, VT, P, S), VN = {S, A, B, D},
VT = {a, b, c, d}, P = {S –>aSbA, S –>a, A–>c, A –>AB,
B –>bBD, B –>b, D –>dD, D –>d}.
Побудувати дерево виведення слова aaabcbbdbc. За дере-
вом побудувати лівостороннє й правостороннє виведення
слова.
6. Побудувати усі можливі дерева виведення слова cabbca
в граматиці G = (VN, VT, P, S), VN = {S, B}, VT = {a, b, c},
P = {S –>SS, S –>a, S –>c, S –>aBc, B –>b, B –>Bb}.
7. Задано граматику G = (VN, VT, P, S), VN = {S}, VT = {i, +, –,
*, /, (, )}, P = {S –> S+S, S –> S–S, S –> S*S, S –> S/S, S –>
(S), S –> i}. Побудувати всі можливі дерева виведення
слова i*(i+i–i)+i в цій граматиці.
8. Задано граматику G = (VN, VT, P, S), VN = {S}, VT = {i, +, –,
*, /, (, )}, P = {S –> S+S, S –> S–S, S –> S*S, S –> S/S, S –>
(S), S –> i}. Побудувати усі можливі дерева виведення
слова i*(i+i–i)+i в цій граматиці.

115
Глибовець М. М., Кирієнко О. В., Проценко В. С.

2.5. Властивості КВ-граматик


2.5.1. Приведення КВ-граматик
Означення 1. Нехай G = (VN, VT, P, S) – КВ-граматика, у якої
V = VN  VT, тоді:
 Нетермінал A  VN – продуктивний, якщо існує x  VT*
таке, що S *=>x.
 Нетермінал A  VN – досяжний, якщо існують A–>x P і
x  VT*, β  V* такі, що S *=>αAβ.
 Граматика G – приведена, якщо для довільного A  VN
існує виведення
S *=> x1 A x3 *=>x1 x2 x3 таке, що x1,x2,x3  VT*.
 Правило виведення A –>B, A, B VN називається ланцю-
говим.
 Правило виведення A –>ε називається анулювальним.
Властивість 1. Для довільної КВ-граматики G можна
побудувати еквівалентну КВ-граматику G1, у якої:
 усі нетермінали продуктивні;
 усі нетермінали досяжні;
 немає ланцюгових правил виведення.
Приклад 1. Нехай G = (VN, VT, P, S), де VN = {S,A,B}, VT = {a,
b, c, d}, P = {S –>aSa, S –>bAb,S –>c, A –>cBd, A –>aSd,
B –>dBc}.
Для пошуку непродуктивних нетерміналів послідовно буду-
ють наступні множини нетерміналів M0, M1, …, Mn, Mn+1, доки не
повторяться Mn = Mn+1.
 M0 = {A VN | A –>x  Pіx  VT*}
 Mi = {A  VN | A –>α  Pіα (VT  Mi-1)*}
Усі нетермінали із множини VN–Mn непродуктивні.
Для граматики G маємо: M0 = {S}, M1 = {S, A}, M2 = {S, A}.
Нетермінал B непродуктивний, після вилучення якого із грама-
тики G отримуємо граматику G1.
G1 = (V`N, VT, P`, S), де V`N = {S,A} і P` = {S –>aSa, S –>bAb,
S –>c, A –>aSd}.

116
Моделі обчислень у програмній інженерії

Приклад 2. Нехай G = (VN, VT, P, S), де VN = {S,A,B}, VT =


= {a,b,c}, P = {S –>aSb, S –>c, A –>bS, A –>a, S –>cBS, B –>a}.
Для пошуку недосяжних нетерміналів послідовно будують
наступні множини нетерміналів M0, M1, …, Mn, Mn+1, доки не
повторяться Mn = Mn+1.
 M0 = {S}
 Mi = {B  VN | A –>α1Bα2  PіA  Mi-1}  Mi-1
Усі нетермінали із множини VN–Mn недосяжні.
Для граматики G маємо: M0 = {S}, M1 = {S, B}, M2 = {S, B}.
Нетермінал A недосяжний, після вилучення якого із граматики G
отримуємо граматику G1.
G1 = (V`N, VT, P`, S), де V`N = {S,B} і P` = {S –>aSb, S –>c,
S –>cBS, B –>a}.
Приклад 3. Нехай G = (VN, VT, P, S), де VN = {S,T,F}, VT = {+,
*, (, ), d}, P = {S –>S+T, S –>T, T –>T*F, T –>F, F –>(S), F –>d}.
Щоб вилучити ланцюгові правила {S –>T, T –>F}, виконують
наступне:
 Ділять множину правил виведень P на дві підмножини:
o P1 = {S –>T, T –>F}
o P2 = {S –>S+T, T –>T*F, F –>(S), F –>d}
 Для кожного з нетерміналів {S, T} знаходять множину не-
терміналів, що вивідні з нього, використовуючи лише
ланцюгові правила виведення:
o S --- {T,F}
o T --- {F}
 Для кожного з нетерміналів {S, T} будують множину но-
вих правил виведення, які можуть замінити відповідне
ланцюгове правило, використовуючи правила з P2 й от-
римані множини нетерміналів:
o PS = {S –>T*F, S –>(S), S –>d} /S –>T ….
{T,F}/
o PT = {T –>(S), T –> d} /T –>F …. {F}/

117
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Нові правила виведення P’ підсумкової граматики отри-


мують об’єднанням побудованих множин правил виве-
дення та множини P2.
 P’ = P2  PS  PT = {S –>S+T, T –>T*F, F –>(S), F –>d,
S –>T*F, S –>(S), S –> d, T –>(S), T –> d}
Властивість 2. Для довільної КВ-граматики G = (VN, VT, P,
S) можна побудувати еквівалентну КВ-граматику G0 = (V’N, VT,
P0, S0), у якої:
 S0  VN
 S0 не з’являється у правій частині жодного правила виве-
дення з P0.
 Усі правила виведення A–>α  P0 (A ≠ S0) – не анулю-
вальні.
 S0–>ε  P0 тоді й тільки тоді, коли ε L(G).
Приклад 4. Нехай G = (VN, VT, P, S), де VN = {S}, VT = {a,b},
P = {S –>aSbS, S –>bSaS, S –>ε}.
На першому кроці будують проміжну еквівалентну грама-
тику із додаванням одного нового нетермінала й одного нового
правила виведення.
G’ = (V’N, VT, P’, S0), V’N= VN  {S0}, P’ = P  {S0 –>S}
На другому кроці вилучають усі анулювальні правила виве-
дення, додають нові правила виведення, що компенсують вилу-
чення анулювальних, і додають, якщо ε  L(G), правило
виведення S0–>ε.
G0 = (V’N, VT, P0, S0),
P0 = {S0 –>S, S –>aSbS, S –>abS, S –>aSb, S –>ab,
S –>bSaS, S –>baS, S –>bSa, S –>ba, S0 –>ε}

2.5.2. Перетворення КВ-граматик


Приклад 5. Нехай G = (VN, VT, P, S), де VN = {S}, VT = {+, –,
(, ), d}, P = {S –>S+S, S –>S–S, S –>(S), S –>d}. Граматика є неод-
нозначною.

118
Моделі обчислень у програмній інженерії

Слово в ал-
фавіті d-d-d  L(G)
має два різні
лівосторонні
виведення.
 S => S-S
=> d-S =>
d-S-S => d-d-S => d-d-d
 S => S-S => S-S-S => d-S-S
=> d-d-S => d-d-d
Цим виведенням відповідають
два різні дерева виведення (мал. 1 і
мал. 2 відповідно).
Неоднозначність граматики відоб-
ражає два можливі варіанти обчис-
лення виразу d-d-d.

 d-(d-d), якщо операція –


правоасоціативна.
 (d-d)-d, якщо операція –
лівоасоціативна.
Увівши додатковий не-
термінал T, граматику можна
перетворити на дві різні одно-
значні граматики G1 і G2 із
множинами виведень:
 P1 = {S –> T+S, S –> T-S, S –> T, T –> d, T –> (S) }, опе-
рації + і – правоасоціативні.
 P2 = {S –> S+T, S –> S-T, S –> T, T –> d, T –> (S) }, опе-
рації + і – лівоасоціативні.
З виведенням слова d-d-d у граматиці G1 зв’язане обчис-
лення d-(d-d), якому відповідає дерево виведення, зображене на
мал. 3.

119
Глибовець М. М., Кирієнко О. В., Проценко В. С.

З виведенням слова d-d-d у граматиці G2 зв’язане обчис-


лення (d-d)-d, якому відповідає дерево виведення, зображене на
мал. 4.
Означення 2. Правило виведення A –>Aα називається ліво-
рекурсивним.
Теорема 1. Якщо КВ-граматика G = (VN, VT, P, S) має ліво-
рекурсивні правила, то існує еквівалентна їй КВ-граматика G1
без ліворекурсивних правил.
Доведення. Продемонструємо ідею побудови еквівалентної
граматики, що не має ліворекурсивних правил, для випадку од-
ного нетермінала A VN, який має лише два правила виведення:
ліворекурсивне A –>Aα  P і A –>β  P, де β не починається з A.
Використовуючи ці правила виведення, можна побудувати
наступні сентенційні форми:
A => β, A => Aα => βα, A *=> βαα, …, A *=> βαn
Якщо ввести додатковий нетермінал A’ й замінити два попе-
редні правила на наступні:
A –>β, A –>βA’, A’ –>α, A’ –>αA’
то, використовуючи ці правила виведення, можна побудувати ті
самі сентенційні форми:
A => β, A => βA’ => βα, A => βA’ => βαA’ => βαα, …, A *=>βαn
Приклад 6. Нехай G = (VN, VT, P, S), де VN = {S,T}, VT = {+, -,
(, ), d}, P = {S –>S+T, S –>S-T, S –>T, T –>(S), T –>d}. З не-
терміналом S зв’язано три правила виведення S –>S+T, S –>S-T,
S –>T, до того ж S –>S+T і S –>S-T – лівосторонні. Для їх вилу-
чення вводиться додатковий нетермінал S’ – нові правила виве-
дення S –> TS’, S’ –> +T, S’ –> +TS’, S’ –> -T, S’ –> -TS’. Еквіва-
лентна граматика G1 = (V’N, VT, P’, S), V’N = {S,S’,T}, VT = {+, -,
(, ), d}, P’ = {S –>T, S –>TS’, S’ –> +S, S’ –> +TS’, S’ –> -T, S’ –>
-TS’, T –>(S), T –>d} не має лівосторонніх виведень.

120
Моделі обчислень у програмній інженерії

2.5.3. Схеми граматик


КВ-граматики надають можливість описувати мови програ-
мування. Крім того, вони дозволяють ефективно розв’язувати
задачу синтаксичного аналізу (див. наступні розділи).
Ці особливості КВ-граматик зумовили появу багатьох фор-
малізмів, еквівалентних КВ-граматикам, але ефективніших (про-
зоріших, зручніших) для опису конкретних мов програмування.
Ці формалізми інколи називають схемами граматик.
Усі схеми граматик містять правила виведення, які визнача-
ють синтаксис мови програмування. Для задання правил виве-
дення використовуються різні форми опису:
 Символічна (КВ-граматики);
 Нотація Бекуса-Наура (BNF, Backus-Naurform);
 Ітераційна форма;
 Синтаксичні діаграми.
Нотація Бекуса-Наура була створена для опису синтаксису
однієї з перших мов програмування Algol 60. Однією з причин
введення нотації є те, що синтаксис реальної мови програ-
мування має велику кількість нетерміналів, що робить синтак-
сичну форму запису ненаочною. У нотації Бекуса-Наура:
 Нетермінал – це комбінація слів у кутових дужках <…>.
 Для скорочення запису правила з однаковим лівим не-
терміналом об’єднуються в одне за допомогою символу |.
 Замість –> уживають ::=
 Наприклад:
o <список> ::= <елемент_списку><cписок>
|<елемент списку>
Ітераційну форму введено для отримання компактнішого за-
дання КВ-граматик. Вважається, що у КВ-граматиці G = (VN, VT,
P, S) узагальнений алфавіт V = VN  VT задовольняє умову
{0,1,(,),*}∩V = {}. Замість правил виведення виду A –>α  P,
A  VN можна вживати правила виведення A –>r(V), де r(V) –
довільний регулярний вираз над алфавітом V.

121
Глибовець М. М., Кирієнко О. В., Проценко В. С.

КВ-граматику G = (VN, VT, P, S), де VN = {S,B}, VT = {+, [, ],


d}, P = {S –>[SB], S –>d, B –> +SB, B –>ε} можна задати в ітера-
ційній формі, використавши одну з наступних множин правил
виведення:
 P1 = { S –> d | [SB], B –> +SB | 1 }
 P2 = { S –> d | [S(+S)*] }
Синтаксичні діаграми для наочності задають графічне зоб-
раження ітераційної форми. Введені Н. Віртом для опису синтак-
сису мови програмування Паскаль.
За довільною КВ-граматикою в ітераційній формі можна по-
будувати еквівалентний набір синтаксичних діаграм. На мал. 5
зображено синтаксичну діаграму для граматики G в ітераційній
формі із правилами виведення P2.

2.5.4. Задачі
1. За контекстно-вільною граматикою
G = (VN, VT, P, S), VN = {S, A, B, C, E}, VT = {0, 1, a}, P = {S
–>0A0, S –>1B1, S –>BB, A –>C, A –>EaA, B –>S, B –>A,
C –>S, C –>ε, C –>CE, E –>CE, E –>aE}
побудувати еквівалентну їй контекстно-вільну:
d. приведену граматику;
e. граматику без ланцюгових правил виведення;
f. граматику без анулювальних правил виведення
A –>ε.
2. За контекстно-вільною граматикою
G = (VN, VT, P, S), VN = {S, B}, VT = {a, b}, P = {S –>aSbS,
S –>BaB, B –>ε, B –>bS}
побудувати еквівалентну їй контекстно-вільну граматику
без анулювальних правил виведення A –>ε. Яку мову
вона породжує?

122
Моделі обчислень у програмній інженерії

3. За контекстно-вільною граматикою
G = (VN, VT, P, S), VN = {S, A, B, C, D, E}, VT = {a, b, d},
P = {S –>aA,
S –>aBB, A –>aaA, A –>ε, B –>bB, B –>bbC, C –>B,
C –>DdA, E –>CE}
побудувати еквівалентну їй контекстно-вільну:
a. приведену граматику;
b. граматику без ланцюгових правил виведення;
c. граматику без анулювальних правил виведення
A –>ε.
4. За контекстно-вільною граматикою
G = (VN, VT, P, S), VN = {S, A, B, C, E}, VT = {a, b, c, d},
P = {S –>ASB,
S –>aB, A –>aAS, A –>a, A –>aEb, B –>SbS, B –>A,
B –>bb, B –>ε, C –>cC, C –>EdA, E –>cE, E –>BdC}
побудувати еквівалентну їй контекстно-вільну:
d. приведену граматику;
e. граматику без ланцюгових правил виведення;
f. граматику без анулювальних правил виведення
A –>ε.
5. Побудуйте усі дерева виведення слова a;a;a;a у грама-
тиці G = ({S}, {a,;}, {S –>S;S, S –>a}, S), що породжує
послідовності символів a, розділених крапкою з комою.
Ця граматика – неоднозначна. Неоднозначність дерева
виведення в цій граматиці пояснюється можливістю по-
різному структурувати групу символів, що стоять поряд
(правило S –>S;S).
a. Побудуйте дві однозначні граматики, які ви-
окремлють по одному символу з такої групи або
ліворуч, або праворуч.
b. Побудуйте дерево виведення слова a;a;a;a в
побудованих граматиках.

123
Глибовець М. М., Кирієнко О. В., Проценко В. С.

2.6. Магазинні автомати


2.6.1. Магазинний автомат і конфігурація
Означення 1. Магазинний автомат (МП-автомат) M = (Q, T,
U, σ, q0, Z0), де:
 Q – скінченна множина станів, q0∈Q;
 T – скінченний вхідний алфавіт;
 U – скінченний магазинний алфавіт;
 σ – скінченна функція переходу,
σ: Q×(T  {ε})×U−>2QxU*;
 q0 – початковий стан;
 Z0∈U – початковий символ магазину.
Магазинний ав-
томат – це логічний
пристрій, що має
стрічку із вхідним
словом x і допоміжну
стрічку (магазин) зі
словом w, із якою він
взаємодіє на вершині.
На кожному кроці
роботи МП-автомат перебуває в одному зі станів q із множини
Q, читає верхній символ z на допоміжній стрічці й, можливо,
читає наступний символ a на вхідній стрічці.
У процесі роботи автомат або прочитує символ a на вхідній
стрічці, або залишається на місці, переходячи з одного стану в
інший і замінюючи верхній символ у магазині на деяке слово
згідно із функцією переходів σ.
Означення 2. Конфігурація МП-автомата (q,ax,Zw) включає:
 q – поточний стан, q∈Q.
 ax – ще непрочитану частину вхідного слова, ax T*.
 Zw – cлово в магазині, Zw U*.
o Z – верхній (лівий) символ на вершині магазину.
Означення 3. Початкова конфігурація (q0, x, Z0).

124
Моделі обчислень у програмній інженерії

 x – вхідне слово.
Означення 4. Заключна конфігурація (q, ε, ε), q ∈ Q –
довільний.
Означення 5. МП-автомат може переходити з однієї конфігу-
рації в іншу згідно з функцією переходів σ, породжуючи на мно-
жині конфігурації відношення переходу ╞>.
 Якщо поточна конфігурація (q, ax, Zw) і σ(q, a, Z) = {(p1,
γ1), …, {(pm, γm)}, q ∈ Q, a  T, Z  U, для 1≤i≤m, pi ∈ Q, γi 
U*, то МП-автомат може виконати один із m-переходів
1≤i≤m – (q, ax, Zw)╞> (pi, x, γiw).
o МП-автомат читає символ a на вході, заміняє
символ Z на вершині магазину на слово γi й
переходить зі стану q у стан pi.
 Якщо поточна конфігурація (q, x, Zw) і σ(q, ε, Z) = {(p1,
γ1), …, {(pm, γm)}, q∈Q, a
 T, Z  U, для 1≤i≤m,
pi ∈ Q, γi  U*, то МП- (q0,abba,Z0)

автомат може виконати


один із m-переходів
1≤i≤m – (q, x, Zw)╞> (pi, (q0,bba,AZ0) (q1,abba,ε)
x, γiw).
o МП-автомат
заміняє символ Z
на вершині мага- (q0,ba,BAZ0)

зину на слово γi й
переходить зі
стану q у стан pi, (q0,a,BBAZ0) (q1,a,AZ0)
нічого не чита-
ючи на вході.
Означення 6. Розширення
(q0,ε,ABBAZ0)
переходу ╞> відношення ╞*> – (q1,ε,Z0)

це рефлексивне й транзитивне
замикання відношення ╞>.
Тобто (q0, x0, α0) ╞*> (qn, xn, αn) (q1,ε,ε)

125
Глибовець М. М., Кирієнко О. В., Проценко В. С.

(із конфігурації (q0, x0, α0) МП-автомат може потрапити в кон-


фігурацію (qn, xn, αn) тоді й тільки тоді, коли:
 (q0, x0, α0) = (qn, xn, αn) або
 існують q1, …, qn-1 ∈ Q, x1, …, xn-1  T*, α1, …, αn-1  U*
такі, що для 1≤i≤n-1 (qi, xi, αi) ╞> (qi+1, xi+1, αi+1).
Означення 7. МП-автомат M розпізнає мову L(M) = {w  T* |
(q0, w, Z0) ╞*> (q, ε, ε), q∈Q }.

2.6.2. Приклад МП-автомата


Приклад 1. МП-автомат M = (Q, T, U, σ, q0, Z0) має:
 Q = {q0, q1}
 T = {a, b}
 U = {Z0, A, B}
 σ(q0, a, Z0) = {(q0, AZ0)}
σ(q0, b, Z0) = {(q0, BZ0)}
σ(q0, ε, Z0) = {(q1, ε)}
σ(q0, a, A) = {(q0, AA), (q1, ε)}
σ(q0, b, A) = {(q0, BA)}
σ(q0, a, B) = {(q0, AB)}
σ(q0, b, B) = {(q0, BB), (q1, ε)}
σ(q1, a, A) = {(q1, ε)}
σ(q1, b, B) = {(q1, ε)}
σ(q1, ε, Z0) = {(q1, ε)}
Поведінку МП-автомата M на деякому вхідному слові x
можна описати, побудувавши дерево можливих конфігурацій.
 Корінь дерева – початкова конфігурація (q0, x, Z0).
 Якщо вузол дерева містить конфігурацію (q, x, y), то його
сини – всі конфігурації (p, x’, y’) такі, що (q, x, y)╞>
(p, x’, y’).
 x  L(M), якщо в дереві можливих конфігурацій є листок
(q, ε, ε), q∈Q.
 Фактично дерево конфігурацій відображає усі можливі
варіанти недетермінованої роботи МП-автомата M на
вхідному слові x.

126
Моделі обчислень у програмній інженерії

 На мал. 2 показано дерево можливих конфігурацій МП-


автомата M на слові abba.
МП-автомат M розпізнає мову T(M) = {wwr | w∈{a,b}*}, тобто
усі слова-паліндроми в алфавіті T = {a,b}.

2.6.3. Властивості МП-автоматів


Означення 7. МП-автомат – детермінований, коли:
 Для довільних q ∈ Q, Z  U, a ∈ (T  {ε})|σ(q, a, Z)| ≤ 1,
o тобто множина σ(q, a, Z)| містить не більше од-
ного переходу.
 Для довільних q ∈ Q, Z  U, якщо σ(q, ε, Z) ≠ {}, то
o для довільного a  Tσ(q, a, Z) = {}.
Властивість 1. Не існує детермінованого МП-автомата, що
розпізнає мову
{wwr | w∈{a,b}*}.
Теорема 1. Якщо L – КВ-мова, то існує недетермінований
МП-автомат M такий, що L = L(M).
Доведення. Нехай G = (VN, VT, P, S) – КВ-граматика така, що
L = L(G). Розглянемо наступний МП-автомат M = ({q}, VT, VN 
VT, σ, q, S) із функцією переходів:
σ(q, ε, A) = {(q, α) | A –>α  P},
σ(q, ε, a) = {} для всіх a VT,
σ(q, a, a) = {(q, ε)} для всіх a  VT,
σ(q, a, Z) = {}, якщо a ≠ Z.
Із побудови МП-автомата випливають такі властивості:
 Для довільних v  VT*, A  VN, β  V*, B *=lm>vAβ у грама-
тиці G тоді й тільки тоді, коли автомат M може перейти
(q, v, B) ╞*> (q, ε, Aβ).
 Для довільного x  VT*B *=lm>x у граматиці G тоді й
тільки тоді, коли автомат M може перейти (q, x, B) ╞*>
(q, ε, ε).
Із цих властивостей виявляється, що мова, породжувана гра-
матикою G, збігається з мовою, яку розпізнає МП-автомат M.
L(G) = L(M)

127
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Приклад 2. Розглянемо КВ-граматику G = ({S}, {a,b}, P, S),


P = {S –>aSa, S –>bSb, S–>ε}. МП-автомат побудований за нею
згідно з теоремою 1 і має вигляд M = ({q}, VT, VN  VT, σ, q, S) із
функцією переходів
σ(q, ε, S) = {(q, aSa), (q, bSb), (q, ε)},
σ(q, a, a) = {(q, ε)}, σ(q, b, b) = {(q, ε)}.

(q,abba,S)

(q,abba,aSa) (q,abba,bSb) (q,abba,ε)

(q,bba,Sa)

(q,bba,aSaa) (q,bba,bSba) (q,bba,a)

(q,ba,Sba)

(q,ba,aSaba) (q,ba,bSbba) (q,ba,ba)

(q,a,Sbba) (q,a,a)

(q,a,aSabba)
магазин не (q,a,bSbba) (q,a,bba) (q,ε,ε)
очиститься

Мал. 3
Дерево можливих конфігурацій, яке описує поведінку цього
а на вхідному слові abba, наведено на мал. 3.
Теорема 2. Якщо M – недетермінований МП-автомат, то
L(M) – КВ-мова.
Доведення. Нехай M = (Q, T, U, σ, q0, Z0) – МП-автомат.
Розглянемо КВ-граматику G = (VN, VT, P, S), у якої VT = T, VN =

128
Моделі обчислень у програмній інженерії

= {S}  {[qAp] | q, p ∈ Q, A  U} і множина правил виведення


включає {S –> [q0Z0q] | q ∈ Q} для кожного (q1, B1…Bm) ∈ σ(q, a,
A) множину
{[qAp] –> a [q1B1q2] … [qmBmp] | для довільних
q2, …, qm, p∈Q} (*).
 Якщо |Q| = n, |U| = m, то |VN| = 1 + n2 * m.
 Якщо (q1, ε)∈σ(q, a, A), то m=0, q1=p і множина (*) є
{[qAp] –> a}.
Доведення базується на допоміжній властивості:
 [qAp] *=> x тоді й тільки тоді, коли (q, x, A) ╞*> (p, ε, ε).
Якщо врахувати, що існують правила виведення S –> [q0Z0q] для
довільного q, зокрема для q=p, то отримуємо властивість:
 S => [q0Z0p] *=> x тоді й тільки тоді, коли (q0, x, Z0) ╞*>
(p, ε, ε) або, що те саме, L(G)=L(M).

2.6.4. Задачі
1. Побудувати МП-автомат, що розпізнає такі слова в алфа-
віті {a, b}, у яких кількість входжень букви a не переви-
щує кількості входжень букви b.
2. Побудувати МП-автомат, що розпізнає мову правильних
виразів, складених із дужок. Наприклад, слово (( )(( )) )( )
належить цій мові, а слово )( ))) – ні.
a. Побудувати КВ-граматику, що породжує мову
правильних виразів, складених із дужок.
3. Побудувати МП-автомат, що розпізнає слова мови
{anb2n | n ≥ 0}.

2.7. Синтаксичний аналіз


2.7.1. Задача синтаксичного аналізу
Означення 1. Задача синтаксичного аналізу – для заданої
G = (VN, VT, P, S) КВ-граматики й слова w VT* визначити, чи w 
L(G). Якщо належить, то побудувати виведення слова w у грама-
тиці G.

129
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Як правило, у задачі синтаксичного аналізу граматика


зафіксована, а слово змінюється.
Задача синтаксичного аналізу для довільної КС-граматики G й
довільного слова w завжди розв’язна.
 За граматикою G можна побудувати еквівалентну
граматику G1 без ланцюгових правил (A –>B) і анулю-
вальних правил (A –>ε).
 У новій граматиці G1 побудувати УСІ лівосторонні
виведення довжини не більше, ніж |w| (довжина сло-
ва w).
 Якщо w  L(G), а L(G) = L(G1), то його виведення
міститься серед побудованих.
Такий розв’язок неефективний. Для практичних задач
(наприклад, компіляції програм) необхідні ефективні алгоритми
синтаксичного аналізу, хоча б для деяких класів КС-граматик.

2.7.2. Приклад синтаксичного аналізу


Приклад 1. Нехай G = (VN, VT, P,
S), де VN = {S,A}, VT = {a,b} і
множина правил виведення P = {S
–>aAS, A –>SbA, S –>a, A –>ba}.
 Слово aabbaa  L(G), дерево
його виведення зображено
на мал. 1.
 Лівостороннє виведення слова:
S =lm> aAS =lm> aSbAS =lm> aabAS =lm> aabbaS =lm>
aabbaa
 Правостороннє виведення слова:
S =rm> aAS =rm> aAa =rm> aSbAa =rm> aSbbaa =rm>
aabbaa
Існує два підходи до побудови дерева виведення:

130
Моделі обчислень у програмній інженерії

 Проходячи лівосторонній вивід, дерево виведення буду-


ють згори вниз.
 Проходячи правосторонній вивід в оберненому порядку,
дерево виведення будують знизу вгору.

2.7.3. Синтаксичні аналізатори


Синтаксичний аналізатор для КВ-граматики G – це розши-
рення магазинного авто-
мата (мал. 4), яке має:
 Вхід – слово x  VT*,
що аналізується.
 Магазин, який мі-
стить символи VT і
VN та додатково ви-
ділений символ $.
 Вихід для фіксації
виведення слова w; найчастіше це просто послідовність
номерів застосовуваних правил виведення.
Аналізатор розпізнає x  VT* тоді й тільки тоді, коли x  L(G):
 Аналізатор починає роботу в початковій конфігурації.
 Послідовно виконує основні дії, поки може.
 Якщо після закінчення автомат перебуває в заключній
конфігурації, то аналізатор розпізнає (accept) слово x.
Кожний вид синтаксичного аналізу має свій тип аналізатора
зі своїми конфігураціями й основними діями.

131
Глибовець М. М., Кирієнко О. В., Проценко В. С.

2.7.4. Синтаксичний аналіз згори вниз


Конфігурація аналізатора для синтаксичного аналізу згори
вниз (y, u), де y = ai…an – ще нерозпізнана частина вхідного
слова x (ai – її перший символ), u = B…$ – слово в магазині (B –
верхній символ).
 (x, S$) – початкова конфігурація.
 (ε, $) – заключна конфігурація (accept).
У процесі синтаксичного аналізу згори вниз виконуються
основні дії:
 Заміна
Вхід Магазин Дія
(change):
aabbaa S$ Заміна S –> aAS
верхній сим-
вол мага- aabbaa aAS$ Скорочення
зину – abbaa AS$ Заміна A –> SbA
нетермінал abbaa SbAS$ Заміна S –> a
A  VN – abbaa abAS$ Скорочення
замінюється bbaa bAS$ Скорочення
на праву ча- baa AS$ Заміна A –> ba
стину α пра- baa baS$ Скорочення
вила виве- aa aS$ Скорочення
дення A –> a S$ Заміна S –> a
α  P. a a$ Скорочення
 Скорочення $ Accept
(pop): верхній
символ магазину – термінал a  VT скорочується разом із
таким самим терміналом a на вході.
 Вид дії однозначно визначається типом верхнього сим-
волу магазину. Головна проблема під час заміни – визна-
чити, яким правило виведення A –> α1, …, A –> αm  P
скористатися.
Таблиця показує послідовність роботи аналізатора згори вниз
для граматики G із прикладу 1 на слові aabbaa.
Найширший клас КС-граматик, які допускають ефективний
аналіз згори вниз – LL(k)-граматики (lefttoright + leftderivation +

132
Моделі обчислень у програмній інженерії

k). Вони аналізують слово зліва направо й будують лівостороннє


виведення, використовуючи для ухвалення рішення k ще не-
розпізнані символи.

2.7.5. Синтаксичний аналіз знизу вгору


Конфігурація аналізатора для синтаксичного аналізу знизу
вгору (u, y), де y = ai…an – ще нерозпізнана частина вхідного
слова x (ai – її перший символ), u = $...B – слово в магазині (B –
верхній символ).
 ($, x) – початкова конфігурація.
 ($S, ε) – заключна конфігурація (accept).
У процесі синтаксичного аналізу знизу вгору виконуються
основні дії: Магазин Вхід Дія
 Зсув (shift): $ aabbaa Зсув
перший $a abbaa Зсув
вхідний $aa bbaa Згортка (S –> a)
символ –
$aS bbaa Зсув
термінал
$aSb baa Зсув
a  VT
$aSbb aa Зсув
читається й
$aSbba a Згортка (A –> ba)
заноситься
на вершину $aSbA a Згортка (A –> SbA)
магазину. $aA a Зсув
 Згортка $aAa Згортка (S –> a)
(reduce): $aAS Згортка (S –> aAS)
декілька $S Accept
верхніх символів магазину, що утворюють праву частину
α правила виведення A –>α  P згортаються (заміню-
ються) на лівий нетермінал правила виведення A.
 На кожному кроці потрібно визначити, яку дію вико-
нати – зсув чи згортку. У разі згортки необхідно
визначити, яким правилом виведення A1 –> α1, …, Ak –>
αk  P скористатися.

133
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Таблиця показує послідовність роботи аналізатора згори вниз


для граматики G із прикладу 1 на слові aabbaa.
Найширший клас КС-граматик, які допускають ефективний
аналіз знизу вгору – LR(k)-граматики (lefttoright + rightderivation
+ k). Вони аналізують слово зліва направо й будують правосто-
роннє виведення, використовуючи для ухвалення рішення k ще
нерозпізнані символи.

2.8. LL(1)-граматики
2.8.1. Означення LL(1)-граматики
Означення 1. Нехай G = (VN, VT, P, S) – КВ-граматика з
об’єднаним алфавітом V = VN  VT.
 Якщо α  V*, то first(α) = {a VT | α*=lm>ax, x VT*}  {ε |
α*=>ε}
o Очевидно, що first(a) = {a}, a VT, first(ε) = {ε}
 G – LL(1)-граматика, якщо для довільних двох ліво-
сторонніх виведень:
o S *=lm> wAα =lm> wβα *=lm> wx, A –> β P
o S *=lm> wAα =lm> wγα *=lm> wy, A –> γ  P
у яких first(x) = first(y), маємо β = γ.
Означення 2. КВ-граматика G – проста LL(1)-граматика,
якщо в ній немає анулювальних правил виведення (A –>ε) і ВСІ
альтернативи для кожного нетермінала починаються з різних
терміналів.
Приклад 1. КВ-граматика G = ({S, B}, {a, b}, P, S) із множи-
ною правил виведення P = {S –>aBS, S –>b, B –>a, B –>bSB} –
проста LL(1)-граматика.
Властивість 1. КВ-граматика (VN, VT, P, S) – LL(1)-граматика
тоді й тільки тоді, коли
first(βα)  first(γα) = {}
для всіх α, β, γ таких, що існують A –>β  P, A –>γ  P, β ≠ γ і S
*=lm>wAα.

134
Моделі обчислень у програмній інженерії

Означення 2. Нехай G = (VN, VT, P, S) – КВ-граматика з


об’єднаним алфавітом V = VN  VT.
 Якщо β  V*, то follow(β) = {a  VT | S *=>γβα, a first(α)}
 {ε | S*=>γβ}
 G – LL(1)-граматика тоді й тільки тоді, коли для кожного
нетермінала A  VN і всіх правил виведення A –>α1, …,
A –>αn  P виконуються умови:
o first(αi)  first(αj) = {}, 1 ≤ i ≠ j ≤ n
o Якщо αi*=>ε, то first(αj)  follow(A) = {}, 1 ≤ i ≠
j≤n
Означення 3. КВ-граматика називається ліворекурсивною,
якщо в ній існує виведення A *=>Aα, α ≠ ε.
Властивість 2. КВ-граматика не LL(1)-граматика, якщо вона
неоднозначна або ліворекурсивна.

2.8.2. LL(1)-аналізатор
Означення 4. LL(1)-аналізатор для LL(1)-граматики M = (VT,
V  {$}, W, σ, S, $), де:
 VT – вхідний алфавіт;
 V  {$} – алфавіт магазину;
 $  V – «дно» магазину;
 W = {1, 2, …, n} – вихідний алфавіт – список номерів
правил виведення граматики;
 S – початковий символ магазину (початковий нетермінал
граматики);
 σ – керівна таблиця, σ: VN×(VT  {$})−>W  {E}, де дії :
o i  W – Заміна (change) верхнього символу мага-
зину з використанням правила виведення з номе-
ром i.
o E – Помилка (error); аналізатор зупиняється.
Означення 5. Конфігурація LL(1)-аналізатора – (y, u, w), де:
 y – нерозпізнана частина слова x (вхід);
 u – магазин;

135
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 w – список номерів правил виведення (Виведення):


o (x$, S$, ε) – початкова конфігурація;
o ($, $, π) – заключна конфігурація.
Означення 6. На множині конфігурацій задається відношення
наступна конфігурація (╞>), яке визначається керівною табли-
цею σ. Якщо поточна конфігурація – (ax, Bα, w) і:
 B  VN і σ(B,a) = i, то i правило виведення є B –>β, i
o (ax, Bα, w) ╞> (ax, βα, wi)
 B  VT i B = a, то
o (ax, Bα, w) ╞> (x, α, w)
 B = $ i a = $, то аналізатор потрапив у заключну конфігу-
рацію ($, $, w)
o початкове слово x розпізнане (x  L(G)) і w задає
його лівостороннє виведення.
 В усіх інших випадках, знайшовши помилку, аналізатор
зупиняється.
o початкове слово x  L(G)
Означення 7. Відношення ╞*> на конфігураціях – це транзи-
тивне й рефлексивне замикання відношення ╞>.
Якщо (x$, S$, ε) ╞*> ($, $, π), то T(x) = π і π вихід M для вхід-
ного слова x.
Якщо аналізатор, почавши працювати з початкового стану
(x$, S$, ε), не потрапляє в заключний стан, то вважається, що
T(x) – невизначено.
M – аналізатор для граматики G, якщо L(G) = {x | T(x) –
визначено}.

2.8.3. Приклад LL(1)-аналізатора


Приклад 1. Нехай G = (VN, VT, P, S), де VN = {S, A}, VT = {a, b}
і множина правил виведення P = {S –>aAS, A –>SbA, S –>a,
A –>ba}. Для перевірки, чи є граматика G LL(1)-граматикою, шу-
каємо значення функції first() для усіх правил виведення A –>.
A: first(SbA) = {a}, first(ba) = {b}
S: first(aAS) = {a}, first(a) = {a}

136
Моделі обчислень у програмній інженерії

Граматика G не є LL(1)-граматикою (нетермінал S).


Приклад 2. Нехай G = (VN, VT, P, S), де VN = {S, A}, VT = {a,b}
і множина правил виведення P = {S –>aAS, S –>b, A –>a,
A –>bSA}. Для перевірки, чи є граматика G LL(1)-граматикою,
шукаємо значення функції first() для усіх правил виведення
A –>.
A: first(a) = {a}, first(bSA) = {b}
S: first(aAS) = {a}, first(b) = {b}
Граматика G є LL(1)-граматикою.
Для використання в аналізаторі усі правила виведення пере-
нумеровуються:
1: S –>aAS, 2: S –>b, 3:A –>a, 4: A –>bSA
Керівна таблиця-аналізатор для гра-
матик має вигляд: a b $
Розглянемо, як працює аналізатор. S 1 2 E
A 3 4 E
 Вхідне слово abbab:
o Послідовність конфігурацій аналізатора:
(abbab$, S$, ε) ╞> (abbab$, aAS$, 1) ╞> (bbab$,
AS$, 1) ╞> (bbab$, bSAS$, 14) ╞> (bab$, SAS$, 14)
╞> (bab$, bAS$, 142) ╞> (ab$, AS$, 142) ╞> (ab$,
aS$, 1423) ╞> (b$, S$, 1423) ╞> (b$, b$, 14232)╞>
($, $, 14232)
o abbab  L(G)
o Лівосторонній вивід слова abbab:
S =lm> aAS =lm> abSAS =lm> abbAS =lm> abbaS
=lm> abbab
 Вхідне слово aaa:
o Послідовність конфігурацій аналізатора:
o (aaa$, S$, ε) ╞> (aaa$, aAS$, 1) ╞> (aa$, AS$, 1)╞>
(aa$, aS$, 13)╞> (a$, S$, 13)╞> (a$, aAS$, 131)╞>
($, AS$, 131), аналізатор зупиняється
o aaa  L(G)

137
Глибовець М. М., Кирієнко О. В., Проценко В. С.

2.8.4. Побудова керівної таблиці


Нехай G = (VN, VT, P, S) – LL(1)-граматика, правила виве-
дення якої перенумеровані послідовно: 1, 2, … n = |P|. Тоді в
LL(1)-аналізатора M = (VT, V  {$}, W, σ, S, $) для цієї граматики
вихідний алфавіт W = {1,2, …, n} і керівна таблиця σ визна-
чається так:
 Для правила виведення A –>α з номером i:
o Якщо a VT і a  first(α), то σ(A,a) = i.
o Якщо ε first(α), то для довільного b follow(A),
σ(A,b) = i.
 Для всіх інших значень B  VN, b  VT  {$}, σ(B,b) = E.
Приклад 3. Нехай G1 = ({S}, {+, -, (, ), d}, P1, S), де P1 = {S
–>S+S, S –>S-S, S –>d, S –>(S)}. Граматика задає всі правильні
арифметичні вирази, які використовують операції + і -, круглі
дужки ( і ) та операнд d. Граматика неоднозначна, тому це не
LL(1)-граматика.
Приклад 4. Наступна граматика G2 = ({S, T}, {+, -, (, ), d},
P2, S), де P2 = {S –>S+T, S –>S-T, S –>T, T –>d, T –>(S)}, еквіва-
лентна граматиці G1 і є однозначною. Граматика задає мови ви-
разів, у яких операції + і - лівоасоціативні. Але граматика має
ліворекурсивні правила S –>S+T та S –>S-T, тому це не LL(1)-
граматика.
Приклад 5. Граматика G3 = ({S, T, S’}, {+, -, (, ), d}, P3, S), де
P3 = {S –>TS’, S’ –>+TS’, S’ –>-TS’, S’ –>ε, T –>d, T –>(S)},
еквівалентна граматиці G2 і являє собою LL(1)-граматику. Для
цієї граматики:
 first(S) = {d, (}, first(T) = {d,(}, first(S’) = {+, -, ε}
 follow(S’) = {), ε}
Якщо правила виведення мають номери 1: S –>TS’, 2: S’
–>+TS’, 3: S’ –>-TS’, 4: S’
–>ε, 5: T –>d, 6: T –>(S), то + - ( ) d $
керівна таблиця, побудована S E E 1 E 1 E
згідно з наведеним алго- T E E 6 E 5 E
ритмом, має вигляд: S’ 2 3 E 4 E 4

138
Моделі обчислень у програмній інженерії

2.8.5. Побудова функцій first і follow


Визначення функцій first і follow використовують простіші
функції fst і nxt.
V {$}
Побудова функції fst: V –> 2 T :
 Будується послідовність наближень цієї функції fst0, fst1,
…, fstm.
o Для довільного a  VT fsti(a) = fsti+1(a) = {a}.
o Для довільного A  VN fsti(A)  fsti+1(A).
o Існує n, починаючи із якого, ці наближення збіга-
ються: fstn = fstn+1 = …
o Покладаємо fst = fstn.
 Для довільного a  VT fst0(a) = {a}
Для довільного A VN такого, що існує A –>ε  P,
fst0(A) = {$}.
Для довільного A  VN такого, що НЕ існує A –>ε  P,
fst0(A) = {}.
 Побудова fstn за fstn-1.
Покладаємо fstn = fstn-1.
Для кожного правила виведення A –>Y1…Yk і кожного
1 ≤ i≤ k:
o fstn(A) = fstn(A)  (fstn(Y1) - {$})
o Якщо для 1 ≤ j<i, $ fstn(Yj), то fstn(A) = fstn(A) 
(fstn(Yj) - {$})
o Якщо для 1 ≤ j≤k, $  fstn(Yj), то fstn(A) = fstn(A) 
{$}
V {$}
Побудова функції nxt: VN –> 2 T :
 Як і для функції fst, будується послідовність наближень
nxt0, nst1, …, nxtn.
 nxt(S) = {$}.
 Якщо A  VN і A ≠ S, nxt(A) = {}.
 Побудова nxtn за nxtn-1.
Покладаємо nxtn = nxtn-1.

139
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Для кожного правила виведення A –>Y1…Yk і кожного


1 ≤ i≤ k, якщо Yi  VN:
o Якщо i=k, то nxtn(Yk) = nxtn(Yk)  nxtn-1(A)
o Якщо i<k, то nxtn(Yi) = nxtn(Yi)  (fstn(Yi+1) - {$})
o Якщо i<k, i+1<j≤k і для всіх i<m<j $ fstn(Ym),
то nxtn(Yi) = nxtn(Yi)  (fstn(Ym) - {$})
o Якщо для i<j≤k, $  fstn(Yj), то nxtn(Yi) = nxtn(Yi) 
nxtn-1(A)
V {$}
Визначаємо функцію first: V* –> 2 T :
 Якщо α=ε, то first(α) = {$}
 Якщо α=a, a  VT, то first(α) = {a}
 Якщо α=A, A VN, то first(α) = fst(A)
 Якщо α=xβ, x  V i $  fst(x), то first(α) = fst(x)
 Якщо α=xβ, x  V i $ fst(x), то first(α) = (fst(x) - {$}) 
fist(β)
V {$}
Для функції follow: VN –> 2 T просто покладаємо
follow = nxt.
Приклад 6. Граматика G3 = ({S, T, S’}, {+, -, (, ), d}, P3, S), де
P3 = {S –>TS’, S’ –>+TS’, S’ –>-TS’, S’ –>ε, T –>d, T –>(S)}.
Побудова функції fst складається із кроків:
 fst0(S)={}, fst0(T)={}, fst0(S’)={$}, fst0(+)={+}, fst0(-)={-},
fst0(()={(}, fst0())={)}, fst0(d)={d}
 fst1(S)={}, fst1(T)={(,d}, fst1(S’)={+,-,$}, для інших значень
x  V, fst1(x) = fst0(x)
 fst2(S)={(,d}, для інших значень x V, fst2(x) = fst1(x)
 Для всіх значень x  V, fst3(x) = fst2(x)
Побудова функції nxt складається із кроків:
 nxt0(S) = {$}, nxt0(T) = {}, nxt0(S’) = {}
 nxt1(S) = {$,)}, nxt1(T) = {+,-}, nxt1(S’) = {$}
 nxt2(S) = {$,)}, nxt2(T) = {+,-,$,)}, nxt2(S’) = {$,)}
 Для всіх значень A  VN, nxt3(A) = nxt2(A)

140
Моделі обчислень у програмній інженерії

3. Java
3.1. Регулярні вирази
3.1.1. Регулярні вирази Java
Тексти всіх Java-програм, наведених у розділі 3, можна
знайти в репозиторії https://github.com/ProtsenkoVS/models-
_java.git.
Регулярний вираз – це рядок, побудований за певними пра-
вилами, що задає деяку регулярну мову – множину рядків.
У Java регулярний вираз називають шаблоном рядків. А
мова, що задається регулярним виразом – це рядки, що
відповідають заданому шаблону.
Більшість символів у регулярному виразі, крім зарезервова-
них, позначають себе самі. Символи . * + ? { ( ) [ \ ^ $ заре-
зервовані.
 Для позначення такого символу його потрібно вживати
разом із символом \ («екранувати»).
 Загалом \c позначає символ c для довільного символу,
крім [A-Za-z0-9].
Можна створювати клас символів, який показує, який символ
із набору може перебувати в цій позиції:
 [abc] – довільний символ a, b або c.
 [^abc] – довільний символ, крім символів a, b або c.
 [a-z] – довільний символ між a і z, включно з a і z.
 [A-Za-z] – символ латинського алфавіту.
Є багато зумовлених (визначених) класів символів:
 \d – всі десяткові цифри, еквівалентно [0-9].
 \D – всі символи, крім десяткових цифр, еквівалентно
[^0-9].
 \s – всі «проміжки», еквівалентно [\t\n\r\f\x0B].
 \S – всі «не проміжки», еквівалентно [^ \t\n\r\f\x0B].
 \w – символи, що вживаються в ідентифікаторах, еквіва-
лентно [0-9_A-Za-z].
У регулярних виразах уживаються:

141
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Операція конкатенації – XY – після рядка з X іде ря-


док з Y.
 Операція об’єднання – X|Y – рядок з X або рядок з Y.
 Круглі дужки – (X) – просто збігається з X.
o Дужки також використовуються для виділення
груп.
Використовуються різні варіанти вказівки на кількість по-
вторень:
 X? – один раз або жодного.
 X* – нуль або більше разів.
 X+ – один або більше разів.
 X{n} – n разів.
 X{n,} – не менше n разів.
 X{n,m} – від n до m повторень.

3.1.2. Методи класу String


У класі String існує низка методів, що використовують регу-
лярні вирази:
 str.matches(RE) – повертає true, якщо весь рядок str
зіставляється з виразом RE.
 str.split(RE) – повертає масив підрядків (String []), розби-
ваючи регулярним виразом RE рядок str на підрядки.
 str.replaceFirst(RE,rs) – замінює в str ПЕРШЕ входження
виразу RE на рядок rs.
 str.replaceAll(RE,rs) – замінює в str УСІ входження ви-
разу RE на рядок rs.
Приклад 1. Задано рядки re, s1 і s2:
String re = “a+y”;
String s1 = “aay”, s2 = “caayyacaayc”;
Регулярний вираз a+y зіставляється з довільним рядком, що
складається з декількох символів a й одного символу y в кінці.
Рядок s1 відповідає регулярному виразу re (s1.matches(re) ==
true), а рядок s2 – не відповідає (s2.matches(re) == false).

142
Моделі обчислень у програмній інженерії

У результаті s1.split(re) отримуємо порожній масив [], а в ре-


зультаті s2.split(re) – масив із трьох елементів [“c”,”ycc”,”c”].
Виклик s1.replaceFind(re,”W”) повертає рядок“W”, а
s2.replaceAll(re,”W”) – рядок “cWyacWc”.

3.1.3. Класи Pattern та Matcher


Найчастіше для роботи з регулярними виразами використо-
вуються класи Pattern і Matcher із пакету java.util.reg. Класи
Pattern і Matcher не мають конструкторів, їхні екземпляри ство-
рюють статичні методи пакету java.util.reg.
За регулярним виразом – рядок reg – спочатку створюється
його представлення pat – об’єкт класу Pattern (метод compile).
Для кожного рядка input, який потрібно зіставити з регулярним
виразом reg, за представленням pat (метод matcher) будується
«пошукач» mch – об’єкт класу Matcher, методи якого й викону-
ють різні варіанти зіставлення регулярного виразу reg та вхід-
ного рядка input.
Наступний фрагмент коду демонструє типову послідовність
дій:
String reg = "[+-])?(\\d+)";
Pattern pat = Pattern.compile(reg);
String input = ".......";
Matcher mch = pat.matcher(input);
Для перевірки зіставлення всього рядка input із регулярним
виразом reg використовується метод mch.matches(), що повертає
true, якщо зіставлення успішне.
Для знаходження в рядку input усіх підрядків, що відповіда-
ють регулярному виразу reg, застовуються методи mch.find() та
mch.group(). З цією метою використовується наступний цикл:
Matcher mch = pat.matcher(input);
while (mch.find()){
String gr = mch.group();
...
}

143
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Приклад 2. Електронна адреса складається з імені ком-


п’ютера (доменне ім’я) та імені користувача (скриньки), що
з’єднуються символом @. Доменне ім’я складається з рівнів
(доменів різного рівня: першого рівня, другого рівня, …, n-
рівня), відокремлених крапками.
 Найчастіше другого або третього рівнів.
 Домен першого рівня – рядок від 2 до 4 букв латинсь-
кого алфавіту, які вказують на географічну належність
(ua – Україна, ru – Росія) або на спеціальне призначення
(com – комерційні сайти, edu – освітні сайти).
Наступні регулярні вирази описують окремі елементи елек-
тронної адреси:
 “\\w+” – ім’я скриньки – непорожня послідовність сим-
волів набору ASCII із визначеної групи \w (оскільки \ –
зарезервований символ, то його потрібно екранувати).
 “(\\w\\.)+” – непорожній список імен доменів
проміжного рівня, за кожним із яких міститься символ
крапка (зарезервований, тому екранується).
 “{[a-z]{2,4}” – ім’я домену першого рівня, складається з
маленьких букв латинського алфавіту (від 2 до 4).
Повний регулярний вираз, що задає електронну адресу (вра-
ховуючи, що доменне ім’я може складатися тільки з обмеженого
набору ASCII-символів), має вигляд:
“\\w+@(\\w\\.)+[a-z]{2,4}”
Приклади електронних адрес:
 procukvs@gmail.com
 my@distedu.ukma.edu.ua
у яких імена скриньок – procukvs і my, а доменні імена –
gmail.com і distedu.ukma.edu.ua відповідно.
public void findEmail(String wem){
String rem = "\\w+@(\\w+\\.)+[a-z]{2,4}";
Pattern pat = Pattern.compile(rem);
Matcher mch = pat.matcher(wem);

144
Моделі обчислень у програмній інженерії

while (mch.find())
System.out.println("e-mail: " + mch.group() );
}
Метод findEmail(input) шукає в рядку input усі поштові
адреси і виводить їх.

3.1.4. Робота з групами


У випадку, коли весь рядок input зіставляється з регулярним
виразом reg, шукач mch надає методи, які дозволяють виділити
частини рядка, що відповідають групам регулярного виразу.
 У регулярному виразі група позначається дужками ( і ).
 У вхідному рядку, що відповідає регулярному виразу,
група – це послідовність символів, що відповідає групі в
регулярному виразі.
 Усі знайдені групи нумеруються послідовно, почи-
наючи з 1.
 Увесь вхідний рядок – це група 0.
 Метод mch.groupCount() повертає кількість знайдених
груп.
 Метод mch.group(i) повертає рядок – i-ту групу.
У регулярному виразі разом із групою можна зафіксувати
ім’я групи ng – конструкція (?<ng>…), – яке можна надалі
використати, щоб вибрати групу за іменем mch.group(“ng”).
Наступний фрагмент коду демонструє типову послідовність
дій:
String reg = "......";
Pattern pat = Pattern.compile(reg);
String input = ".......";
Matcher mch = pat.matcher(input);
if (mch.matches()){
...
... допускається робота із групами …
...
}

145
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Приклад 3. Регулярний вираз reg = \\s*[+-]?\\d+\\s* описує


ціле число, можливо, зі знаком, на початку і в кінці якого може
міститися декілька «проміжків» (символи із групи). Якщо рядок
s відповідає виразу reg, то для перетворення цього рядка на ціле
значення необхідно виділити послідовність із 0 або 1 символу,
що задає знак числа, і послідовність десяткових чисел, що зада-
ють саме число. З цією метою у виразі можна вказати дві групи
reg = “\\s*([+-])?(\\d+)\\s*”.
Наступний метод parseInt(s) повертає ціле число, якщо рядок
s відповідає виразу reg, і null – в іншому разі. Метод toInt(s) пе-
ретворює послідовність десяткових цифр s на ціле число.
private Integer parseInt(String s){
Integer r = null;
String reg = "\\s*([+-])?(\\d+)\\s*";
Pattern ip = Pattern.compile(reg);
Matcher im = ip.matcher(s);
if (im.matches()){
r = toInt(im.group(2));
String sign = im.group(1);
if((sign!=null) && (sign.charAt(0)=='-')) r *= (-1);
}
return r;
}
private int toInt(String s){
int r =0;
for(int i=0; i<s.length(); i++) r=r*10+(s.charAt(i)- '0');
return r;
}
Приклад 4. Рядок s містить послідовність цілих чисел, мож-
ливо, зі знаком, що виокремлюються комами, і, можливо,
проміжками. Метод listInt(s) обраховує суму всіх заданих цілих
чисел, якщо рядок s синтаксично правильний, і повертає null –
в іншому разі.
public Integer listInt(String s){
Integer t=0, r = 0;
String[] sInt = s.split(",");

146
Моделі обчислень у програмній інженерії

int i = 0;
do{
t = parseInt(sInt[i++]);
if(t==null) r=null; else r +=t;
} while ((r!=null)&& (i<sInt.length));
return r;
}

3.1.5. Розпізнавання дійсного числа


Дійсне число завжди має або дробову частину, або степінь,
або й те й інше. Метод parseDouble(s) обробляє рядок s, який
містить дійсне число, повертаючи його значення, або null, якщо
рядок синтаксично неправильний.
Для спрощення розглядається регулярний вираз, що містить
три зафіксовані групи:
 Обов’язкова (?<i>[+-]?\\d+) – ціла частина.
 Можлива (?<f>\\.\\d+)? – дробова частина.
 Можлива (?<p>[Ee][+-]?\\d+)? – степінь.
Ситуація, коли відсутні й дробова частина, й степінь, і тоді
число – ціле, розпізнається за допомогою окремого зіставлення.
public Double parseDouble(String s){
Double r = null;
String reg = "(?<i>[+-]?\\d+)(?<f>\\.\\d+)?(?<p>[Ee][+-]?\\d+)?";
Pattern dp = Pattern.compile(reg);
Matcher dm = dp.matcher(s);
if (dm.matches() && !s.matches("[+-]?\\d+")){
String i = dm.group("i"); // ціла частина
String f = dm.group("f"); // можливо дробова частина
String p = dm.group("p"); // можлива степінь
Double rd=0
// обраховуємо цілу частину і перетворюючи в double
String si=i;
if(i.charAt(0)=='+' || i.charAt(0)=='-') si=i.substring(1);
for(int t=0; t<si.length(); t++) rd=rd*10+(si.charAt(t)- '0');
if(i.charAt(0)=='-') rd = -rd;
if (f!=null) { // додаємо дробову частину

147
Глибовець М. М., Кирієнко О. В., Проценко В. С.

double coef = 1;
for(int j=1; j<f.length(); j++){
coef /= 10;
rd += (f.charAt(j)-'0') * coef;
}
}
if (p!=null){ // додаємо степінь
String pi = p.substring(1);
if(p.charAt(1)=='+' || p.charAt(1)=='-') pi=p.substring(2);
double pow = 1;
int pw = 0;
for(int k=0; k<pi.length(); k++)
pw = pw*10+(pi.charAt(k)-'0');
for(int k=0; k<pw; k++) pow = pow*10;
if(p.charAt(1)=='-') rd /= pow; else rd *= pow;
}
r = rd;
}
return r;
}

3.1.6. Задачі
1. Побудуйте регулярний вираз у мові Java, що задає:
 Непорожню послідовність символів a і/або b.
 Ціле десяткове число, можливо, зі знаком + або -.
 Ідентифікатор – непорожня послідовність букв латинсь-
кого алфавіту, десяткових цифр і символу _, що почи-
нається з букви.
2. Напишіть наступні статичні методи, використовуючи лише
методи класу String:
 static boolean isFromab(String str), який перевіряє, чи ря-
док str – непорожня послідовність символів a і/або b;
 static int cntFromab(String str), який підраховує, скільки в
рядку str непорожніх підрядків, що складаються з сим-
волів a і/або b;

148
Моделі обчислень у програмній інженерії

 static String repFromab(String str), який замінює в рядку


str кожний непорожній підрядок, що складається з сим-
волів a і/або b, на символ +.
3. Напишіть наступні статичні методи, використовуючи методи
класів Pattern і Matcher:
 static boolean isFromabP(String str), який перевіряє, чи
рядок str – непорожня послідовність символів a і/або b;
 static int cntFromabP(String str), який підраховує, скільки
в рядку str непорожніх підрядків, що складаються з сим-
волів a і/або b;
 static String repFromabP(String str), який замінює в рядку
str кожний непорожній підрядок, що складається з сим-
волів a і/або b, на символ +.
4. Побудувати клас Algorithm, що реалізує модель обчислень
Нормальні алгоритми Маркова. Клас містить:
 Внутрішній клас Rule (представлення однієї підста-
новки) з полями:
o Pattern left – ліва частина підстановки;
o String rigth – права частина підстановки;
o Boolean end – ознака заключної підстановки.
 Поле ArrayList <Rule> base задає список підстановок.
 Конструктор Algorithm (String desc), який за рядком desc,
що задає список підстановок, будує список base, якщо
рядок синтаксично правильний, або покладає base
рівним null в іншому випадку.
o Рядок desc – це список підстановок, що розділя-
ються символом ʻ;ʼ. Кожна підстановка – це два
слова, ліва і права частини підстановки, між якими
міститься одне зі слів ʻ–>ʼ або ʻ–>.ʼ. Ліва і права
частини підстановки – слова, зокрема порожні, що
містять символи латинського алфавіту, десяткові
цифри та символи ʻ#ʼ і ʻ_ʼ. В довільному місці, крім
слів, дозволяється вживати проміжки.

149
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Метод String eval(String input, int cnt), що виконує алго-


ритм на слові input і повертає результат. Якщо, вико-
навши cnt підстановок, алгоритм не зупинився, то ре-
зультат – ʻundefinedʼ.
 Метод boolean isGood() повертає значення true, якщо
екземпляр класу задає коректний алгоритм (base != null),
і false – в іншому випадку (base == null).

3.2. Робота з КВ-граматиками


3.2.1. Клас Grammar
Розглядаються контекстно-вільні граматики, у яких не-
термінали задаються великими літерами латинського алфавіту:
‘A’, ‘B’, …, ‘Z’, а всі інші літери, крім ‘$’, можна використо-
вувати як термінали.
public class Production {
private Character non;
private String rull;
public Production(Character c, String sr) {
non=c; rull=sr;
}
public Character getNon() {return non;}
public String getRull() {return rull;}
public String toString(){
return non + "->" + (rull.isEmpty()?"Eps":rull);
}
}
Клас Production описує одне правило виведення. Поле non
задає нетермінал – ліву частину, а поле rull задає рядок – праву
частину правила виведення.
public class Grammar {
protected Character start;
protected Set<Character>nonterminals, terminals;
protected ArrayList<Production>product;
public Grammar(char[] left, String[] right){
product = new ArrayList<> ();
nonterminals = new TreeSet<>();

150
Моделі обчислень у програмній інженерії

terminals = new TreeSet<>();


start = null;
for(int i=0;i<left.length&&i<right.length; i++){
char n = left[i];
if(i<right.length&& Character.isUpperCase(n)){
String rull = right[i];
if (start==null) start = n;
nonterminals.add(n);
for(char c:rull.toCharArray()){
if(Character.isUpperCase(c)) nonterminals.add(c);
else terminals.add(c);
}
product.add(new Production(n,rull));
}
}
if(start==null) { // початковий нетермінал ЗАВЖДИ є!
start = 'S'; nonterminals.add('S');
}
}
public Character getStart() {return start;}
public Set<Character> getNonterminals() {return nonterminals;}
public Set<Character> getTerminals(){return terminals;}
public ArrayList<Production> getProduct() { return product;}
………..
………..
public String toString(){
String ns = Arrays.toString(nonterminals.toArray());
String ts = Arrays.toString(terminals.toArray());
String pl = Arrays.toString(product.toArray());
return"[nonterminals -> " + ns + ",\n terminals -> " + ts
+ ",\n product -> " + pl + ",\n start - " + start + "\n]";
}
}
Контекстно-вільні граматики – це екземпляри класу
Grammar, поля якого задають: start – початковий нетермінал,
nonterminals, terminals – алфавіти нетермінальних і термінальних
символів і product – список усіх правил виведення.

151
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Конструктор Grammar(char[] left, String[] right) будує екзем-


пляр класу Grammar за масивами:
 left – містить ліві часини (нетермінали) правил виве-
дення, за домовленістю, нетермінал першого з них –
початковий.
 right – містить праві частини (рядки) правил виведення
граматики.
Якщо елемент масиву left[i] не є великою латинською літе-
рою (Character.isUpperCase(left[i])), то правило ігнорується.
Приклад 1. Для створення граматики G = (VN, VT, P, S), де
VN = {S, T, F}, VT = {+, *, (, ), d}, P = {S –>S+T, S –>T, T –>T*F, T
–>F, F –>(S), F –>d} потрібно виконати:
char[] l = {'S','S','S','T','T'};
String[] r ={"S+T","S-T", "T","(S)","d"};
Grammar g = new Grammar(l,r);

3.2.2. Вивідність
Оскільки всі правила виведення граматики – це список
ArrayList<Production> product, то кожне правило виведення має
унікальний номер 0 ≤ i < product.size(). Довільне лівостороннє
виведення S =>α1=> α2=> … => αn=α деякої сентенціальної
форми α, у якому на кожному кроці 0 ≤ i ≤ n застосовується ji
правило виведення (0 ≤ ji<product.size()), можна задати масивом
ArrayList<Integer>dv, у якогоdv.size()=n та dv.get(i-1) = ji для
довільного 0 ≤ i<n.
public boolean leftDerivation(ArrayList<Integer>dv){
boolean r=true;
Production pr;
ArrayDeque<Character>sb = new ArrayDeque<>();
int i=0;
sb.push(start);
while(r&&i<dv.size() ){
r = (dv.get(i)>=0 &&dv.get(i) <product.size() && !sb.isEmpty());
if(r){
pr=product.get(dv.get(i++));
r = pr.getNon().equals(sb.peek());

152
Моделі обчислень у програмній інженерії

if(r){
sb.pop();
for(int j=pr.getRull().length(); j>0; j--){
Character c = pr.getRull().charAt(j-1);
if (Character.isUpperCase(c))sb.push(c);
}
}
}
}
return r;
}
Метод leftDerivation(dv) перевіряє, чи масив dv задає деяке
лівостороннє виведення в заданій граматиці (екземпляр об’єкта
Grammar). Метод використовує стек sb, що містить усі не-
термінальні символи, які з’являються в процесі цього лівосто-
роннього виведення в оберненому порядку.
 На початку стек містить початковий нетермінал start.
 Для кожного кроку виведення 0 ≤ i<dv.size() верхній еле-
мент стека – це лівий нетермінал правила виведення, що
застосовується на цьому кроці pr.getNon().equals-
(sb.peek()), який необхідно замінити на всі нетермінали,
що трапляються у правій частині правила виведення в
оберненому порядку.

3.2.3. Синтаксичне дерево


Наступний клас SynTree задає синтаксичне дерево. Кожне
дерево має мітку кореня root і список нащадків sons (синтаксичні
дерева).
public class SynTree {
private Character root;
private List<SynTree>sons;
public SynTree(){
root=null; sons=null;
}
public SynTree(charc){

153
Глибовець М. М., Кирієнко О. В., Проценко В. С.

root=c; sons=null;
}
public void addSon(SynTree s){
if (sons==null) sons = new ArrayList<SynTree> ();
sons.add(s);
}
public Character getRoot() {return root;}
publicList<SynTree> getSons() {return sons;}
public String toString() {
if (sons==null)return(root==null?"Eps":String.valueOf(root));
StringBuilder buf = new StringBuilder();
buf.append('('); buf.append(root);
for (int i=0; i<sons.size(); i++){
buf.append(' ');
buf.append(sons.get(i).toString());
}
buf.append(')');
return buf.toString();
}
}
Конструктор SynTree() будує дерево-листок, у якого обидва
поля root i sons дорівнюють null. Це синтаксичне дерево, що
відповідає листку, поміченому символом ε (порожній рядок).
Воно утворюється після застосування правила виведення A –>ε.
Конструктор SynTree(char c) будує дерево-листок, корінь
якого містить символ с (термінал або нетермінал).
Додати синтаксичне дерево-нащадок e можна за допомогою
методу addSon (SynTreee).
public SynTree buildSynTree(ArrayList<Integer>dv){
SynTree tr = null;
if(leftDerivation(dv)){
tr = new SynTree(start);
buildInDepth(tr,0,dv);
}
return tr;
}

154
Моделі обчислень у програмній інженерії

private int buildInDepth(SynTree t, int k, ArrayList<Integer>dv){


if(Character.isUpperCase(t.getRoot()) &&k<dv.size()) {
String rull = product.get(dv.get(k++)).getRull();
if(!rull.isEmpty())
for(int j=0; j<rull.length(); j++){
SynTree tr1 = new SynTree(rull.charAt(j));
t.addSon(tr1);
k = buildInDepth(tr1, k, dv);
}
else t.addSon(new SynTree());
}
return k;
}
Якщо список ArrayList<Integer>dv задає деяке лівостороннє
виведення, leftDerivation(dv) = true, то метод buildSynTree(dv)
будує синтаксичне дерево, що відповідає цьому лівосторонньому
виведенню.

3.2.4. Перетворення граматик


Якщо граматика має ліворекурсивні нетермінали, то метод
findLeft() повертає один із них, в іншому разі – null.
Предикат leftRecursion() перевіряє, чи має граматика ліворе-
курсивні правила виведення.
Метод removeLeft() перетворює граматику, замінюючи всі
ліворекурсивні правила виведення еквівалентною множиною
правил виведення, що не є ліворекурсивними. Використовується
метод newNonterminal(), який генерує новий нетермінальний
символ.
private Character findLeft(){
for(Production pr:product){
if(!pr.getRull().isEmpty()&&
(char)pr.getNon()==pr.getRull().charAt(0))
return pr.getNon()
}
return null;
}

155
Глибовець М. М., Кирієнко О. В., Проценко В. С.

public boolean leftRecursion(){ return (findLeft()!=null);}

public void removeLeft(){


// A->As1 ... A->Asn ----> N->s1N ... N->snN N->Eps
// A->b1 ... A->bm ----> A->b1N ... A->bmN
Character ln=null;
do{ln=findLeft();
if(ln!=null){
char nn = newNonterminal();
for(int i=0;i<product.size();i++){
Production pr = product.get(i);
if(pr.getNon().equals(ln)){
if((char)pr.getNon() == pr.getRull().charAt(0))
pr=new
Production(nn,pr.getRull().substring(1)+nn);
else pr=new Production(pr.getNon(), pr.getRull()+nn);
product.set(i,pr);
}
}
product.add(new Production(nn,""));
}
} while (ln!=null);
}

private char newNonterminal(){


char r = 'A';
while(nonterminals.contains(r)) r++;
nonterminals.add(r);
return r;
}

3.3. Робота з LL(1)-граматиками


3.3.1. LL(1)-граматики
Клас GrammarLL1 розширює клас Grammar, надаючи методи
для роботи з LL(1)-граматиками.
public class GrammarLL1 extends Grammar {
private Map<String,Set<Integer>>terrors;

156
Моделі обчислень у програмній інженерії

private Map<String,Integer>test;
private Map<Character,Set<Character>>fst, nxt;

public GrammarLL1(char[] left, String[] right) {super(left,right);}


public boolean isLL1(){
if (leftRecursion())return false;
buildFst(); buildNxt();
terrors = new TreeMap<String,Set<Integer>>();
test = new TreeMap<String,Integer>();
for(int i=0; i<product.size();i++){
Production pr = product.get(i);
Character n = pr.getNon();
String rull = pr.getRull();
Set<Character>fs = first(rull);
Set<Character>ns = follow(n);
for(Character c:fs){
if(c=='$') for(Character s:ns) add(n,s,i);
else add(n,c,i);
}
}
return terrors.isEmpty();
}
private void add(Character N, Character t, int i){
String key = ""+N +t;
if(test.containsKey(key)){
int pr = test.get(key);
if (pr!=i){
addError(key,i);
if(pr>=0){
addError(key,pr);
test.put(key, -1); // not LL(1) !!!!!!
}
}
} else test.put(key, i);
}
private void addError(String key, int r){
Set<Integer>si;
if(terrors.containsKey(key)) si=terrors.get(key);

157
Глибовець М. М., Кирієнко О. В., Проценко В. С.

else si = new TreeSet<Integer>();

si.add(r);
terrors.put(key, si);
}
…….
}
Предикат isLL1()перевіряє, чи є граматика LL(1)-граматикою.
Основна частина методу isLL1() зводиться до побудови двох
відображень (таблиць):
 Map<String, Set<Integer>> terrors;
 Map<String, Integer> test;
Ключами кожного відображення є рядки “Nt”, де N – не-
термінал і t – термінал граматики.
Якщо граматика є LL(1)-граматикою, то відображення
terrors – порожнє, а відображення test – це керівна таблиця
LL(1)-аналізатора.
 Якщо в граматиці існують правила A –>β (із номером k)
та A –>γ (із номером j) такі, що S*=lm>wAα і t  first(βα)
 first(α), то відображення terrors має ключ “At” і k, j
terrors.get(“At”), а test.get(“At”)== -1.
Для цього перебирають усі правила виведення pr =
product.get(i) граматики, і для нетермінала n = pr.getNon(), із
використанням функцій first і follow, визначаються всі терміна-
льні символи c, що можуть з’явитися першими в лівосторон-
ньому виведенні, який використовує правило виведення pr.
Метод add(N,t,i) зв’язує із ключем key = “”+N+t правило ви-
ведення i та робить спробу додати цю пару у відображення test.
Якщо в цьому відображенні із ключем key уже зв’язане інше
значення j>=0 або -1, то граматика не являє собою LL(1)-грама-
тику. В останньому випадку ключ key додається у відображення
terrors, а у відображенні test із ключем key зв’язується значе-
ння -1.

158
Моделі обчислень у програмній інженерії

3.3.2. LL(1)-аналіз
Метод analys(input) визначає, чи належить слово input мові,
що задається граматикою. Якщо слово input належить мові, то
повертається його лівостороннє виведення ArrayList<Integer>
dv != null, а в іншому разі – null.
public ArrayList<Integer> analys(String input){
ArrayList<Integer>dv = new ArrayList<> ();
String wd = input + '$';
String stack = "" + start + '$';
while(stack.charAt(0)!='$'){
Character r = stack.charAt(0);
stack = stack.substring(1);
if(nonterminals.contains(r)){
String key = ""+r+wd.charAt(0);
if(test.containsKey(key)){
Integer j = test.get(key);
dv.add(j);
stack = product.get(j).getRull() + stack;
} else return null;
} else
if(r.equals(wd.charAt(0)))wd=wd.substring(1); else return null;
}
if (wd.charAt(0)!='$') return null;
return dv;
}
Метод просто моделює роботу LL(1)-аналізатора, використо-
вуючи відображення test як керівну таблицю σ. Компоненти
конфігурації відповідно реалізують змінні:
 String wd – вхід y – нерозпізнана частина слова input;
 String stack – магазин u;
 ArrayList<Integer>dv – виведення w – список номерів
правил виведення.

3.3.3. Побудова fst і nxt


VT {$} VT {$}
Функції fst: V –> 2 і nxt: VN –> 2 реалізують
відображення

159
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Map <Character,Set<Character>> fst, nst;


Ці відображення будують методи buildFst() і buildNxt()
відповідно.
Методи практично покроково реалізують побудову функцій,
описану раніше. Попередні наближення відображень зберіга-
ються в локальних змінних fstp i nxtp відповідно. На початку
кожної ітерації необхідно виконати повне копіювання (deepcopy)
об’єкта fst (nst) на об’єкт fstp (nxtp).
private void buildFst(){
Map<Character,Set<Character>>fstp;
fstp = new TreeMap<>();
fst = new TreeMap<>();
// initial
for(Character c: terminals){
fstp.put(c, new TreeSet<>());
TreeSet sc = new TreeSet<>(); sc.add(c);
fst.put(c, sc);
}
for(Character c: nonterminals){
fstp.put(c, new TreeSet<>());
fst.put(c, new TreeSet<>());
}
for(Production pr:product){
if(pr.getRull().isEmpty()){
Set ns = fst.get(pr.getNon());
ns.add('$'); fst.put(pr.getNon(),ns);
}
}
// iteration
while (!fstp.equals(fst)){
// deep copy
fstp = new TreeMap<>();
for(Character c:fst.keySet()) fstp.put(c, new TreeSet(fst.get(c)));
for(Production pr:product){
Character n = pr.getNon();
Set<Character>ns = fst.get(n);
String rull = pr.getRull();

160
Моделі обчислень у програмній інженерії

Boolean go = true;
int i=0;
while (i<rull.length() &&go){
Set<Character>cs = fstp.get((Character)rull.charAt(i));
go = cs.contains('$');
addWithoutEps(ns,cs);
i++;
}
if (i==rull.length() &&go) ns.add('$');
fst.put(n,ns);
}
}
}

private void buildNxt(){


Map<Character,Set<Character>>nxtp;
nxtp = new TreeMap<>();
nxt = new TreeMap<>();
// initial
for(Character c: nonterminals){
nxtp.put(c, new TreeSet<>());
Set<Character>ts = new TreeSet<>();
if (c.equals(start)) ts.add('$');
nxt.put(c, ts);
}
// iteration
while (!nxtp.equals(nxt)){
// deep copy
nxtp = new TreeMap<>();
for(Character c:nxt.keySet()) nxtp.put(c, new TreeSet(nxt.get(c)));
for(Production pr:product){
Character n = pr.getNon();
String rull = pr.getRull();
int i=0;
while (i<rull.length()){
Character nt = (Character)rull.charAt(i);
if (nonterminals.contains(nt)){
Set<Character>cs = nxt.get(nt);

161
Глибовець М. М., Кирієнко О. В., Проценко В. С.

if(i<rull.length()-1){
Set ntc = fst.get((Character)rull.charAt(i+1));
addWithoutEps(cs,ntc);
Boolean go = ntc.contains('$');
int j=i+2;
while(j<rull.length() &&go){
ntc = fst.get((Character)rull.charAt(j));
addWithoutEps(cs,ntc);
go = ntc.contains('$');
j++;
}
if(go)cs.addAll(nxtp.get(n));
}
else cs.addAll(nxtp.get(n));
}
i++;
}
}
}
}

private Set<Character> addWithoutEps(Set<Character>bs,


Set<Character>as){
for(Character c:as)
if(!c.equals('$')) bs.add(c);
return bs;
}

3.3.4. Реалізація first і follow


Відображення fst і nxt використовуються в методах first(wd)
VT {$}
i follow(n), які реалізують функції first: V* –> 2 і follow: VN
VT {$}
–> 2 відповідно.
public Set<Character> first(String wd){
Set<Character>rs = new TreeSet<>();
if (!wd.isEmpty()){
Character c = wd.charAt(0);
if(wd.length()>=1){

162
Моделі обчислень у програмній інженерії

if(fst.get(c).contains('$')){
addWithoutEps(rs,fst.get(c));
rs.addAll(first(wd.substring(1)));
} else rs = fst.get(c);
}
else rs = fst.get(c);
}
else rs.add('$');
return rs;
}

public Set<Character> follow(Character c){


Set<Character>rs = new TreeSet<>();
if(nonterminals.contains(c)) rs = nxt.get(c);
return rs;
}

3.4. Рекурсивний спуск


3.4.1. Метод рекурсивного спуску
Завдання синтаксичного аналізу – для заданої G = (VN, VT,
P, S) КВ-граматики й слова w VT* визначити, чи w  L(G). У
задачі граматика G фіксується, а слово w – змінюється, тому для
розв’язку задачі синтаксичного аналізу за граматикою G необ-
хідно побудувати відповідну програму-аналізатор.
З цією метою на практиці використовується метод рекурсив-
ного спуску (recursive descent), який реалізує синтаксичний ана-
ліз згори вниз, намагаючись побудувати для вхідного слова w
його лівостороннє виведення у граматиці G.
На кожному кроці синтаксичного аналізу аналізатор, побуду-
вавши частину лівостороннього виведення S=rm>...=rm>xAγ й
розпізнавши частину вхідного слова w=xav, повинен із декількох
альтернативних правил виведення A –>α1, …, A –>αk граматики
G обрати те A –>αi, застосування якого зможе вивести частину
слова w, що залишилася: xAγ =rm> xαiγ *=rm> w=xav.
Цей метод називається прогнозним (predictive parser), тому
що вибір альтернативного правила виведення спирається на про-

163
Глибовець М. М., Кирієнко О. В., Проценко В. С.

гноз, чи виведе використання цього правила частину вхідного


слова w, що залишилося, – av, і перш за все термінал a.
Використовуючи метод рекурсивного спуску, за граматикою
G будується клас-аналізатор ParserG, який для кожного нетермі-
нала A містить метод, що розпізнає всі сентенційні форми грама-
тики G, що можна вивести з нетермінала A:
voidA() {…..}.
Тіло методу визначають правила виведення граматики, ліва
частина яких – нетермінал A. Якщо A –>α1, …, A –>αk, то тіло
методу має вигляд:
if (“прогнозується альтернатива 1”) {“аналіз α1”}
else if (“прогнозується альтернатива 2”) {“аналіз α2”}
…………………………
else if (“прогнозується альтернатива k”) {“аналіз αk”}
else “генерується виключення”.
Аналіз кожної альтернативи перетворюється на просту по-
слідовність викликів методів, що реалізують (аналізують) нетер-
мінали граматики й методу match(t), який розпізнає наступний
термінальний символ t.
Наприклад, якщо αi = aBCdE, де a, d – термінальні й B, C,
E – нетермінальні символи, то “аналіз αi” має вигляд:
match(‘a’); B(); C(); match(‘d’); E();
 match(t) – метод, що порівнює наступний необроблений
символ вхідного слова з терміналом-символом t.
o Якщо вони збігаються, то вводиться наступний
необроблений символ вхідного слова й аналіз
продовжується.
o Якщо вони не збігаються, то аналіз завер-
шується (знайдено синтаксичну помилку) й ге-
нерується відповідне виключення.
 B(), C(), E() – просто виклики методів, що відповідають
нетерміналам B, C, E.
Приклад 1. Нехай G = (VN, VT, P, S), де VN = {S, A}, VT =
= {a, b}, P = {S –>aSbAa, S –>b, A –>baAS, A –>a}. Граматика

164
Моделі обчислень у програмній інженерії

має прості правила виведення й допускає ефективний


синтаксичний аналіз. Далі наводиться клас-аналізатор ParserG,
що реалізує синтаксичний аналіз граматики G методом
рекурсивного спуску, та класи SyntaxError і Letter, які він
використовує.
public class ParserG {
Letter input;
char next;

public ParserG(){}
public boolean analys(String word){
input = new Letter(word);
try{
next = input.nextChar();
S(); match('$');
} catch(SyntaxError ex){
System.out.println(ex.getMessage());
return false;
}
return true;
}

void S() throws SyntaxError{


if(next=='a'){
next=input.nextChar(); //match(‘a’);
S();match('b'); A();
} else match('b');
}
void A() throws SyntaxError{
if(next=='b'){
next=input.nextChar();//match(‘b’);
match('a'); A(); S();
}else match('a');
}
void match(char c) throws SyntaxError{
if(next==c) next=input.nextChar();
else throw new SyntaxError("Expecting " + c + ", found " + next);
}

165
Глибовець М. М., Кирієнко О. В., Проценко В. С.

}
public class SyntaxError extends Exception {
public SyntaxError(String msg) { super(msg);}
}

public class Letter {


String input;// $ - end of string
int p;
char c;

public Letter(String input){


this.input=input; p=0; c=' ';
}
public char nextChar() {
if (p<input.length()) c=input.charAt(p++); else c='$';
return c;
}
}
Оскільки в разі знайдення синтаксичної помилки синтаксич-
ний аналіз завершується, то для фіксації такої події використову-
ється виключення SyntaxError, що перевіряється, конструктор
якого має параметр – символьний рядок із повідомленням про
синтаксичну помилку.
Призначення класу Letter – створити на основі вхідного ря-
дка input, який необхідно проаналізувати, потік символів, дода-
ючи в кінець вхідного слова спеціальний термінал – символ ‘$’,
що позначає кінець вхідного слова. Цей клас має єдиний метод
nextChar(), який під час кожного виклику повертає новий
(наступний) символ вхідного слова, починаючи з першого.
Синтаксичний аналіз граматики G реалізує клас-аналізатор
ParserG. Клас містить змінні:
 input – екземпляр класу Letter, який реалізує потік
символів, що утворюють вхідне слово, яке необхідно
проаналізувати;
 next – наступний, ще не проаналізований символ вхід-
ного слова.

166
Моделі обчислень у програмній інженерії

Аналіз вхідного слова word виконує метод analys(word),


який повертає значення:
 true – якщо слово word належить мові L(G);
 false – якщо слово word НЕ належить мові L(G).
Водночас виводиться повідомлення про відповідну
синтаксичну помилку.
Цей метод створює екземпляр класу Letter, уводить перший
нерозпізнаний символ вхідного слова next=input.nextChar() і
викликає метод S(), що розпізнає всі сентенційні фрази грама-
тики G, зокрема слово word, якщо word  L(G). Після завершення
перевіряється, чи слово розпізнане повністю match(‘$’).
Якщо в процесі аналізу знайдено помилку, то генерується
виключення SyntaxError, яке перехоплюється в цьому методі,
повертаючи false.
Перед викликом методу, що відповідає довільному не-
терміналу, у змінну next потрібно ввести наступний нерозпізна-
ний символ.
У деяких випадках, коли відомо, що наступний нерозпізна-
ний символ збігається із символом t, замість match(t) ефектив-
ніше вживати next=input.nextChar().

3.4.2. Прості арифметичні вирази


Простий арифметичний вираз будується з операцій та
круглих дужок, а його операндами є однорозрядні цілі числа, які
задаються десятковими цифрами. Наступні рядки містять прості
арифметичні числа: “5”, “5-7*3+9”, “3+(9-5)%3”, “(6)”. Прості
арифметичні вирази не містять символів проміжку. На початку
перед першим доданком простий арифметичний вираз може
мати знак + або -. Тобто наступні вирази синтаксично правильні:
“-5”, “+4*3/(+2-1)”, “-(1+7)*3”.
Наступна граматика G1 = (VN, VT, P1, S), де VN = {S, E, T, F},
VT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, +, -, *, /, %, (, )} і P1 = {S–>+E, S–>-
E, S–>E, E–>E+T, E–>E-T, E–>T, T–>T*F, T–>T%F, T–>T/F,
T–>F, F->0, F–>1, F->2, F–>3, F–>4, F–>5, F–>6, F–>7, F–>8,

167
Глибовець М. М., Кирієнко О. В., Проценко В. С.

F–>9, F–>(S)} задає всі прості арифметичні вирази. На жаль, ця


граматика має ліворекурсивні правила виведення, тому не може
бути застосована для методу рекурсивного спуску.
Використовуючи стандартний метод вилучення ліворекур-
сивних правил виведення (див. Властивості КВ-граматик),
отримуємо наступну граматику із двома додатковими не-
терміналами A і B, G2 = (VN, VT, P2, S), де VN = {S, E, T, F, A, B},
VT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, +, -, *, /, %, (, )} і P2 = {S–>+E, S–>
-E, S–>E, E–>TA, A–>+TA, A–>-TA, A–>ε, T–>FB, B–>%FB,
B–>/FB, B–>ε, F–>0, F–>1, F–>2, F–>3, F–>4, F–>5, F–>6,
F–>7, F–>8, F–>9, F–>(S)}. Ця граматика допускає ефективний
(безперебірний) синтаксичний аналіз методом рекурсивного
спуску, насправді ця граматика є LL(1)-граматикою (див. LL(1)-
граматика). Клас-аналізатор ParserExpr, що виконує її син-
таксичний аналіз, має вигляд:
public class ParserExpr {
Letter input;
char next;
public ParserExpr(){}

public boolean analys(String word){


input = new Letter(word);
try{
next = input.nextChar();
S(); match('$');
} catch(SyntaxError ex){
System.out.println("----Syntax ERROR: " + ex.getMessage());
return false;
}
return true;
}
void S() throws SyntaxError{
if(next=='+'){
next=input.nextChar(); E();
} else if(next=='-'){
next=input.nextChar(); E();
}

168
Моделі обчислень у програмній інженерії

else E();
}
void E() throws SyntaxError{
T(); A();
}
void A() throws SyntaxError{
if(next=='+'){
next=input.nextChar(); T(); A();
} else if(next=='-'){
next=input.nextChar(); T(); A();
}
}
void T() throws SyntaxError{
F(); B();
}
void B() throws SyntaxError{
if(next=='*'){
next=input.nextChar(); F(); B();
} else if(next=='/'){
next=input.nextChar(); F(); B();
} else if(next=='%'){
next=input.nextChar(); F(); B();
}
}
void F() throws SyntaxError{
if (next=='(' ){
next=input.nextChar(); S(); match(')');
} else if(next<='9'&&next>='0')
next=input.nextChar();
else throw new SyntaxError(
"Expecting one from \"0123456789(\", found " + next);
}
void match(char c) throws SyntaxError{
if(next==c) next=input.nextChar();
else throw new SyntaxError("Expecting " + c + ", found " + next);
}
}

169
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Наведений приклад показує, що використання методу рекур-


сивного спуску у випадку граматик, що описують фрагменти
реальних мов програмування, має низку недоліків:
 Дуже велика кількість нетерміналів (див. у Граматики
G2), що призводить до великої кількості методів, які ре-
алізують синтаксичний аналіз. Хоча самі методи мають
просту структуру.
 Через велику кількість нетерміналів і правил виведення
складно перевірити, чи граматика допускає ефективний
синтаксичний аналіз методом рекурсивного спуску. До-
сить перевірити, що граматика є LL(1)-граматикою (див.
LL(1)-граматики), але навіть для наведеного прикладу
зробити це без відповідних програмних засобів важко.

3.4.3. Ітераційні форми та синтаксичні діаграми


На практиці під час реалізації синтаксичного аналізу викори-
стовуються компактніші форми опису синтаксису: ітераційні
форми та синтаксичні діаграми.
В ітераційній формі для кожного нетермінала граматики вво-
диться лише одне правило виведення, але його права частина має
вигляд регулярного виразу, який, по суті, задає нескінченну
множину традиційних правил виведення. Найчастіше в регуляр-
ному виразі використовують операції: об’єднання X|Y (взяти X
або Y), ітерації {X}* (взяти X нуль або багато разів), вибору [X]
(взяти X нуль або один раз). Кожний термінальний символ у
регулярному виразі вживається в лапках ‘’.
Найпростіша граматика в ітераційній формі отримується
об’єднанням усіх правих частин правил виведення, зв’язаних з
одним нетерміналом. Із граматики G1 отримуємо граматику G3 =
= (VN, VT, P3, S), де VN = {S, E, T, F}, VT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+, -, *, /, %, (, )} і P3 = {S–> ’+’E | ‘-‘E | E, E–>E’+’T | E’-‘T | T,
T–> T’*’F | T’%’F|T’/’F | F, F–>’0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ |
‘7’ | ‘8’ | ‘9’ | ‘(‘S’)’}.

170
Моделі обчислень у програмній інженерії

Використовуючи ітерацію, легко вилучити ліворекурсивність


і отримати граматику G4 = (VN, VT, P4, S), де VN = {S, E, T, F},
VT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, +, -, *, /, %, (, )} і P4 = {S–> ’+’E | ‘-
‘E | E, E–>T{’+’T |’-‘T}*, T–>F {’*’F | ’%’F|’/’F}*, F–>’0’ | ‘1’ |
‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’ | ‘(‘S’)’}.
Групуючи спільні частини, отримуємо заключну граматику в
ітераційній формі G5 = (VN, VT, P5, S), де VN = {S, E, T, F}, VT = {0,
1, 2, 3, 4, 5, 6, 7, 8, 9, +, -, *, /, %, (, )} і P5 = {S–>[’+ | ‘-‘]E,
E–>T{(’+’ |’-‘)T}*, T–>F {(’*’ | ’%’ |’/’)F}*, F–>’0’ | ‘1’ | ‘2’ | ‘3’
| ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’ | ‘(‘S’)’}.
Для того щоб покращити зорове сприйняття й полегшити ро-
зуміння складних синтаксичних конструкцій, їх представляють у
вигляді синтаксичних діаграм.
Правила побудови синтаксичних діаграм за граматиками в
ітераційній формі:
 Кожному правилу відповідає одна синтаксична діаграма.
 Поява термінала ‘a’ у правилі відображається на діаграмі
дугою, що помічається цим символом a в колі (мал. 1).
 Поява не-
термінала A
у правилі
відображається
на діаграмі
дугою, що
помічається
цим нетерміналом A в
прямокутнику (мал. 2).
 Частина правила виведення
α1α2…αn відображається на
діаграмі малюнком, де <α1>,
<α2>, …, <αn> – малюнки, що
відображають, відповідно, α1,
α2, …, αn (мал. 3).

171
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Частина правила виведення α1|α2|…|αn відображається на


діаграмі малюнком, де <α1>, <α2>, …, <αn> – малюнки,
що відображають, відповідно, α1, α2, …, αn (мал. 4).

172
Моделі обчислень у програмній інженерії

 Частина правила виведення ітерації {α}* відображається


на діаграмі малюнком, де <α> – малюнок, що відобра-
жає α (мал. 5).
 Частина правила виведення [α] відображається на
діаграмі малюнком, де <α> – малюнок, що відображає α
(мал. 6).
Використовуючи ці правила, за граматикою G5 в ітераційній
формі можна побудувати синтаксичні діаграми (мал. 7–10).
Щоб скоротити кількість діаграм, їх об’єднують, замінюючи
нетермінали, що входять у діаграму, побудованими для них
діаграмами. Наприклад, можна об’єднати діаграми для не-
терміналів S (мал. 7) і E (мал. 8), вставляючи діаграму для не-
термінала E замість прямокутника з нетерміналом E і отримуючи
діаграму (мал. 11).
Метод рекурсивного спуску з незначними модифікаціями
можна застосувати як для граматик в ітераційній формі, так і для
синтаксичних діаграм.
Ще одна перевага синтаксичних діаграм: за ними значно
простіше визначити, чи допускають вони ефективний (безпе-
ребірний) синтаксичний аналіз методом рекурсивного спуску.

public class ParserIter {


Letter input;
char next;
public ParserIter(){ }
public boolean analys(String word){
input = new Letter(word);
try{ next = input.nextChar();
S(); match('$');
} catch(SyntaxError ex){
System.out.println("----Syntax ERROR: " +
ex.getMessage());
return false;
}
return true;
}

173
Глибовець М. М., Кирієнко О. В., Проценко В. С.

void S() throws SyntaxError{


if(next=='+' || next=='-') next=input.nextChar();
T();
while (next=='+' || next=='-'){
next=input.nextChar(); T();
}
}
void T() throws SyntaxError{
F();
while (next=='*' || next=='/' || next=='%'){
next=input.nextChar(); F();
}
}
void F() throws SyntaxError{
if (next=='(' ){
next=input.nextChar(); S(); match(')');
}
else if(next<='9'&&next>='0') next=input.nextChar();
else throw new SyntaxError(
"Expecting one from \"0123456789(\", found " + next);
}
void match(char c) throws SyntaxError{
if(next==c) next=input.nextChar();
else throw new SyntaxError("Expecting " + c + ", found " + next);
}
}
Клас-аналізатор ParserIter, що розпізнає прості арифметичні
вирази, побудований на основі синтаксичних діаграм (мал. 11,
мал. 9 і мал. 10).

3.4.4. Регулярні вирази


Розглянемо регулярні мови над набором маленьких літер ла-
тинського алфавіту a, b, c, …, x, y, z. Регулярні мови задаються
наступними регулярними виразами (деякі рядки слів в алфавіті
{a,b,c,…,x,y,z,|,(,),*,+,? }):
 Довільна маленька літера латинського алфавіту a задає
мову, що складається з одного слова – {“a”}.

174
Моделі обчислень у програмній інженерії

 Якщо p і q – регулярні вирази, що задають мови PQ, то:


o pq – регулярний вираз, що задає мову PQ;
o p|q – регулярний вираз, що задає мову P  Q.
 Якщо p – регулярний вираз, що задає мову P, то:
o p* – регулярний вираз, що задає мову P*;
o p+ – регулярний вираз, що задає мову P* \ {“”};
o p? – регулярний вираз, що задає мову P  {“”};
o (p) – регулярний вираз, що задає мову P.
Множина всіх регулярних виразів – це мова, що породжу-
ється наступною граматикою в ітераційній формі G6 = (VN, VT,
P6, S), де VN = {E, T, F, P}, VT = {a, b, c, …, x, y, z, |,
(, ), +, *, ?} і P6 = {E–>T{‘|’T}, T–>F{F}, F–>P{‘*’|’+’|’?’},
P–>’(‘E’)’|’a’|’b’|’c’|…|’x’|’y’|’z’}.

public class ParserRegul {


Letter input;
char next;
public ParserRegul(){ }

public boolean analys(String word){


input = new Letter(word);
try{
next = input.nextChar();
E(); match('$');
} catch(SyntaxError ex){
System.out.println("----Syntax ERROR: " + ex.getMessage());
return false;
}
return true;
}

void E() throws SyntaxError{


T();
while(next=='|'){
next=input.nextChar(); T(); }
}

175
Глибовець М. М., Кирієнко О. В., Проценко В. С.

void T() throws SyntaxError{


F();
while(next=='(' || next>='a'&&next<='z') F();
}
void F() throws SyntaxError{
P();
while (next=='*' || next=='+' || next=='?') next=input.nextChar();
}
void P() throws SyntaxError{
if (next=='(' ){
next=input.nextChar(); E(); match(')');
} else if(next>='a'&&next<='z') next=input.nextChar();
else throw new
SyntaxError("Expecting one from \"abc...xyz(\", found " +
next);
}
void match(charc) throws SyntaxError{
if(next==c) next=input.nextChar();
else throw new SyntaxError("Expecting " + c + ", found " + next);
}
}
Клас-аналізатор ParserRegul, що розпізнає методом рекур-
сивного спуску синтаксично правильні регулярні вирази, побу-
дований на основі граматики G6 в ітераційній формі.

3.4.5. Задачі
1. Мова містить усі слова, що мають баланс круглих дужок,
що відкривають і закривають. Тобто кожне слово мови
або порожнє, або будь-яка пара дужок розділяє його на
три частини α, β, γ: α’(‘β’)’γ, і кожна з частин α, β, γ,
своєю чергою, має баланс дужок. Алфавіт мови T = {‘(‘,
’)’}. КВ граматика, що задає цю мову G = ({S}, {(, )}, {S
–> (S)S, S –> ε}, S). Побудувати клас Balance, який
використовує рекурсивний спуск і має відкриті методи:
 boolean analys(String word), що реалізує синтаксич-
ний аналіз мови;

176
Моделі обчислень у програмній інженерії

 int count(String word), що повертає кількість дужок у


слові word, якщо слово word належить мові, і -1 – в
іншому випадку;
 int depth(String word), що повертає найбільшу гли-
бину вкладеності дужок у слові word, якщо слово
word належить мові, і -1 – в іншому випадку.
2. Розглядаються арифметичні вирази, які можна скласти з
цифр 1 і 2, круглих дужок і бінарних операцій ‘+’ та ‘*’.
Побудувати клас Simple, що використовує рекурсивний
спуск і має відкриті методи:
 boolean analys(String word), який перевіряє, чи рядок
word містить синтаксично правильний арифметич-
ний вираз;
 Integer evalLeft(String word), який повертає ціле зна-
чення виразу, якщо рядок містить синтаксично пра-
вильний арифметичний вираз, або null – в іншому
випадку. Припускається, що всі операції лівоасоціа-
тивні та мають однаковий пріоритет;
 Integer evalRigth(String word), який повертає ціле
значення виразу, якщо рядок містить синтаксично
правильний арифметичний вираз, або null – в
іншому випадку. Припускається, що всі операції
правоасоціативні та мають однаковий пріоритет.
 Integer evalPrior(String word), який повертає ціле
значення виразу, якщо рядок містить синтаксично
правильний арифметичний вираз, або null – в
іншому випадку. Припускається, що всі операції
лівоасоціативні, пріоритет операції + менший, ніж
пріоритет операції *.

3.5. Аналіз мов


3.5.1. Лексичний аналіз
Аналіз мов програмування або їх фрагментів має низку особ-
ливостей. Порівняємо прості арифметичні вирази та звичайні

177
Глибовець М. М., Кирієнко О. В., Проценко В. С.

арифметичні вирази, операнди яких – цілі числа і які можуть


використовувати різні види проміжків (‘ ‘, ‘\t’,’\n’ тощо) для
покращення зовнішнього вигляду. Наприклад: “17 -241*(5 - 20)”,
“\t + 32 \n * -41”.
У такому виразі окремі десяткові цифри не мають семантич-
ного значення, а з’являється воно лише в послідовності цифр, які
утворюють одне десяткове число. Інші символи (проміжки) вза-
галі не впливають на результат аналізу, якщо вони не ділять
однієї послідовності десяткових чисел на два числа, тож повинні
бути проігноровані.
З погляду синтаксичного аналізу мови програмування важ-
ливим є не окремий символ, а мінімальна послідовність сим-
волів, що мають певну семантику (сенс), необхідну для подаль-
шої обробки. Такі послідовності називаються лексемами, а про-
цес їх формування – лексичним аналізом.
Кожна лексема (клас Token) має тип type (ціле) й значення
String (рядок). Наприклад, ціле число 276 має тип NUMB і зна-
чення “276”, символ операції * має тип MULOP (операція з пріо-
ритетом множення) і значення “*”. Для виведення типу лексеми
використовуються типи лексем, що описуються в лексичному
аналізаторі – масив Lexer.tokenNames[].
public class Token {
public int type;
public String text;
public Token(int type, String text) {this.type=type; this.text=text;}
public String toString() {
String tname = Lexer.tokenNames[type];
return"<'"+text+"',"+tname+">";
}
}

public class Lexer {


public static final int EOFT = 1; // represent EOF token type
public static final int NUMB = 2;
public static final int LPAREN = 3; // parenthesis
public static final int RPAREN = 4;

178
Моделі обчислень у програмній інженерії

public static final int ADDOP = 5;


public static final int MULOP = 6;
public static String[] tokenNames =
{ "n/a", "<EOF>", "NUMB", "LPAREN", "RPAREN",
"ADDOP",
"MULOP" };
char c; // current character
Letter input;
public Lexer(Letter src) {input = src; consume();}
public String getTokenName(int x) { return tokenNames[x]; }

public Token nextToken() throws SyntaxError {


while ( c!=input.EOF ) {
switch ( c ) {
case' ': case'\t': case'\n': case'\r': whileSpace(); continue;
case'(' : consume(); return new Token(LPAREN, "(");
case')' : consume(); return new Token(RPAREN, ")");
case'+' : consume(); return new Token(ADDOP, "+");
case'-' : consume(); return new Token(ADDOP, "-");
case'*' : consume(); return new Token(MULOP, "*");
case'/' : consume(); return new Token(MULOP, "/");
case'%' : consume(); return new Token(MULOP, "%");
default: if ( isDigit() ) return number();
throw new SyntaxError("invalid character: "+c);
}
}
return new Token(EOFT,"<EOF>");
}

void consume(){ c = input.nextChar(); }


boolean isDigit() { return c>='0'&&c<='9'; }
Token number() {
StringBuilder buf = new StringBuilder();
do { buf.append(c); consume(); } while ( isDIGIT() );
return new Token(NUMB, buf.toString()); }
void whiteSpace() {
while ( c==' ' || c=='\t' || c=='\n' || c=='\r' ) consume();
} }

179
Глибовець М. М., Кирієнко О. В., Проценко В. С.

public class Letter {


public static char EOF = (char)-1; // represent end of chars from source
String input; // source string
int p; // index into input of current character
char c; // current character
public Letter(String input){
this.input=input; p=0; c=' ';
}
public char nextChar() {
if (p<input.length()) c=input.charAt(p++); else c=EOF;
return c;
}
}
Лексичний аналіз (сканер, лексичний аналізатор) Lexer утво-
рює потік лексем із потоку символів. У класі Lexer уводяться
типи лексем (цілі константи):
 EOFT – тип лексеми-константи EOF, що позначає кінець
потоку лексем.
 NUMB – тип лексеми-числа. Кожне ціле число – це
послідовність десяткових чисел.
 LPAREN, RPAREN – типи лексем-символів ‘(‘ і ‘)’.
 ADDOP, MULOP – типи лексем-символів, що задають
відповідно операції додавання (‘+’, ‘-’) та операції мно-
ження (‘*’, ‘/’, ‘%’).
Основний метод nextToken() повертає наступну лексему по-
току (аналогічний методу nextChar()). Інші методи:
 number() – формує лексему-число, виділяючи в потоці
символів значення лексеми неперервну послідовність де-
сяткових цифр;
 whileSpace() – пропускає в потоці символів символи
проміжку;
 getTokenName(intx) – повертає рядок, що представляє тип
лексеми x;
 consume() – вводить у змінну c наступний символ потоку;

180
Моделі обчислень у програмній інженерії

 isDigit() – перевіряє, чи наступний символ – десяткова


цифра.
Клас Letter0, що генерує потік символів, модифіковано –
заміною символу ‘$’, який позначає кінець потоку, на універ-
сальний символ EOF.
public static char EOF = (char)-1;

3.5.2. Синтаксичний аналіз


Синтаксичний аналіз реалізує метод рекурсивного спуску,
використовуючи граматику в ітераційній формі, у якої:
 Нетермінали – ідентифікатори з маленької літери, що
описують відповідну синтаксичну конструкцію.
 Початковий нетермінал – expr.
 Термінали – типи лексем (ідентифікатори, що почина-
ються з великої літери).
Для арифметичних виразів граматика має три наступні пра-
вила:
 expr –> ADDOP term {ADDOP term}
 term –> factor {MULOP factor}
 factor –> LPAREN expr RPAREN | NUMB
Задачу синтаксичного аналізу реалізує метод synAna-
lys(Stringsrc) класу Parser, що повертає значення true, якщо
рядок src синтаксично правильний, і false – в іншому разі,
виводячи повідомлення про знайдену синтаксичну помилку.
public class Parser {
Lexer input;
Token next;

public Parser() {}
public boolean synAnalys(String src){
try{
input = new Lexer(new Letter(src));
next = input.nextToken();
expr(); match(Lexer.EOFT);
} catch(SyntaxError ex){

181
Глибовець М. М., Кирієнко О. В., Проценко В. С.

System.out.println("----Syntax ERROR: " +


ex.getMessage());
return false;
}
return true;
}

void expr() throws SyntaxError{


if(next.type==Lexer.ADDOP) next=input.nextToken();
term();
while (next.type==Lexer.ADDOP){
next=input.nextToken(); term();
}
}
void term() throws SyntaxError{
factor();
while (next.type==Lexer.MULOP){
next=input.nextToken(); factor();
}
}
void factor() throws SyntaxError{
if(next.type == Lexer.LPAREN){
next=input.nextToken(); expr(); match(Lexer.RPAREN);
}
elseif (next.type == Lexer.NUMB) next=input.nextToken();
else throw new SyntaxError(
"Expecting LPAREN or NUMB, found " +
input.getTokenName(next.type));
}
void match(int x) throws SyntaxError {
if ( next.type == x ) next=input.nextToken();
else throw new SyntaxError(

"Expecting "+input.getTokenName(x)+ "; found "+ next);


}
}

182
Моделі обчислень у програмній інженерії

3.5.3. Різні джерела символів


На практиці арифметичний вираз може міститися або в
рядку, або у файлі, єдина різниця цих двох випадків полягає в
тому, що початкове джерело символів різне.
Одна з можливих модифікацій – перетворити клас Letter на
інтерфейс, створивши дві його реалізації SrcString і StrcFile, а в
класі Parser – задати два методи analysStr(Stringword) та analys-
File(Stringfile) відповідно для роботи з різними джерелами сим-
волів.
public interface Letter {
public static final char EOF = (char)-1; // represent end of source
char
char nextChar();
}

public class SrcString implements Letter{


String input; // input string
int p; // index into input of current character
char c; // current character

public SrcString(String input){


this.input=input; p=0; c=' ';
}
public char nextChar() {
if (p<input.length()) c=input.charAt(p++); else c=EOF;
return c;
}
}
public class SrcFile implements Letter {
BufferedReader r;
String input; // input string
int p; // index into input of current character
char c; // current character
public SrcFile(String name) {
try {
r = new BufferedReader( new FileReader(name));
inputLine(); consume();

183
Глибовець М. М., Кирієнко О. В., Проценко В. С.

} catch(Exception e) {
System.out.println(
">>>: SrcFile: openfile" + name + ": " + e.getMessage());
input = null; c=EOF;
}
}
public char nextChar() {
consume(); return c;
}
private void inputLine(){
if (r!=null) {
try {
input = r.readLine(); p=0; c=' ';
if (input==null) c=EOF;
} catch(Exception e) {
System.out.println(">>>: SrcFile: inputLine " +
e.getMessage()); }
} else c=EOF;
}
private void consume() {
if (input != null){
if ( !(p<input.length()) ) inputLine();
} else c= EOF;
if (c!=EOF) c = input.charAt(p++);
}
}
Інтерфейс Letter містить константу EOF, що задає кінець по-
току символів, і єдиний метод nextChar(), що повертає наступний
символ потоку.
Клас SrcString(Stringinput) реалізує інтерфейс, задаючи дже-
релом потоку символів рядок input (фактично це модифікація
попереднього класу Letter).
Клас SrcFile(Stringname) реалізує інтерфейс, задаючи джере-
лом потоку символів файл name.
Зміни в класі Parser мінімальні – це поява двох методів, ко-
жний із яких створює екземпляр класу SrcFile або SrcString, що

184
Моделі обчислень у програмній інженерії

розширюють інтерфейс Letter, реалізуючи відповідний потік


символів.
public class Parser {
Lexer input;
Token next;
public Parser() { }

public boolean analysFile(String file){


input = new Lexer(new SrcFile(file));
return synAnalys();
}

public boolean analysStr(String word){


input = new Lexer(new SrcString(word));
return synAnalys();
}

boolean synAnalys(){
try{
next = input.nextToken();
expr();match(Lexer.EOFT);
} catch(SyntaxError ex){
System.out.println("----Syntax ERROR: " + ex.getMessage());
return false;
}

return true;
}
……..
}

3.5.4. Абстрактні синтаксичні


дерева
У багатьох випадках потрібне не лише
підтвердження того, що слово синтаксично
правильне, а й необхідно отримати інформацію

185
Глибовець М. М., Кирієнко О. В., Проценко В. С.

про його синтаксичну структуру, наприклад синтаксичне дерево,


яку буде використано надалі. Синтаксичне дерево має над-
лишкову інформацію і прив’язане до КВ-граматики мови, тому
неефективне у випадку синтаксичних діаграм або граматик в
ітераційній формі. На практиці використовують абстрактні
синтаксичні дерева (AST) – деяке проміжне представлення
синтаксичної структури, яке орієнтується на подальше вико-
ристання: побудова машинних команд, обчислення, оптимізація
тощо.
Для арифметичних виразів AST-дерево містить лише
інформацію, необхідну для його коректного обчислення. Кожний
вузол дерева – лексема і, можливо, список нащадків. Мал. 1
містить AST-дерево для виразу, що заданий у рядку “75*(13-5)”.
У вузлах дерева показані лише значення лексем.
Наведений далі клас AST містить метод int evalExpr(), що об-
числює значення арифметичного виразу, заданого цим абстракт-
ним синтаксичним деревом.
public class AST {
Token root;
List<AST>sons;
public AST(Token c){
root=c; sons=null;
}
public void addSon(AST s){
if (sons==null) sons = new ArrayList<AST> ();
sons.add(s);
}
public Token getRoot(){return root;}
public List<AST> getSons() {return sons;}
@Override
public String toString() {
if (sons==null) return(root.toString());
StringBuilder buf = new StringBuilder();
buf.append('('); buf.append(root);
for (int i=0; i<sons.size(); i++){
buf.append(' ');

186
Моделі обчислень у програмній інженерії

buf.append(sons.get(i).toString());
}
buf.append(')');
return buf.toString();
}
// обчислює AST, що задає арифметичний вираз
public int evalExpr(){
int res,op1,op2;
switch(root.type){
case Lexer.ADDOP:
op1=sons.get(0).evalExpr();
if (sons.size()==2){
op2=sons.get(1).evalExpr();
if (root.text.equals("-")) res=op1-op2; else res=op1+op2;
} else res=-op1;
break;
case Lexer.MULOP:
op1=sons.get(0).evalExpr();
op2=sons.get(1).evalExpr();
if (!root.text.equals("*")){
if(op2==0)
throw new Error("В операції " + root.text + " дільник =
0");
if (root.text.equals("/")) res=op1/op2; elseres=op1%op2;
} else res= op1*op2;
break;
case Lexer.NUMB: res=evalNumber(root.text); break;
default: throw new Error("Неочікувана лексема " +
root.toString());
}
return res;
}
private int evalNumber(String numb){
int res=0;
for(int j=0; j<numb.length(); j++)
res = res*10+(numb.charAt(j)-'0');
return res;
} }

187
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Клас ParserAST, що будує AST, якщо вираз синтаксично пра-


вильний, – ще проста модифікація класу Parser, розширеного
операторами побудови AST.
public class ParserAST {
Lexer input;
Token next;

public ParserAST() { }
public AST analysFile(String file){
input = new Lexer(new SrcFile(file));
return synAnalys();
}
public AST analysStr(String src){
input = new Lexer(new SrcString(src));
return synAnalys();
}

AST synAnalys(){
AST t;
try{
next = input.nextToken();
t=expr(); match(Lexer.EOFT);
} catch(SyntaxError ex){
System.out.println("----Syntax ERROR: " +
ex.getMessage());
return null;
}
return t;
}
AST expr() throws SyntaxError{
AST t = null,t1;
if(next.type==Lexer.ADDOP) {
if (next.text.equals("-")) t= new AST(next);
next=input.nextToken();
}
if (t==null) t=term(); else t.addSon(term());
while (next.type==Lexer.ADDOP){
t1 = new AST(next); next=input.nextToken();

188
Моделі обчислень у програмній інженерії

t1.addSon(t); t1.addSon(term()); t=t1;


}
return t;
}
AST term() throws SyntaxError{
AST t,t1;
t = factor();
while (next.type==Lexer.MULOP){
t1 = new AST(next); next=input.nextToken();
t1.addSon(t); t1.addSon(factor()); t=t1;
}
return t;
}
AST factor() throws SyntaxError{
AST t=null;
if(next.type == Lexer.LPAREN){
next=input.nextToken(); t=expr(); match(Lexer.RPAREN);
}
else if (next.type == Lexer.NUMB) {
t = new AST(next); next=input.nextToken();
}
else throw new SyntaxError(
"Expecting LPAREN or NUMB, found " +
input.getTokenName(next.type));
return t;
}
void match(int x) throws SyntaxError {
if ( next.type == x ) next=input.nextToken();
else throw new SyntaxError(
"expecting "+input.getTokenName(x)+ "; found "+ next);
}
}

3.6. Лямбда-вирази
3.6.1. Потоки даних
Потік даних – це послідовність, можливо, нескінченна, да-
них одного типу. Всі елементи одного потоку – або екземпляри

189
Глибовець М. М., Кирієнко О. В., Проценко В. С.

одного класу T – Stream<T>, або значення одного примітивного


типу – IntStream (LongStream, DoubleStream).
Головне призначення потоків – організувати конвеєр опе-
рацій, який можна виконувати послідовно або паралельно. Кон-
веєр має наступні три етапи:
 створення потоку даних;
 виконання проміжних операцій, які перетворюють потік
на інший, можливо, декілька разів;
 виконання заключної операції для отримання результату.
Особливість обробки потоків полягає в тому, що всі операції
на першому й другому етапі відкладені («ліниві»), вони викону-
ються на вимогу заключної («енергійної») операції.
Більшість класів, інтерфейсів, методів і операцій із потоками
даних міститься в пакеті java.util.stream, крім того, ціла низка
методів розширила вже відомі класи: метод splitAsStream – у
класі Pattern, метод lines – у класі Files. Далі для знайомства
наводяться найпростіші з них. Можливості інших коментуються
на конкретних прикладах роботи з потоками даних.
Потік можна створити із файлу, масиву або колекції даних.
Наприклад, якщо є масив або колекція
String as1 = …..;
ArrayList <Book> library …..;
то наступні методи з інтерфейсів Stream і Collection утворюють
потоки відповідних даних:
Stream<String> ss1 = Stream.of(as1);
Stream<Book> slibrary = library.stream();
Потік даних без елементів (порожній) можна утворити ста-
тичним методом Stream.empty().
Stream<String> sno = Stream.empty();
Для створення потоків примітивного типу існують власні ме-
тоди. Далі створюється потік цілих чисел від 0 до 99.
IntStream si = IntStream.range(0,100);
Розглянемо основні методи перетворення потоків. Нехай
Stream<T>st – потік даних типу T:

190
Моделі обчислень у програмній інженерії

 Якщо p – предикат, тобто функція, що перетворює тип T


на логічний boolean, то st.filter(p) перетворює потік st на
новий, залишаючи лише елементи, що задовольняють
предикат p.
 Якщо f – функція, що перетворює значення типу T на
значення типу K, то st.map(f) застосовує функцію до
кожного елемента потоку st, створюючи новий потік
типу K (Stream<K>).
 Якщо f – функція, що перетворює значення типу T на
потік значень типу K (Stream<K>), то st.flatMap(f) засто-
совує функцію f до кожного елемента потоку st, злива-
ючи підсумкові потоки типу K на один потік типу K
(Stream<K>).
Потоки примітивного типу додатково мають свої методи.
Наприклад, якщо IntStreamis – потік цілих значень і f – функція
із цілих значень у значення типу T, то is.mapToObj(f) будує потік
даних типу T, застосовуючи функцію до кожного елемента по-
току is.
Існує багато методів, які використовуються в ролі заключної
(«енергійної») операції. Наведемо лише деякі з них.
 Після закінчення обробки потоку st потрібно перегля-
нути всі його елементи. Метод st.forEach(f) застосовує
функцію f до кожного елемента потоку st.
 Після закінчення обробки потоку st потрібно зібрати
(накопичити) всі елементи в структурі даних. Метод
st.toArray() повертає масив типу Object[].
 Низка методів зводить потік st до одного значення:
o st.count() повертає ціле – кількість елементів у
потоці st;
o низка методів повертає логічне значення залежно
від виконання умови – предиката p:
 st.anyMatch(p) – умова виконується хоча б
для одного елемента потоку;

191
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 st.allMatch(p) – умова виконується для


всіх елементів потоку;
 st.noneMatch(p) – умова НЕ виконується
для всіх елементів потоку.
 Найпотужніший метод – collect(), який приймає екзем-
пляр класу, що реалізує інтерфейс Collector. Далі наво-
дяться деякі приклади використання методів класу
Collectors:
o st.collect(Collectors.toList()) – згортає всі елементи
потоку st у список;
o st.collect(Collectors.counting()) – вираховує кіль-
кість елементів потоку, аналог методу st.count().

3.6.2. Функціональні інтерфейси


Функціональний інтерфейс – це інтерфейс із єдиним аб-
страктним методом. Більшість функціональних інтерфейсів опи-
сано в пакеті java.util.function. Деякі функціональні інтерфейси,
крім абстрактного методу, мають й інші (неабстрактні) методи.
Наприклад, функціональний інтерфейс Predicate<T> має аб-
страктний метод test, який представляє предикат – функцію від
одного аргументу типу T, результат якої – логічне значення
boolean. Для об’єднання предикатів інтерфейс має методи and(),
or(), negate() і метод isEqual().
Лямбда-вираз можна використовувати, коли потрібно ство-
рити об’єкт класу, що реалізує функціональний інтерфейс.
Єдине, що можна зробити з лямбда-виразом у Java – при-
своїти його змінній, типом якої є функціональний інтерфейс, аби
потім компілятор перетворив лямбда-вираз на екземпляр такого
інтерфейсу. Зауважимо, що у Java така змінна, типом якої є
функціональний інтерфейс, може бути лише параметром методу.
 У Java функції будуються із використанням об’єктів,
що являють собою екземпляри класів, які реалізують
конкретний інтерфейс. Лямбда-вирази надають зруч-
ний синтаксис для отримання таких об’єктів.

192
Моделі обчислень у програмній інженерії

Функціональні інтерфейси досить часто трапляються в стан-


дартній бібліотеці Java.
 Клас ArrayList має метод:
o removeIf із параметром Predicate<T>;
o forEach із параметром Consumer<T> – функ-
ціональний предикат.
 Клас Array має метод sort із параметром Compara-
tor<T>:
o Comparator<T> – інтерфейс з одним абстракт-
ним методом compare, тобто функціональний
інтерфейс.
 Викликаючи такі методи, можна вживати лямбда-
вирази.
Лямбда-вираз із двома параметрами x і y, відповідно типу T1
і T2, значення якого обчислюється за виразом expr(x,y), має
вигляд:
(T1 x, T2 y) –> expr(x,y)
Якщо в тілі лямбда-виразу потрібно виконати обчислення,
яке не вписується в один вираз, то використовується по-
слідовність операторів, що закінчується оператором return expr;
у фігурних дужках {}:
(T1 x, T2 y) –> {op1; …; opn; return expr;}
Тип результату обчислення лямбда-виразу не вказується, але
компілятор виводить його з тіла виразу й перевіряє на відпо-
відність очікуваному результату. У багатьох випадках компі-
лятор може вивести тип параметрів із контексту, що зумовлює:
(x,y) –> expr(x,y)
Якщо лямбда-вираз не має параметрів, то потрібно вказати
порожні дужки:
() –> expr
Якщо лямбда-вираз має один параметр, тип якого виво-
диться, то можна не вживати круглих дужок:
x –> expr(x)

193
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Якщо робота лямбда-виразу зводиться лише до виклику ме-


тоду, то лямбда-вираз можна замінити посиланням на метод.
Тобто замість list.remove(x –> Objects.isNull(x)) можна викори-
стати list.remove(Objects::isNull), а замість list.forEach(x –>
System.out.println(x)) – list.forEach(System.out::println).

3.6.3. Обробка масиву


Дано масив inta[]. Метод evalArray(a) повинен збільшити
кожний елемент масиву на 2, відібрати кратні 3 й обчислити
їхню суму. Далі наводяться два можливі варіанти розв’язку.
static int evalArrayIt(int[] a){
int s = 0;
for(int i=0; i<a.length; i++){
int c = a[i]+2;
if(c%3 == 0) s +=c;
}
return s;
}

static int evalArraySt(int[] a){


return IntStream.of(a)
.map(x->x+2)
.filter(x->x%3==0)
.sum();
}
Метод evalArrayIt(a) обчислює значення традиційно (ітера-
ційно), послідовно обробляючи кожний елемент масиву a.
Метод evalArraySt(a) працює з потоками даних, використо-
вуючи wholemael programming – роботу з цілими структурами
даних, а не їх частинами:
 IntStream.of(a) – створює з елементів масиву потік даних
примітивного (цілого) типу.
 .map(x –> x+2) – збільшує кожний елемент потоку на 2.
 .filter(x –> x%3==0) – перетворює потік, залишаючи в
ньому лише цілі числа, кратні 3.

194
Моделі обчислень у програмній інженерії

 .sum() – заключна операція – обчислює суму всіх еле-


ментів потоку.

3.6.4. Числа Фібоначчі


Числа Фібоначчі – числова послідовність Fn, задана реку-
рентним співвідношенням
F1 = 1, F2 = 1, Fn+2 = Fn + Fn+1, n = 1, 2, 3, …
Необхідно обчислити й вивести перші десять чисел Фібо-
наччі.
static void fibonacciIter(){
int[] f = new int[10];
f[0] = 1; f[1] = 1;
System.out.println(f[0]);
System.out.println(f[1]);
for(int i=2; i<10; i++) {
f[i] = f[i-2]+ f[i-1];
System.out.println(f[i]);
}
}

static void fibonacciStream(){


System.out.println(1);
Stream.iterate(new Pair(1,1), p->new Pair(p.snd(),p.fst()+p.snd()))
.limit(9)
.forEachOrdered(p->System.out.println(p.snd()));
}

class Pair {
int f, s;
Pair(int fi, int si){
f=fi; s=si;
}

int fst() {return f;}


int snd() {return s;}
}

195
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Метод fibonacciIter() виводить перші два числа, а потім цикл


і послідовно обчислює й виводить інші вісім чисел Фібоначчі.
Метод fibonacciStream(), використовуючи простий клас Pair,
кожний елемент якого – пара цілих чисел fі s, формує нескінчен-
ний потік пар чисел Фібоначчі (F1,F2), (F2,F3), (F3,F4), (F4,F5),
(F5,F6), …, обираючи з нього потім необхідну кількість.
 Stream.iterate(new Pair(1,1), p–> new Pair(p.snd(),
p.fst()+p.snd()) формує нескінченний потік пар чисел
Фібоначчі, використовуючи статичний метод Stream.
iterate(init, f), у якого init – перший елемент потоку й f –
функціональний інтерфейс UnaryOperator<T>. Метод
формує потік init, f(init), f(f(init)), f(f(f(init))), … Перший
елемент потоку – пара newPair(1,1). Функціональний
інтерфейс f реалізує лямбда-вираз p –> newPair(p.snd(),
p.fst()+p.snd()), який по парі сусідніх чисел Фібоначчі
Pairp будує наступну.
 .limit(9) із нескінченного потоку обирає його початок –
потік із дев’яти елементів, який містить перші десять
чисел Фібоначчі.
 .forEachOrdered(p –> System.out.println(p.snd())) –
заключна операція, що послідовно (Ordered) обробляє всі
елементи потоку, використовуючи лямбда-вираз p –>
System.oup.println(p.snd()), який виводить другий елемент
кожної пари. Метод forEachOrdered(f) має один параметр
f – функціональний інтерфейс Consumer<T>, який має
абстрактний метод action, що споживає параметр типу T,
нічого не повертаючи.

3.6.5. Текстові файли і слова


Довільний текстовий файл складається з рядків. Своєю чер-
гою, кожний рядок складається зі слів. Кожне слово – це непе-
рервна послідовність символів, що не містить символів про-
міжку.

196
Моделі обчислень у програмній інженерії

Регулярний вираз (зразок) “\\s+” зіставляється з непорожнім


словом, що містить лише символи проміжку.
Метод input.split(“\\s+”) повертає String[] масив усіх слів, що
входять у рядок input.
Метод Pattern.compile(“\\s+”).splitAsStream(input) класу
Pattern повертає потік усіх слів, що входять у рядок input.
Якщо p – екземпляр класу Path, що задає шлях до тестового
файлу, то Files.lines(p) повертає Stream<String> – потік усіх ряд-
ків, що становлять файл.
Використовуючи потік слів, на які перетворюється текстовий
файл, просто вирахувати різні його характеристики – на кшталт
кількості й частоти слів. Усі ці методи мають схожу структуру,
відрізняючись лише обробкою потоку слів. Далі наводиться
структура такого методу method, який повертає значення типу T
за файлом з іменем file.
static Tmethod(String file){
Pattern delim = Pattern.compile("\\s+");
try (Stream<String> lines = Files.lines(Paths.get(file))){
Stream<String>words = lines.flatMap(l->delim.splitAsStream(l));
return words. . . обробка потоку слів . . .;
} catch(Exception ex){
System.out.println("method: " + ex.getMessage());
return null;
}
}
Підрахунок кількості слів у файлі:
static Long cntWords(String file){
....
return words.collect(Collectors.counting());
....
}
 .collect(col) – метод, який реалізує заключну операцію,
застосовуючи до потоку колектор col – екземпляр класу,
що реалізує інтерфейс Collector.
 Клас Collectors надає низку готових реалізацій цього
інтерфейсу.

197
Глибовець М. М., Кирієнко О. В., Проценко В. С.

 Collectors.counting() – реалізація, що підраховує кількість


елементів потоку.
Підрахунок кількості слів, що містять символ a:
static Long cntWordsWitha(String file){
....
return words.filter(Pattern.compile("\\.*a\\.*").asPredicate())
.collect(Collectors.counting());
....
}
 Зразок (регулярний вираз) “\\.*a\\.*” зіставляється зі сло-
вами, що містять символ a.
 Метод pat.asPredicate() за pat – екземпляром класу Pat-
tern – створює предикат, що реалізує функціональний
інтерфейс Predicate<String>.
Підрахунок кількості різних слів у файлі:
static Long cntDistinctWords(String file){
....
return words.distinct()
.count();
....
}
 Метод words.distinct() – перетворює потік words на потік,
що не має слів-дублікатів.
 Метод .count() – повертає кількість елементів потоку.
Аналогічний методу .collect(Collectors.counting()).
Підрахунок частоти слів у файлі:
static Map<String,Long> frequentWords(String file){
....
return words.collect(Collectors.groupingBy(x->x,Collectors.counting()));
....
}
 Метод Collectors.groupingBy(f), де f – функція, що за зна-
ченням типу T обчислює значення типу K, згортає потік
значень типу T (Stream<T>) у відображення (таблицю)
типу Map<K, List<T>>. Цей метод групує усі значення
потоку в списки List<T>. В один список потрапляють усі

198
Моделі обчислень у програмній інженерії

елементи st-потоку, на яких функція набуває одного зна-


чення k = f(st). У підсумковому відображенні цей список
зв’язується із ключем k = f(st).
 Якщо кожний список потрібно якось обробити, застосу-
вавши функцію dst, що за списком List<T> будує зна-
чення типу B, то додатково до групування використову-
ють понижувальний колектор dst, отримуючи метод
Collectors.groupingBy(f,dst), який список значень типу T
(Stream<T>) перетворює на відображення типу
Map<K,T>.
 Метод Collector.groupingBy(x –> x, Collectors.counting()),
використовуючи лямбда вираз x –> x, що задає тотожну
функцію, будує відображення, зв’язуючи із кожним сло-
вом файлу список усіх таких слів, що трапилися у файлі.
Потім до кожного списку застосовується понижувальний
колектор Collectors.counting(), який обчислює його дов-
жину – значення типу Long.

3.6.6. Бібліотека
Кожен екземпляр класу Book задає інформацію про одну
книжку бібліотеки:
 Список авторів книжки – ArrayList<String> authors.
 Назва книжки – String titleName.
 Об’єм книжки, кількість сторінок – int pages.
 Множина жанрів, у які потрапляє книжка – Set<String>
types.
 Назва видавництва – String pubName.
 Для доступу до кожного з атрибутів клас має відповідний
селектор get…
public class Book {
ArrayList <String>authors;
String titleName;
int pages;
Set <String>types;
String pubName;

199
Глибовець М. М., Кирієнко О. В., Проценко В. С.

public Book(ArrayList<String>authors, String titleName,


int pages, Set<String>types, String pubName) {
this.authors = authors;this.titleName = titleName;
this.types = types; this.pages = pages;
this.pubName = pubName;
}
public ArrayList<String> getAuthors() {return authors;}
public String getTitleName() {return titleName;}
public Set<String> getTypes() {return types;}
public int getPages() {return pages;}
public String getPubName() {return pubName;}
public String toString() {
return "Book [authors=" + authors + ", titleName=" + titleName +
", pages=" + pages + ", types=" + types +
", pubName=" + pubName + "]";
}
}
Інформацію про бібліотеку задає потік Stream<Book>library.
Далі наводиться низка методів, які за бібліотекою формують
різну інформацію про неї, демонструючи методи роботи з пото-
ками.
public long countAllBooks(Stream <Book>library){
return library.collect(Collectors.counting());
}
Метод countAllBooks(library) обчислює загальну кількість
книжок у бібліотеці.
public Set<String> allAuthors(Stream <Book>library){
return library.flatMap(x->x.getAuthors().stream())
.collect(Collectors.toSet());
}
Метод allAuthors(library) формує множину авторів, чиї кни-
жки є в бібліотеці.
public Map <String, Integer> numberOfBooksC(Stream <Book>library){
Map<String, List<Book>>booksByPublishes =
library.collect(Collectors.groupingBy(book-
>book.getPubName()));
// (Book::getPubName)

200
Моделі обчислень у програмній інженерії

Map <String, Integer>numberOfBooks = new HashMap<>();


for(Entry <String, List<Book>>entry : booksByPublishes.entrySet()){
numberOfBooks.put(entry.getKey(), entry.getValue().size());
}
return numberOfBooks;
}
public Map <String, Long> numberOfBooksS(Stream <Book>library){
return library.collect(Collectors.groupingBy(book-
>book.getPubName()),
Collectors.counting())); //
(Book::getPubName)
}
Методи numberOfBooksC(library) і numberOfBooksS(library)
обчислюють, скільки книжок у бібліотеці випущено кожним із
видавництв.
Для формування заключного відображення використовується
ітератор або понижувальний колектор.
public Map <String, List<String>> nameOfBooksC(Stream
<Book>library){
Map<String, List<Book>>booksByPublishes =
library.collect(Collectors.groupingBy(book-
>book.getPubName()));
Map <String, List<String>>nameOfBooks = new HashMap<>();
for(Entry <String, List<Book>>entry : booksByPublishes.entrySet()){
nameOfBooks.put(entry.getKey(),
entry.getValue()
.stream()
.map(b->b.getTitleName())
//(Book::getTitleName)
.collect(Collectors.toList()));
}
return nameOfBooks;
}

public Map <String, List<String>> nameOfBooksS(Stream


<Book>library){
return library.collect(

201
Глибовець М. М., Кирієнко О. В., Проценко В. С.

Collectors.groupingBy(book->book.getPubName(), //
Book::getPubName
Collectors.mapping(b->b.getTitleName(), //Book::getTitleName,
Collectors.toList())));
}
Методи nameOfBooksC(library) і nameOfBooksS(library)
формують відображення (таблицю), у якому всі книжки бібліо-
теки класифіковані за видавництвами, що їх видало.
Для формування заключного відображення використовується
ітератор або понижувальний колектор.

3.6.7. Множина Мандельброта


Множина Мандельброта – обмежена та зв’язана множина
на комплексній множині, межа якої утворює фрактал. Фрактал
(термін, уведений Мандельбротом) – це нерегулярна, само-
подібна фігура, малі частини якої в довільному збільшенні
подібні до неї самої.
Множина Мандельброта – це множина таких точок c на
комплексній площині, для яких рекурентне співвідношення
zn+1 = zn2 + c при z0 = 0 задає обмежену послідовність.
 Тобто такі c, для яких існує R таке, що |zn| < R для всіх
натуральних n.
 Якщо для заданого с послідовність zn відповідає умові
𝑠𝑢𝑝𝑛  𝑁 |𝑧𝑛 | < ∞, то c належить множині Мандель-
брота.
 Якщо комплексні точки можна трактувати як точки на
площині, то множина Мандельброта – обмежена фігура
на площині.
 З погляду математики малюнок множини Мандель-
брота – це чорно-біла фігура. Якщо точка c належить
множині, то її колір – чорний, в іншому разі – білий.
Неважко довести наступну властивість послідовності zn:
якщо для деякого n |zn| > 2, то послідовність прямує до нескін-
ченності.

202
Моделі обчислень у програмній інженерії

Якщо виразити комплексне zn через координати дійсної xn і


комплексної yn частин, то:
 z n  x  y * i , | |  y , 2  y2  4
xn n
2
z x
2
n n n n n

 Число 2 вважається «межею нескінченності»; щойно


|zn| > 2, подальший процес розрахунку zn закінчується.
 Якщо зафіксувати n, при якому |zn| > 2 закінчується про-
цес ітерації, тому що досягнено «межі нескінченності»,
то це число фіксує швидкість руху в нескінченність, і з
ним можна зв’язати певний колір.
У кольоровому зображенні множини Мандельброта, якщо
точка належить множині, то її колір – чорний, а в іншому разі її
колір – це швидкість, із якою послідовність zn прямує до нескін-
ченності.

 У реальній побудові кількість ітерацій обмежують,


max=1000. Тому точка c має чорний колір, якщо для всіх
кроків ітерації від 0 до max-1, xn2+yn2 > 4.
 Значення n, при якому xn2+yn2 стає більше 4, визначає
колір точки в іншому разі.
Відомо, що вся множина повністю розміщується в межах
прямокутника з координатами вершин (-2,1), (2,1), (2,-1), (-2,-1).
Щоб побудувати множину Мандельброта, цей прямокутник за-

203
Глибовець М. М., Кирієнко О. В., Проценко В. С.

повнюється множиною точок: width = 2*1980 – за горизонталлю


і height = 2*1080 – за вертикаллю.
 Вважається, що width точок заповнюють відрізок на
дійсній осі від -2 до 2.
 Точці (col,row), де 0 ≤ col<width і 0 ≤ row<height
відповідає точка комплексної площини (c_re, c_im), де:
o Дійсна частина c_re = (col-width/2) * 4.0 / width.
o Комплексна частина c_im = (row-height/2) * 4.0 /
width.
Графічне зображення (образ) у Java – це прямокутний дво-
вимірний масив пікселів. Кожний піксель задає колір у позиції,
що описується координатами col і row.
Для представлення образу множини Мандельброта викори-
стовується буфер image – екземпляр класу BufferedImage.
 BufferedImage image = new BufferedImage (width, height,

BufferedImage.TYPE_INT_RGB)
 Для запису в буфер image інформації про піксель з
координатами x,y і кольором rgb використовується метод
setRGB(int x, inty, int rgb).
 Для запису образу у файл використовується статичний
метод write класу ImageIO ImageIO.write(image, format,
file):
o image – графічний образ – екземпляр класу
BufferedImage;
o format – формат вихідного файлу “png”;
o file – файл, куди потрібно записати графічний
образ у потрібному форматі, – екземпляр класу
File. Якщо nmFile – ім’я файлу, то файл – екзем-
пляр класу File може створити конструктор
newFile(nmFile).
public class Mandelbrot {
int width = 2*1920;
int height = 2*1080;
BufferedImage image =

204
Моделі обчислень у програмній інженерії

new BufferedImage(width, height,


BufferedImage.TYPE_INT_RGB);
int max = 1000;
int black = 0;
int[] colors = new int[max];

public Mandelbrot(){
for (int i = 0; i<max; i++) {
colors[i] = Color.HSBtoRGB(i/256f, 1, i/(i+8f));
}
}

private int findColor(Coor t){


int col = t.x; introw = t.y;
double c_re = (col - width/2)*4.0/width;
double c_im = (row - height/2)*4.0/width;
double x = 0, y = 0;
int iteration = 0;
while (x*x+y*y< 4 &&iteration<max) {
double x_new = x*x-y*y+c_re;
y = 2*x*y+c_im;
x = x_new;
iteration++;
}
return (iteration<max)? colors[iteration]: black;
}
private class Coor{
int x, y;
Coor(int xi, int yi){x=xi; y=yi;}
}
private class Pixel{
int x, y, color;
Pixel(Coor t){
x=t.x; y=t.y; color = findColor(t); }
void addImage(){image.setRGB(x, y, color);}
}

205
Глибовець М. М., Кирієнко О. В., Проценко В. С.

private Stream<Coor> allPixel(){


return IntStream.range(0,width).boxed()
.flatMap(x-> IntStream.range(0, height)
.mapToObj(y->new
Coor(x,y)));
}

public void sequantialForm(String nmFile) throws Exception {


allPixel().sequential()
.map(Pixel::new)
.forEach(Pixel::addImage);
ImageIO.write(image, "png", new File(nmFile));
}

public void parallelForm(String nmFile) throws Exception {


allPixel().parallel()
.map(Pixel::new)
.forEach(Pixel::addImage);
ImageIO.write(image, "png", new File(nmFile));
}
}
Клас Mandelbrot має два методи – sequentialForm(nmFile) і
parallelForm(nmFile), – кожний із яких будує образ множини
Мандельброта у файлі формату .png з іменем nmFile, відповідно,
послідовно й паралельно.
Під час утворення екземпляру класу формується масив ко-
льорів color, кількість елементів якого відповідає кількості іте-
рацій max=1000, протягом яких визначається, чи сходиться пос-
лідовність zn.
Клас Mandelbrot містить два внутрішніх класи:
 Coor – описує кординати x та y одного пікселя.
 Pixel – описує один піксель:
o Під час створення екземпляру класу викликається
метод findColor(coor), який обчислює колір пік-
селя з координатами coor, визначаючи, чи схо-
диться послідовність zn.

206
Моделі обчислень у програмній інженерії

o Клас має метод addImage(), що встановлює один


піксель образу.
Метод allPixel() формує потік координат усіх пікселів образу
Stream<Coor>:
 IntStream.range(0,width) – формує потік даних цілого
типу IntStream – числа від 0 до width-1.
 .boxed() – перетворює потік IntStream на потік
Stream<Integer>.
 .flatMap(x–>expr…) – перетворює потік Stream<Integer>
на потік Stream<Coor>. За кожним елементом (Integerx)
початкового потоку метод, що задається лямбда-виразом
expr…, будує потік Stream<Coor>, який уливається в під-
сумковий.
 x –> IntStream.range(0,height).mapToObj(y –>new-
Coor(x,y)) – лямбда-вираз, що будує потік даних
Stream<Coor> за даним Integer x:
o IntStream.range(0,height) – формує потік даних
цілого типу IntStream – числа від 0 до height-1.
o .mapToObj(f) – використовуючи функцію f з int у
Coor, перетворює потік IntStream на потік
Stream<Coor>.
o y –> newCoor(x,y) – лямбда-вираз, що реалізує
функціональний інтерфейс IntFunction<Coor>,
будуючи координати пікселя.
Методи sequentialForm(nmFile) і parallelForm(nmFile), що
будують образ множини Мандельброта, мають однакову струк-
туру:
 allPixel() – утворюється потік Stream<Coor>.
 .sequential() або .parallel() – утворює еквівалентний
послідовний або паралельний потік. Якщо потік уже та-
кий, то не змінюється.
 .map(Pixel::new) – перетворює потік Stream<Coor> на
потік Stream<Pixel>.

207
Глибовець М. М., Кирієнко О. В., Проценко В. С.

o Використовується виклик конструктора, еквіва-


лентно використанню лямбда-виразу x –> new-
Pixel(x).
 .forEach(Pixel::addImage) – заключна операція, що ство-
рює графічний образ, застосовуючи до кожного елемента
потоку метод addImage().
o Використовується виклик методу, еквівалентно
використанню лямбда-виразу x –> x.addImage().

3.6.8. Задачі
1. Написати статичний метод, використовуючи IntStream:
 int sumA(int[] ia), що знаходить суму всіх елементів ма-
сиву ia;
 int[] onlyPos(int[] ia), що формує за масивом ia новий
масив, що включає лише додатні елементи масиву ia;
 IntStream factS(), що формує нескінченний потік фак-
торіалів додатних натуральних чисел;
 OptionalInt maxPos(IntStream is), що знаходить у потоці
is максимальне парне додатне число.
2. Написати статичний метод, використовуючи Stream<String>:
 boolean isIn(String[] as, String s), що визначає, чи є в ма-
сиві as елемент, який дорівнює рядку s;
 Stream<String> onlyAb(String word), який формує потік
непорожніх підрядків рядка word, що містять лише сим-
воли a і b;
 Stream<String> onlyAbBeginA(String word), який формує
потік усіх підрядків рядка word, що починаються з сим-
волу a й містять лише символи a та b;
 Optional<String> minAb(String word), який знаходить у
слові word найменший підрядок, що починається з сим-
волу a й містить лише символи a та b. (У слові може не
бути жодного такого підрядка.)
3. Написати статичний метод, використовуючи Stream<String>
і роботу із файлами:

208
Моделі обчислень у програмній інженерії

 long sizeFile(String file), що визначає розмір (кількість


рядків) текстового файлу file;
 long cntDistAb(String file), який знаходить, скільки різних
підрядків, що починаються із символу a й містять лише
символи a та b, є в текстовому файлі file.
4. Написати статичний метод, використовуючи Stream<Book>:
 String[] allPub(Stream<Book> library), що формує
впорядкований масив різних типографій, у яких були
видані книжки бібліотеки library;
 long cntBookAuthor(Stream<Book> library, String aut), що
знаходить кількість книжок у бібліотеці library, автором
яких є aut.
 Optional<Book> minBookPub(Stream<Book> library,
String pub), який знаходить найтоншу книжку в
бібліотеці library, видану у видавництві pub. (У бібліо-
теці може не бути жодної книжки, виданої у видавництві
pub).

Список літератури
1. Ахо А., Лам М., Сети Р., Ульман Д. Компиляторы: прин-
ципы, технологии и инструментарий. СПб. : ООО «Диа-
лектика», 2019. 1184 с.
2. Катленд Т. Вычислимость. Введение в теорию рекурсив-
ных функций. Москва : Мир, 1983. 256 с.
3. Коузен К. Современный Java: рецепты программирова-
ния. Москва : ДМК Пресс, 2018. 274 с.
4. Хорстманн Кей С. Java SE 8. Базовый курс. Москва : Изд.
дом «Вильямс», 2016. 464 с.

209
Навчальне видання

Глибовець М. М., Кирієнко О. В., Проценко В. С.

МОДЕЛІ ОБЧИСЛЕНЬ У ПРОГРАМНІЙ ІНЖЕНЕРІЇ

Навчальний посібник

Оригінал-макет підготовлений
Видавничим домом «Києво-Могилянська академія»

Підписано до друку 20.11.2019 р. Формат 60×90 1/16


Папір офсетний. Друк офсетний. Гарнітура Times New Roman
Наклад 300 прим. Ум. друк. арк. 13,25. Зам. № 19-19

Видавничий дім «Києво-Могилянська академія»


Свідоцтво про внесення суб’єкта видавничої справи
до Державного реєстру видавців, виготовлювачів і розповсюджувачів
видавничої продукції ДК № 6795 від 10.06.2019 р.

Адреса видавництва:
04070, м. Київ, Контрактова пл., 4
Тел./факс: (044) 425-60-92
E-mail: realization.ukma@gmail.com
http://www.publish-ukma.kiev.ua

Надруковано у друкарні «7БЦ»


м. Київ, вул. Виборзька, 84
Глибовець М. М.
Г54 Моделі обчислень у програмній інженерії : навчальний по-
сібник / М. М. Глибовець, О. В. Кирієнко, В. С. Проценко. Київ :
Видавничий дім «Києво-Могилянська академія», 2019. 212 с.
(Серія «Могилянський підручник»).

ISBN 978-966-2410-99-0 (Серія «Могилянський підручник»)


ISBN 978-966-518-767-7

У навчальному посібнику викладено теоретичний базис, який є


підґрунтям більшості елементів програмного забезпечення, – головні
моделі обчислень, що лежать в основі провідних мов програмування.
Розглядаються різні методи конструювання моделей, які утворюють
фундамент базових алгоритмів, що використовуються в практиці
програмування. Також описано механізми породження та аналізу фор-
мальних мов: форми Бекуса-Наура, контекстно-вільні та регулярні гра-
матики. Представлено використання цих теоретичних напрацювань у
мові програмування Java. В посібнику наведено велику кількість
прикладів.
Для вивчення моделей обчислень описано використання про-
грами ModelComp. Програма має зручний інтерфейс та дозволяє буду-
вати, виконувати й зберігати різні моделі обчислень.
Видання призначене для студентів вищих навчальних закладів та
аспірантів, які спеціалізуються за напрямом «Інженерія програмного
забезпечення» й «Комп’ютерні науки».

УДК 004.4/.9(075.8)

You might also like