Может ли рассуждать ваш код?

в 19:13, , рубрики: Программирование, программная инженерия, рассуждения

Когда мы думаем о рассуждениях (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), статически типизированного кода (с повышенными требованиями к надежности, безопасности, эффективности для конкретных сценариев) и (б) слабосвязанных, динамически типизированных и компонентных конструкций естественного языка (без требований надежности, безопасности, которые должны быть урегулированы уровнем приложений, и которые могут быть гибко применены для широкого диапазона сценариев). Стоит ли пытаться делать более гибкими конструкции, которые оптимизированы для детерминизма? Стоит ли пытаться делать более производительным конструкции, которые ориентируются скорее на неопределенность? Скорее всего нет и нет. Приложения должны быть узкоспециализированными, естественный язык адаптироваться под любые условия, каждому свое.

Автор: meaningful

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js