Гарантируется ли последовательное выполнение функций более высокого порядка для коллекций?

В другом вопросе пользователь предложил написать такой код:

def list = ['a', 'b', 'c', 'd']
def i = 0; 
assert list.collect { [i++] } == [0, 1, 2, 3]

Такой код в других языках считается плохой практикой, потому что содержимое collect изменяет состояние своего контекста (здесь оно изменяет значение i). Другими словами, закрытие имеет побочные эффекты.

Такие функции более высокого порядка должны иметь возможность запускать замыкание параллельно и снова собирать его в новый список. Если обработка в замыкании длительная, интенсивно использующая процессор, возможно, стоит выполнять их в отдельных потоках. Было бы легко изменить collect для использования ExecutorCompletionService для этого, но это нарушит код выше.

Другой пример проблемы: если по какой-то причине collect просмотреть коллекцию, скажем, в обратном порядке, в этом случае результатом будет [3, 2, 1, 0]. Обратите внимание, что в этом случае список не был восстановлен, 0 действительно является результатом применения замыкания к «d»!

Интересно, что эти функции задокументированы как «Итерации по этой коллекции» в JavaDoc коллекции, который предполагает, что итерация является последовательной.

Определяет ли спецификация groovy порядок выполнения функций более высокого порядка, таких как collect или each? Приведенный выше код не работает или все в порядке?


person Antoine    schedule 11.11.2011    source источник


Ответы (2)


Мне не нравятся явные внешние переменные, на которые полагаются мои закрытия по причинам, которые вы указали выше.

Действительно, чем меньше переменных мне нужно определить, тем я счастливее ;-)

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

Что касается самого вопроса, если мы возьмем collect в качестве примера функции и рассмотрим исходный код, мы можем видеть, что с учетом Object (Collection и Map выполняются аналогичным образом с небольшими отличиями в том, как работает итератор. указанный) он выполняет итерацию по InvokerHelper.asIterator(self), добавляя результат каждого вызова закрытия в результирующий список.

InvokerHelper.asIterator (снова источник находится здесь) в основном вызывает метод iterator() для переданного объекта.

Итак, для списков и т. д., он будет перебирать объекты в порядке, определенном итератором.

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

Я думаю, что, спрашивая о спецификации Groovy, этот ответ может быть не тем, что вы хотите, но я не думаю, что ответ есть. У Groovy никогда не было «полной» спецификации (действительно, речь идет о Groovy, который некоторым не нравится).

person tim_yates    schedule 11.11.2011
comment
Обновлен мой ответ, чтобы добавить ссылки на различия между Collections, Lists и Objects, которые имеют метод iterator() ;-) - person tim_yates; 11.11.2011
comment
Вы разместили ссылку на исходный код в надежде, что люди заметят ваше имя в тегах автора? Никто не любит понты (на меня, правда, подействовало). - person Dónal; 11.11.2011
comment
Спасибо, я не думаю, что могу ожидать лучшего ответа. - person Antoine; 11.11.2011
comment
@ Антуан, ты мог бы попробовать список рассылки groovy-user - person tim_yates; 11.11.2011
comment
@ Дон хе-хе .. очень далеко в списке авторов ;-) - person tim_yates; 11.11.2011

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

Но в случае с each нет особого смысла сохранять функцию свободной от побочного эффекта, поскольку она ничего не сделает (на самом деле единственная цель этого метода — заменить действие в качестве цикла for-each). В документации Groovy есть несколько примеров использования each (и его вариантов, eachWithIndex и reverseEach), которые требуют порядок выполнения должен быть определен.

Теперь, с прагматической точки зрения, я думаю, что иногда можно использовать функции с некоторыми побочными эффектами в таких методах, как collect. Например, для преобразования списка в [index, value] пары transpose и диапазон можно использовать

def list = ['a', 'b', 'c']
def enumerated = [0..<list.size(), list].transpose()
assert enumerated == [[0,'a'], [1,'b'], [2,'c']]

Или даже inject

def enumerated = list.inject([]) { acc, val -> acc << [acc.size(), val] }

Но collect и счетчик тоже делают свое дело, и я думаю, что результат будет наиболее читаемым:

def n = 0, enumerated = list.collect{ [n++, it] }

Этот пример не имел бы смысла, если бы Groovy предоставил acollect и подобные методы с функцией index-value-param (см. проблема Jira), но это как бы показывает, что иногда практичность побеждает чистоту IMO :)

person epidemian    schedule 11.11.2011