Зачем нужна неизменяемая очередь?

Я работаю с Java уже несколько лет. Недавно наткнулся на Vavr, функциональную библиотеку для Java, предоставляющую API неизменяемых коллекций. Мне любопытно узнать причину наличия неизменной очереди.

Насколько я понимаю, очередь используется для создания данных на одном конце, а затем другой поток потребляет данные с другого конца.

Immutable Queue не позволяет вам добавлять данные после его построения, тогда зачем мне здесь использовать Queue.

В идеале я бы обработал очередь, как показано ниже, но для неизменной очереди это переходит в бесконечный цикл.

while(!queue.isEmpty()) {
    queue.dequeue(); // process elements in queue.
}

Когда я гуглил, все обсуждения касались того, как реализовать неизменяемую очередь, но не объясняли ее необходимость.


person Naresh Babu    schedule 15.07.2017    source источник
comment
Хороший вопрос, но он больше подходит для SoftwareEngineering.   -  person Dioxin    schedule 15.07.2017
comment
предположительно это может быть более эффективным: blogs.msdn.microsoft.com/ericlippert/2007/12/10/   -  person tima    schedule 15.07.2017
comment
@tima Опять же, блог показывает нам эффективную реализацию неизменяемой очереди. На самом деле очередь Vavr состоит из двух отдельных списков для поставленных в очередь и исключенных из очереди элементов. Но у меня до сих пор нет ответа на мой вопрос: какую проблему решает неизменяемая очередь?   -  person Naresh Babu    schedule 15.07.2017
comment
@VinceEmigh, ссылаясь на другие сайты, часто полезно указать, что кросс-постинг не одобряется   -  person gnat    schedule 15.07.2017


Ответы (2)


Насколько я понимаю, очередь используется для создания данных на одном конце, а затем другой поток потребляет данные с другого конца.

Очередь представляет собой структуру данных FIFO (First In-First-Out). Он имеет множество применений, помимо связи между потоками.

Мне любопытно узнать причину наличия неизменной очереди.

Если вас сбивает с толку необходимость неизменного что угодно, похоже, вы не понимаете функциональное программирование. Помните, вы сами говорили, что Vavr — это функциональная библиотека, т.е. библиотека для написания функционального кода на Java.

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

Это включает в себя очередь. Если вам нужна очередь, то есть коллекция FIFO, для хранения ваших данных, то она тоже должна быть неизменной.

В качестве примера предположим, что вы хотите добавить числа от 1 до 10 в очередь, а затем прочитать из этой очереди и распечатать значения.

В императивном языке программирования, таком как Java, вы должны сделать это следующим образом, используя java.util.Queue и такую ​​реализацию, как java.util.LinkedList:

// Build queue with numbers 1-10
Queue<Integer> queue = new LinkedList<>();
for (int i = 1; i <= 10; i++)
    queue.offer(i);

// Poll queue and print numbers
for (Integer num; (num = queue.poll()) != null; )
    System.out.println(num);

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

Помните, что в императивном стиле счетная переменная i и очередь queue изменяются во время итерации.

В функциональном стиле они оба должны быть неизменяемыми, поэтому вы делаете это, написав рекурсивную функцию, подобную этой (на Java), используя io.vavr.collection.Queue:

private static Queue<Integer> build(int i, int end, Queue<Integer> queue) {
    if (i > end)
        return queue;
    return build(i + 1, end, queue.enqueue(i));
}

Затем назовите это:

// Build queue with numbers 1-10
Queue<Integer> queue = build(1, 10, Queue.empty());

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

Примечание: в функциональном языке, который реализует оптимизацию хвостовой рекурсии (в Java ее нет), функция build() выше фактически не создает стек вызовов, поэтому она не вызовет переполнения стека. Кроме того, новая очередь, возвращаемая enqueue(), не копирует все существующие значения, так что это не так дорого, как кажется.

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

private static void print(Queue<Integer> queue) {
    if (queue.isEmpty())
        return;
    Tuple2<Integer,Queue<Integer>> t = queue.dequeue();
    System.out.println(t._1());
    print(t._2());
}

Здесь dequeue()< /a> возвращает два значения: значение, удаленное из очереди, и новая очередь с удаленным значением. Затем функция выводит значение и делает рекурсивный вызов для вывода остальной части очереди.

person Andreas    schedule 15.07.2017
comment
По спецификации Queue impls не всегда FIFO: Очереди обычно, но не обязательно, упорядочивают элементы в порядке FIFO (первым пришел — первым обслужен). Среди исключений — приоритетные очереди (из документации). Но я определенно вижу, где это могло бы сыграть роль с чем-то вроде vavr, поощряя программирование без сохранения состояния. +1, лучше, чем мой ответ ИМО, более конкретный случай - person Dioxin; 15.07.2017
comment
@VinceEmigh Вы имеете в виду javadoc Queue< /a> интерфейс в Java. Я говорю об абстрактной очереди, которая является чистой структурой FIFO. PriorityQueue не является структурой FIFO. - person Andreas; 15.07.2017
comment
Да, но языковой тег — java, а спецификации важны для целостности. Это то, что они выбрали для своей спецификации, и это практикуется в JCL. Но, как я уже сказал, мой ответ является обобщенным (с точки зрения Java) и может не применяться к требованиям, специфичным для парадигмы. Просто шел по спецификации, не хотел нарушать контракты - person Dioxin; 15.07.2017
comment
@VinceEmigh Но вопрос не в java.util.Queue. Речь идет о io.vavr.collection.Queue, который не имеет ничего общего с java.util.Queue, кроме названия очереди. - person Andreas; 15.07.2017
comment
Если идея состоит в том, чтобы просто сохранить набор элементов и передать его другой программе для их обработки, то мы могли бы использовать список или набор. Почему именно Queue здесь? Я вижу преимущество наличия неизменяемого списка/набора. - person Naresh Babu; 15.07.2017
comment
@NareshBabu В функциональном программировании или, по крайней мере, в библиотеке Vavr, List — это стек, то есть структура LIFO. Этот вопрос конкретно касается Queue, структуры FIFO. Вопрос касается библиотеки функционального программирования Vavr и не имеет ничего общего с интерфейсами/классами java.util Collection. Это вопрос о функциональном программировании, которое сильно отличается от императивного программирования, к которому привыкли Java-программисты. - person Andreas; 15.07.2017
comment
Не могу с этим поспорить. @NareshBabu List не не гарантирует FIFO по спецификации, равно как и Set. Требования отличаются - person Dioxin; 15.07.2017
comment
Хорошо. Теперь мне интересно, какая структура данных будет использоваться в функциональном программировании для достижения структуры FIFO, совместно используемой несколькими потоками, и требование состоит в том, чтобы более одного потока не обрабатывали один и тот же элемент. - person Naresh Babu; 15.07.2017
comment
@NareshBabu Очевидно, вы не делаете этого, используя Queue. :-) Если вы подумаете об этом, вы поймете, что использование (блокирующей) очереди для отправки элементов в один или несколько запущенных потоков-потребителей на самом деле является просто способом, чтобы выделенные потоки выполняли работу, потоки, которые остаются бездействующими, когда очередь пуста. . Вместо использования очереди отправка задач в пул потоков будет иметь тот же эффект. Таким образом, отправка задач исполнителю является функциональным эквивалентом отправки элементов через очередь в поток обработки, и это также лучше, поскольку позволяет потокам делать другие вещи, если это необходимо. - person Andreas; 15.07.2017

Нет нужды. Это бесполезно. Обычно люди задают вопросы, когда упоминают об этом. in-java#comment352902_183766">1, 2

Из документа:

Коллекция, предназначенная для удержания элементов перед обработкой. Помимо основных операций сбора, очереди предоставляют дополнительные операции вставки, извлечения и проверки.

Это операции element()/peek(), poll()/remove() и add(E)/offer(E) соответственно.

Извлечение и проверка — это операции мутации, поэтому у нас остались element() и peak(). Эти операции работают с головой очереди, делая следующие записи недоступными из-за поведения, специфичного для Queue.

Методы element() и peek() возвращают, но не удаляют заголовок очереди.


Насколько я понимаю, очередь используется для создания данных на одном конце, а затем другой поток потребляет данные с другого конца.

Вы обычно использовали бы BlockingQueue для чего-то подобного. В спецификации Queue прямо указано:

Интерфейс Queue не определяет блокирующие методы очереди, которые распространены в параллельном программировании. Эти методы, ожидающие появления элементов или освобождения места, определены в интерфейсе BlockingQueue, который расширяет этот интерфейс.

person Dioxin    schedule 15.07.2017