Статьи

Составление нескольких асинхронных результатов с помощью Applicative Builder в Java 8

Несколько месяцев назад я выпустил публикацию, в которой подробно объясняю предложенную мной абстракцию под названием Outcome , которая помогла мне ОЧЕНЬ много писать без побочных эффектов за счет применения семантики . Следуя этому простому (и в то же время мощному) соглашению, я в конечном итоге превратил любой тип сбоя (иначе, исключение) в явный результат функции, что значительно упрощает процесс рассуждения. Я не знаю вас, но мне надоело иметь дело с исключениями, которые все унизили, поэтому я кое-что с этим сделал, и, честно говоря, это сработало очень хорошо Поэтому, прежде чем я продолжу рассказывать истории из окопов , я действительно рекомендую просмотреть этот пост. Теперь давайте решим некоторые асинхронные проблемы, используя эксцентричные аппликативные идеи.

Что-то нечестивое таким образом приходит

Жизнь была действительно хорошей, наше кодирование было быстрым, чистым и сочетаемым, как всегда, но внезапно мы наткнулись на «недостающую» особенность (зло смеется, пожалуйста): нам нужно было объединить несколько асинхронных экземпляров Outcome в блокирующая мода…

о, Боже, почему

В восторге от идеи я взялся за работу. Я экспериментировал довольно много времени в поисках надежного и в то же время простого способа выражения подобных ситуаций; в то время как новый API ComposableFuture оказался намного приятнее, чем я ожидал (хотя я до сих пор не понимаю, почему они решили использовать такие имена, как applyAsync или thenComposeAsync вместо map или flatMap ), я всегда заканчивал реализацией слишком многословными и повторяющимися сравнениями на некоторые вещи, которые я делал со Scala , но после нескольких долгих сессий « Mate » у меня было «Привет! момент »: почему бы не использовать нечто похожее на аппликатив ?

Эта проблема

Предположим, что у нас есть эти два асинхронных результата:

1
2
3
4
5
CompletableFuture<Outcome<String>> textf =
    completedFuture(maybe("And the number is %s!"));
 
CompletableFuture<Outcome<Integer>> numberf =
    completedFuture(maybe(22));

и глупая сущность под названием Сообщение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public static class Message{
 
    private final String _text;
    private final Integer _number;
 
    private Message(String msg, Integer number){
        _text = msg;
        _number = number;
    }
 
    public String getContent(){
        return String.format(_text,_number);
    }
}

Мне нужно что-то, что с учетом textf и numberf это вернет мне что-то вроде

1
2
//After combining textf and numberf
CompletableFuture<Outcome<Message>> message = ....

Поэтому я написал письмо Деду Морозу:

  1. Я хочу асинхронно форматировать строку, возвращаемую textf, используя число, возвращаемое numberf, только когда оба значения доступны, это означает, что оба фьючерса успешно завершены, и ни один из результатов не завершился неудачей. Конечно, мы должны быть неблокирующими.
  2. В случае сбоев, я хочу собрать все сбои, которые произошли во время выполнения textf и / или numberf, и вернуть их вызывающей стороне, опять же, вообще не блокируя.
  3. Я не хочу быть ограниченным количеством значений, которые нужно объединить, он должен быть способен обрабатывать достаточное количество асинхронных результатов. Я сказал без блокировки? Там вы идете …
  4. Не умереть во время попытки.

waaat

Аппликативный строитель на помощь

Если вы думаете об этом, один простой способ выразить то, что мы пытаемся достичь, заключается в следующем:

1
2
// Given a String -> Given a number -> Format the message
f: String -> Integer -> Message

Проверяя определение f , он говорит что-то вроде: «Учитывая String , я верну функцию, которая принимает Integer в качестве параметра, который при применении возвращает экземпляр типа Message », таким образом, вместо ожидания всех чтобы значения были доступны сразу, мы можем частично применить одно значение за раз, получая реальное описание процесса построения экземпляра Message . Это звучало замечательно.

Чтобы достичь этого, было бы действительно здорово, если бы мы могли взять лямбда- сообщение конструкции : new и curry it, boom !, done !, но на Java это невозможно (сделать в общем, красивом и кратком виде), так что для ради нашего примера я решил использовать наш любимый шаблон Builder , который вроде как делает свою работу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public static class Builder implements WannabeApplicative<Message> {
 
    private String _text;
    private Integer _number;
 
    public Builder text(String text){
        _text=text;
        return this;
    }
 
    public Builder number(Integer number){
        _number=number;
        return this;
    }
 
    @Override
    public Message apply() {
        return new Message(_text,_number);
    }
}

А вот определение WannabeApplicative <T>:

1
2
3
4
public interface WannabeApplicative<V>
{
    V apply();
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static class CompositionSources<B>
{
    private CompositionSources(){ }
 
    public interface Partial<B>
    {
        CompletableFuture<Outcome<B>> apply(CompletableFuture<Outcome<B>> b);
    }
 
    public interface MergingStage<B, V>{
        Partial<B> by(BiFunction<Outcome<B>, Outcome<V>, Outcome<B>> f);
    }
 
    public <V> MergingStage<B, V> value(CompletableFuture<Outcome<V>> value){
 
        return f -> builder
                 -> builder.thenCombine(value, (b, v) -> f.apply(b, v)
                                                          .dependingOn(b)
                                                          .dependingOn(v));
    }
 
    public static <B> CompositionSources<B> stickedTo(Class<B> clazz)
    {
        return new CompositionSources<>();
    }
}

Во-первых, у нас есть два функциональных интерфейса: один — Partial <B> , который представляет собой отложенное применение значения для компоновщика , а второй, MergingStage <B, V> , представляет «как» комбинировать и строитель, и ценность . Затем у нас есть метод с именем value, который, учитывая экземпляр типа CompletableFuture <Outcome <V >> , будет возвращать экземпляр типа MergingStage <B, V> , и верить или нет, вот где происходит волшебство , Если вы помните определение MergingState , вы увидите, что это BiFunction , где первый параметр имеет тип Outcome <B>, а второй — типа Outcome <V> . Теперь, если вы следуете за типами, вы можете сказать, что у нас есть две вещи: частичное состояние процесса сборки с одной стороны (параметр типа B) и новое значение, которое необходимо применить к текущему состоянию компоновщика (введите параметр V), чтобы при применении он генерировал новый экземпляр компоновщика со «следующим состоянием в последовательности компоновки», которое представлено Partial <B> . И последнее, но не менее важное: у нас есть метод stickedTo , который по сути является хаком (ужасной java), чтобы придерживаться определенного аппликативного типа (построителя) при определении шага сборки. Например, имея:

1
CompositionSources<Builder> sources = CompositionSources.stickedTo(Builder.class);

Я могу определить приложения с частичным значением для любого экземпляра Builder следующим образом:

1
2
3
4
5
6
7
8
9
//What we're gonna do with the async text when available
Partial<Builder> textToApply =
    sources.value(textf)
            .by((builder, text) -> builder.flatMapR(b -> text.mapR(b::text)));
 
//Same thing for the number
Partial<Builder> numberToApply =
    sources.value(numberf)
            .by((builder, number) -> builder.flatMapR(b -> number.mapR(b::number)));

Обратите внимание, что мы еще ничего не создали, мы только что описали, что мы хотим сделать с каждым значением, когда придет время , мы могли бы выполнить некоторые проверки перед использованием нового значения (вот когда Outcome играет важную роль) или просто использовать это как есть, это действительно зависит от нас, но главное, что мы еще ничего не применили. Для того, чтобы сделать это и, наконец, сжать все свободные концы, я предложил другое определение, которое выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class FutureCompositions<V , A extends WannabeApplicative<V>>{
 
    private final Supplier<CompletableFuture<Outcome<A>>> _partial;
 
    private FutureCompositions(Supplier<CompletableFuture<Outcome<A>>> state)
    {
        _partial=state;
    }
 
    public FutureCompositions<V, A> binding(Partial<A> stage)
    {
        return new FutureCompositions<>(() -> stage.apply(_partial.get()));
    }
 
    public CompletableFuture<Outcome<V>> perform()
    {
        return _partial.get().thenApply(p -> p.mapR(WannabeApplicative::apply));
    }
 
    public static <V, A extends WannabeApplicative<V>> FutureCompositions<V, A> begin(A applicative)
    {
        return new FutureCompositions<>(() -> completedFuture(maybe(applicative)));
    }
}

Надеюсь, это не так уж сложно, но я постараюсь разбить это как можно яснее. Чтобы начать указывать, как вы собираетесь объединить все это вместе, вы начнете с вызова begin с экземпляром типа WannabeApplicative <V> , который, в нашем случае, параметр типа V равен Builder .

1
FutureCompositions<Message, Builder> ab = begin(Message.applicative())

Обратите внимание, что после вызова begin вы получите новый экземпляр FutureCompositions с лениво оцененным частичным состоянием внутри, что делает его единственным владельцем всего состояния процесса строительства, и это было конечной целью всего, что мы мы сделали это до сих пор, чтобы полностью получить контроль над тем, когда и как все будет сочетаться. Далее мы должны указать значения, которые мы хотим объединить, и для этого предназначен метод привязки :

1
2
ab.binding(textToApply)
  .binding(numberToApply);

Именно так мы предоставляем нашему экземпляру компоновщика все значения, которые необходимо объединить вместе со спецификацией того, что должно происходить с каждым из них, используя наши ранее определенные частичные экземпляры. Также обратите внимание, что все по-прежнему лениво оценивается, пока ничего не произошло, но все же мы сложили все «шаги», пока не решим, наконец, материализовать результат, который произойдет, когда вы вызовете « execute» .

1
CompletableFuture<Outcome<Message>> message = ab.perform();

С того самого момента все развернется, каждый этап сборки будет оценен, где ошибки могут быть возвращены и собраны в экземпляре Outcome или просто новые доступные значения будут предоставлены целевому экземпляру компоновщика, так или иначе, все шаги будут исполняться, пока ничего не будет сделано. Я постараюсь изобразить то, что только что произошло, следующим образом

аппликативны

Если вы обратите внимание на левую сторону рисунка, вы можете легко увидеть, как каждый шаг «определен», как я показал ранее, следуя направлению стрелки «декларация», то есть, как вы на самом деле описали процесс строительства. Теперь, с того момента, как вы вызываете execute , каждый аппликативный экземпляр (помните, в нашем случае, Builder ) будет лениво оцениваться в противоположном направлении: он начнется с оценки последнего указанного этапа в стеке, который затем перейдет к оценке следующего один и так далее вплоть до того момента, когда мы достигнем «начала» определения здания, где оно начнет разворачиваться или развертывать оценку каждый шаг до самого верха, собирая все, что может, используя спецификацию MergingStage .

И это только начало….

Я уверен, что многое можно сделать, чтобы улучшить эту идею, например:

  • Два последовательных вызова метода зависящего от CompositionSources.values ​​() отстой , слишком многословный, на мой вкус, я должен что-то с этим поделать.
  • Я не совсем уверен, что буду продолжать передавать экземпляры Outcome в MergingStage , это будет выглядеть чище и проще, если мы развернем значения для объединения перед его вызовом и просто вместо этого вернем Either <Failure, V> — это уменьшит сложность и увеличит гибкость на то, что должно происходить за кулисами.
  • Хотя использование шаблона Builder сделало свою работу, мне кажется, что это старая школа , я бы с удовольствием запросто карриировал конструкторы, поэтому в моем списке дел стоит проверить, есть ли у jOOλ или Javaslang что-то, что можно предложить по этому вопросу.
  • Лучший вывод типа, так что любой ненужный шум удаляется из кода, например, метода stickedTo , это действительно запах кода, то, что я ненавидел с самого начала. Определенно нужно больше времени, чтобы найти альтернативный способ вывести аппликативный тип из самого определения.

Пожалуйста, присылайте мне любые ваши предложения и комментарии. Ура и помни …

показатель