[Из песочницы] Стрелки как подход к представлению систем на Java

Часто встречается описание систем, алгоритмов и процессов в виде структурных схем. Следовательно, актуальна задача представления структурных схем, к примеру, из технической документации или спецификации, на языке программирования.


В статье рассматривается подход к представлению структурных схем с использованием концепции стрелок (arrows), описанных Джоном Хьюзом и нашедших применение в Haskell в FRP-фреймворках Yampa и Netwire, а также в XML-фреймворке Haskell XML Toolbox.


Особенностью структурных схем является наглядное представление последовательностей операций (блоков) без акцентирования внимания на самих обрабатываемых данных (переменных) и их состояниях. Для примера рассмотрим радиоприёмник прямого усиления


структурная схема приёмника


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


Традиционное описание такой схемы на C-подобном языке программирования выглядело бы примерно так


// Создаём блоки обработки
Antenna antenna = new Antenna(Ether.getInstance());
Filter filter1 = new Filter(5000);      // параметр - частота настройки
Filter filter2 = new Filter(5000);
Filter filter3 = new Filter(5000);

Detector detector = new Detector("AM"); // тип модуляции - амплитудная
Amplifier amp = new Amplifier(5);       // коэффициент усиления
Speaker speaker = new Speaker(10);      // громкость

Signal inputSignal = antenna.receive();

# Описываем связи между блоками
Signal filter1Res = filter1.filter(inputSignal);
Signal filter2Res = filter2.filter(filter1Res);
Signal filter3Res = filter3.filter(filter2Res);
Signal detected = detector.detect(filter3Res);
Signal amplified = amp.amplify(detected);

speaker.speak(amplified);

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


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



Receiver receiver = Receiver.join(filter1).join(filter2).join(filter3)
    .join(detector).join(amp).join(speaker);

receiver.apply(antenna.receive());

Метод join() описывает последовательное соединение блоков, то есть a.join(b) означает, что результат обработки блоком a будет передан на вход блока b. При этом лишь требуется, чтобы соединяемые классы Filter, Amplifier, Detector, Speaker дополнительно реализовывали метод apply(), выполняющий «действие по умолчанию» (для фильтра Filter — filter(), для Amplifier — amplify() и т. д.) и позволяющий вызывать объект как функцию.


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


Receiver receiver = Receiver.join(filter(5000)).join(filter(5000)).join(filter(5000))
    .join(detector("AM")).join(amplifier(5)).join(speaker(10));

receiver.apply(antenna.receive());

Стрелки как способ описания вычислений


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


Стрелки (arrows) также являются комбинатором и позволяют обобщенно описывать составные вычисления. В этой статье используется реализация стрелок jArrows, написанная на Java 8.


Что такое стрелка


Стрелка Arrow a от функции Out f(In x) представляет вычисление, которое выполняется функцией f. Как вы уже могли догадаться In — тип входного значения стрелки (принимаемого функцией f), Out — тип выходного значения (возвращаемого функцией f). Преимуществом представления вычислений в виде стрелок является возможность явного комбинирования вычислений различными способами.


Например вычисление y = x * 5.0, на Java представленное функцией


double multBy5_0(int in) { 
    return in*5.0; 
}

можно представить в виде стрелки


Arrow arrMultBy5_0 = Action.of(multBy5_0);

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


Композиция стрелок


Стрелку arrMultBy5_0 можно последовательно соединить с другой стрелкой — например, прибавляющей к входному значению 10.5, а затем — со следующей стрелкой, представляющей результат в виде строки. Получится цепочка из стрелок


Arrow mult5Plus10toStr = arrMultBy5_0.join(in -> in+10.5)
                                                      .join(in -> String.valueOf(in));
mult5Plus10toStr.apply(10);      //  "60.5"                  

Получившееся вычисление, представленное составной стрелкой mult5Plus10toStr, можно представить в виде структурной схемы:


098ac75fa52643d784ac84cb5b5820ac.png

Вход этой стрелки имеет тип Integer (входной тип первого вычисления в цепочке), а выход имеет тип String (выходной тип последнего вычисления в цепочке).


Метод someArrow.join(g) соединяет в цепочку вычисление, представленное стрелкой someArrow с вычислением, представленным g, при этом g может быть другой стрелкой, лямбда-функцией, методом, или чем-то ещё, что реализует интерфейс Applicable с методом apply(x), который можно применить к входному значению x.


Несколько упрощенная реализация join
class Action implements Arrow, Applicable {
    Applicable func;

    public Arrow join(Applicable b) {
        return Action.of(i -> b.apply(this.func.apply(i)));
    }
}

Здесь In — тип входных данных стрелки a, OutB — тип выходных данных b, и он же — тип выходных данных получившейся новой составной стрелки a_b = a.join(b), Out — тип выходных данных стрелки a, он же — тип входных данных стрелки b.


Функция func хранится в экземпляре стрелки, инициализируется при её создании и выполняет само вычисление. Аргумент b поддерживает интерфейс Applicable и может быть другой стрелкой или функцией, поэтому мы просто применяем b к результату применения a.func(i) к входным данным i стрелки a_b. Сам входные данные будут переданы при вызове apply составной стрелки a_b, так что a_b.apply(x) вернёт результат вычисления b.func(a.func(x)).


Другие способы композиции стрелок


Кроме последовательного соединения методом join стрелки можно соединять параллельно методами combine, cloneInput и split. Пример использования метода combine для описания вычисления sin(x)^2+cos(x)^2


Arrow, Pair> 
    sin_cos = Action.of(Math::sin).combine(Math::cos);

Arrow sqr = Action.of(i -> i*i);

Arrow, Double> sum_SinCos = sin_cos.join(sqr.combine(sqr))
                            .join(p -> p.left + p.right);

sum_SinCos.apply(Pair.of(0.7, 0.2));    // 1.38

70d843488c1f462db6a5bca4ebdee89b.png

Получившаяся «широкая» стрелка sin_cos принимает на вход пару значений типа Pair, первое значение pair.left пары попадает на вход первой стрелки (функция sin), второе pair.right — на вход второй стрелки (функция cos), их результаты тоже объединяются в пару. Следующая составная стрелка sqr.combine(sqr) принимает на вход значение типа Pairи возводит оба значения пары в квадрат. Последняя стрелка суммирует результат.


Метод someArrow.cloneInput(f) создаёт стрелку, параллельно соединяя someArrow и f и применяя их к входу, её выход представляется в виде пары, объединяющей результаты вычилений этих стрелок. Входные типы someArrow и f должны совпадать.


Arrow> sqrAndSqrt = Action.of((Integer i) -> i*i)
                            .cloneInput(Math::sqrt); 
sqrAndSqrt.apply(5);  // Pair(25, 2.236)

147b5d464d89480fba966bc32d6e5969.png

Параллельное соединение в данном случае означает, что результаты двух вычислений, соединённых параллельно, не зависят друг от друга — в отличии от последовательного соединения методом join, когда результат одного вычисления передаётся на вход другого. Многопоточные параллельные соединения реализуется классом ParallelAction.


Метод someArrow.split(f, g) — дополнительный метод, эквивалентный someArrow.join(f.cloneInput(g)). Результат вычисления someArrow параллельно передаётся на вход f и g, выходом такой стрелки будет пара с результатами вычислений f и g.


Обход вычислений


Иногда нужно передать часть входного значения стрелки далее по цепочке вместе с результатом вычисления. Это реализуется методом someArrow.first() и дополняющим его someArrow.second(), преобразующим стрелку someArrow так, что получившаяся стрелка принимает на вход пару значений и применяет вычисление лишь к одному из элементов этой пары


Arrow arr = Action.of(i -> Math.sqrt(i*i*i));

Pair input = Pair.of(10, 10);

arr.first().apply(Pair.of(10, 10)));     // Pair(31.623, 10)
arr.second().apply(Pair.of(10, 10)));    // Pair(10, 31.623)

984adb50d5b649b59bd19c33255dc498.png

ce5bc22b2bab4d84add1acc225476e74.png

Эти методы аналогичны методам someArrow.bypass2nd() и someArrow.bypass1st() соответственно.


Полнота описания


Согласно Хьюзу, описание любого вычисления возможно с использованием лишь трёх функций:


  • 1) Конструктора, строящего стрелку из функции (в данной реализации Action.of)
  • 2) Функции, последовательно соединяющей две стрелки (Arrow: join)
  • 2) Функции, применяющей вычисление к части входа (Arrow: first)

Реализация jArrows также расширена дополнительными методами, упрощающими описание систем.


Выводы


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


Как показано в статье Хьюза, стрелки, по-сути являющиеся реализацией описания систем в виде блок-схем в рамках функционального программирования, являются более обобщенным описанием, чем монады, которые уже получили распространение в мейнстриме, в частности в виде их реализации Optional в Java 8. В данной статье описаны основные принципы данного подхода, в дальнейшем представляет интерес адаптация существующих и разработка новых паттернов для применения стрелок в мейнстримовой разработке программного обеспечения.

Комментарии (0)

© Habrahabr.ru