Може міркувати ваш код?

Коли ми думаємо про міркуваннях (reasoning) в програмуванні, перше, що приходить в голову — це логічне програмування і підхід базується на правилах (rule-based), експертні системи і системи управління бізнес-правилами (business rule management systems, BRMS). Загальнопоширені мультипарадигмальные мови практично не включають ці підходи, хоча і працюють з ними за допомогою бібліотек і інтерфейсів. Чому? Тому що ці мови не можуть включати себе форми, які в деякому сенсі суперечать їх суті. Популярні мови програмування зазвичай працюють з детермінізмом (очікувані дані, сценарії використання, тощо), в той час як підходи, що використовують міркування, зазвичай працюють з невизначеністю (непередбачувані дані, сценарії використання, тощо). Міркування (reasoning) буде різним в обох випадках теж. У першому, міркує архітектор або розробник, у другому ж міркує машина виводу/правил (reasoning/rule engine).

Ми не можемо віддати перевагу ту чи іншу сторону цього дуалізму. Зовнішній світ сповнений детермінізму і невизначеності, вони поєднуються в одних і тих же речах і явищах. Наприклад, кожен день ми можемо їхати додому по одному і тому ж найкоротшим маршрутом. Ми вже витратили часу в минулому на його пошук, ми знаємо його особливості (так що ми можемо їхати по нього, чи не "на автоматі"). Використання цього маршруту дуже ефективно, але не гнучко. Якщо утворилася пробка та інші обставини, найкоротший маршрут може стати найдовшим. Ми можемо використовувати навігатор, але це не дає повної гарантії. Пробка може утворитися, коли ми будемо в дорозі і просто не буде поворотів, щоб прокласти інший маршрут. Ми і самі можемо знати про те, що через 30 хвилин буде щоденна пробка якраз на маршруті, запропонованому навігатором. Висновок полягає в тому, що краще мати вибір між одного разу обраним рішенням (і не витрачати на це час знову) і гнучкими шляхами (якщо обставини змінилися).
ми Можемо поєднувати дані підходи в коді? Найбільш популярні рішення на зараз: плагіни, кастомізації, предметно-орієнтовані мови (DSL), вже згадані правила, і т. п. Плагіни обмежені необхідністю написання коду і вимагають певний рівень компетентності. Кастомізації, правила і предметно-орієнтовані мови обмежені необхідністю вивчення і тієї функціональністю програми, яка доступна для них. Чи можемо ми полегшити вивчення і дати доступ до якомога більшої функціональності програми? Одне з можливих рішень: мова розмітки сенсу. Що він може зробити?
  • Базується на природному мовою, тому навчання може бути мінімальним
  • Дозволяє зв'язування з кодом, утворюючи інтерфейс природної мови для такого коду, що мотивує більше покриття функціональності в порівнянні з кастомізацією, яка створюється для певної областей коду
  • Так як базується на природному мові (що допускає невизначеність) і на алгоритмах (які допускають швидше детермінізм), може працювати з обома сторонами дуалізму невизначеності-детермінізму
Розглянемо це на прикладі обчислення обсягу планет. Класичне рішення в об'єктно-орієнтованій мові може виглядати так:
class Ball {
int radius;
double getVolume() { ... }
}

class Planet extends Ball { ... }

Planet jupiter = new Planet("Jupiter");
double vol = jupiter.getVolume();

У цьому коді досить міркувань: у визначеннях, полях, методи, ієрархії класів, і т. п. Наприклад, те, що планета є кулею, а Юпітер є планетою. Однак, ці міркування неявні, не можуть переиспользоваться, сильно взаємозалежні (tightly coupled) з кодом. У схожому коді JavaScript міркування приховані в умовностях (conventions):
function getBallVolume() { ... }

var jupiter = planet.getPlanet('Jupiter');
var volume = getBallVolume(jupiter.diameter);

Коли ж ми використовуємо розмітку сенсу:
meaningful.register({
func: planet.getPlanet,
question: 'Будь {_} {є} діаметр {чого} планета',
input: [{
name: 'планета',
func: function(planetName) { return planetName ? planetName.toLowerCase() : undefined; }
}],
output: function(result) { return result.diameter; }
});

meaningful.register({
func: getBallVolume,
question: 'Будь {_} {є} обсяг {чого} кулі',
input: [{
name: 'діаметр'
}]
});
meaningful.build([ 'Юпітер {є примірник} планета', 'планета {є} куля' ]);
expect(meaningful.query('Якою {_} {є} обсяг {чого} Юпітер')).toEqual([ 1530597322872155.8 ]);

то ситуація відрізняється, так як:
  • Опції прямо відповідають питань природної мови "Який діаметр планети?" або "Який об'єм кулі?"
  • Ці питання можуть бути використані для інтеграції з іншими додатками/інтерфейсами/функціями. Наприклад, даний код може моделювати ситуацію, коли у нас є (а) додаток з даними про планетах, (б) додаток, яке може розрахувати обсяг різних геометричних об'єктів. Машина виводу (reasoning engine) тепер може відповісти на питання "Який обсяг Юпітера?"
  • Ці питання можуть бути використані пошуковими системами і інтерфейсом природної мови
  • Можливо використовувати по-іншому (override) міркування, властиві кодом. Тобто, ми можемо більш гнучко використовувати схожість (similarity), не змінюючи їх. Наприклад, ми можемо застосувати формулу об'єму кулі для грушоподібних об'єктів, яке, швидше за все, не будуть успадковані від кулястих об'єктів. Але, якщо нас влаштовує апроксимація такого об'єкта з кулею (або апроксимація верхній і нижній частині цього об'єкта з кулями), ми можемо використовувати схожість і підрахувати обсяг грушоподібної тіла як кулі. Звичайно, те ж саме може бути зроблено за допомогою шаблону проектування (таким як Адаптер). Однак проблема полягає в тому, що ми не можемо передбачити всі можливі сценарії використання, коли схожість буде використана, і не можемо включити їх в код завчасно.
  • Ми можемо використовувати підходи, які не притаманні мові, який ми використовуємо. Насправді, розширена схожість може бути розглянута як різновид динамічної типізації. Або ж ми можемо по суті реалізувати щось подібне мультиметоду.
  • Ми можемо використовувати локальні міркування, які характерні тільки для даного твердження. Наприклад, схожість грушоподібної об'єкта з кулею може поширюватися тільки на одне твердження, а не фігурувати як загальна істина.
І це все? Не зовсім. Тепер ми можемо вступити в області, яких немає в основних мовах програмування, наприклад, причина-наслідок. Уявіть, що ми маємо справу з інструкцією, як встановити операційну систему OS_like_OS. Сучасне ПО розглядає таку інструкцію як текстову документацію. Але ми можемо вважати, що це набір причин-наслідків установки ОС. І в цьому випадку ми можемо отримати відповідь на питання "Як встановити OS_like_OS?" безпосередньо:
var text = [
'Завантажити образ нашого сайту',
'Запишіть цей образ на USB або оптичний диск',
'Завантажте комп'ютер з ним',
'Дотримуйтесь інструкцій на екрані'
];
_.each(text, function(t) {
meaningful.build('встановити {що робить} OS_like_OS {є наслідком}' + t);
});
var result = meaningful.query ('{_ @причина} встановити{що робить} OS_like_OS');
expect(result).toEqual(text);

Дуже просто для міркувань? Але це тільки початок. Адже при відповіді на подібні питання ми можемо оперувати не тільки причинами-наслідками, але умовами та іншими відносинами. Ви можете подивитися приклад тесту, який робить запит шляху до функціональності з альтернативами і умовами. Це більше, ніж документація. Це пояснення і карта залежностей між компонентами програми, яка може бути переиспользована, в тому числі для відповіді на численні запитання "Як?" і "Чому?".
Схожа ситуація і з обробкою помилок. Сьогодні помилки у кращому випадку являють собою читаються повідомлення зі стеком викликів, які можуть підказати, що ж сталося. У гіршому випадку це може бути повідомлення про помилку виконання без всяких підказок. Що розмітка сенсу і її інтеграція з кодом можуть поліпшити — так це допомогти краще роз'яснити, що ж відбулося насправді, а, можливо, і допомогти виправити ситуацію. Давайте подивимося на прикладі функції, яка додає елементи в список:
// Опція, яка дозволяє/забороняє виконання даної функціональності.
var addEnabled = true;
var planets = [];

meaningful.register({
func: function(list, element) {
if (addEnabled)
eval(list + '.push(\" + element + '\')');
else
// Якщо опція дорівнює false, то впадає виключення
throw "додавання заборонено";
},
question: 'додати {} елемент {} {} список',
input: [ { name: 'list' }, { name: 'елемент' } ],
error: function(err) {
if (err === 'додавання заборонено')
// Перехватываем повідомлення виключення і перетворимо його в переиспользуемую рекомендацію
return 'дозволити {} додавання';
}
});

// Рекомендація відповідає функції, яка включає опцію
meaningful.register({
func: function(list, element) { addEnabled = true; },
question: 'дозволити {} додавання'
});

meaningful.build([ 'planets {є екземпляром} список' ]);
meaningful.build([ 'Земля {є екземпляром} елемент' ]);
meaningful.build([ 'Юпітер {є екземпляром} елемент' ]);

meaningful.query('додати {} Земля {} {} planets', { execute: true });
// Додавання Землі в список успішно, т. к. опція дорівнює true
expect(planets).toEqual([ 'Земля' ]);
// Потім ми вимикаємо опцію.
addEnabled = false;
// Та таке додавання не проходить
meaningful.query('додати {} Юпітер {} {} planets', { execute: true, error: function(err) {
// Але ми перехватываем рекомендацію і "виконуємо" її, що знову включає опцію
meaningful.query(err, { execute: true });
}});
expect(planets).toEqual([ 'Земля' ]);
meaningful.query('додати {} Юпітер {} {} planets', { execute: true });
// Це робить наступне додавання успішним
expect(planets).toEqual([ 'Земля', 'Юпітер' ]);

Як ми можемо бачити, результатом обробки помилки може бути не тільки повідомлення і стек виклику, але і комплекс причин-наслідків, станів змінних, тощо, все, що може допомогти відповісти на питання "Чому сталася ця помилка?". Щоб це було можливим, підготовка повинна починатися ще на етапі формулювання вимоги, коли ми можемо конструювати макети фактів на основі текстового опису задачі. Наприклад, ця розмітка:
діаметр {чого} Юпітер {має значення} 142984

4.1887902047864 {є значенням} обсяг {чого} кулі {має} діаметр {має значення} 2
1530597322872155.8 {є значенням} обсяг {чого} кулі {має} діаметр {має значення} 142984

може розглядатися як макет і очікування функцій getPlanetDiameter() і getBallVolume(). На основі цього можуть бути згенеровані справжні макет-функції:
function getDiameterOfPlanet(planet) {
if (planet === 'Юпітер')
return 142984;
}

function getVolumeOfBall(diameter) {
if (diameter === 2)
return 4.1887902047864;
if (diameter === 142984)
return 1530597322872155.8;
}

Такі макет-функції дозволяють міркувати (наприклад, допомогти обчислити обсяг Юпітера), що може допомогти оцінити, як майбутнє додаток може "вбудуватися" у вже існуючу екосистему даних і коду. Далі розробник вже може замінити макет-функцію з реальним кодом. Тобто, завдяки відповідності між вимогами кодом, тестами, користувальницьким інтерфейсом, документацією (яке підтримується за допомогою компонируемых конструкцій природної мови), ми можемо працювати з обмеженою макет-функцією, реальною функцією, відповідною частиною інтерфейсу та документації схожим чином. Що дозволяє ще більше скоротити цикл очікування між різними видами інженерної активності, що стосується і міркувань: для того, щоб з ними працювати, нам не потрібно чекати повної імплементації.
Природно, що для етапу виконання критичне питання продуктивності, але, на відміну від Семантичного Вебу, який прагне побудувати Гігантський Глобальний Граф даних, ваш додаток може бути обмежена тільки власними даними. Що не призведе до більш гнучким і глобальних висновків (т. до. ми обмежені тільки нашим додатком), але і буде більш визначеним і перевіряється (т. до. ми обмежені тільки нашим додатком).
Отже, зможе міркувати ваш код? Точніше кажучи, скоріше це буде робити не сам код, а машини виводу на основі вашого коду, але це можливо. Хоча б тому, що ми можемо бути мотивовані поліпшенням розробки додатків, за рахунок того, що можна буде краще перевірити відповідність коду з вимогами, за рахунок того, що можна буде простіше знайти, що ж надає даний додаток або бібліотека (т. к. їх функціональність доступна у вигляді розмітки сенсу), за рахунок того, що можна зробити більш явними причинно-наслідкові ланцюжки як для логіки додатка, так і для тієї, яка призвела до помилки.
Що ж стосується дуалізму невизначеності і детермінізму, можливо нам потрібно припинити намагатися поєднати непоєднуване. Робота будь-якої програми може бути представлена у вигляді (а) тесносвязанного (tightly coupled), статично типізованого коду (з підвищеними вимогами до надійності, безпеки, ефективності для конкретних сценаріїв) і (б) слабосвязанних, динамічно типізованих і компонентних конструкцій природної мови (без вимог надійності, безпеки, які повинні бути врегульовані рівнем додатків, і які можуть бути гнучко застосовані для широкого діапазону сценаріїв). Чи варто намагатися робити більш гнучкими конструкції, які оптимізовані для детермінізму? Чи варто намагатися робити більш продуктивним конструкції, які орієнтуються скоріше на невизначеність? Швидше за все немає і немає. Додатки повинні бути вузькоспеціалізованими, природний мову адаптуватися під будь-які умови, кожному своє.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.