Гарантируется ли выполнение функций более высокого порядка в коллекциях последовательно? - PullRequest
4 голосов
/ 11 ноября 2011

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

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

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

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

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

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

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

Ответы [ 2 ]

3 голосов
/ 11 ноября 2011

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

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

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

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

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

Таким образом, для Списков и т. Д. Он будет перебирать объекты в порядке, определенном итератором.

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

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

1 голос
/ 12 ноября 2011

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

Но в случае 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 предоставит collect и аналогичныеметоды с функцией index-value-param (см. выпуск Jira ), но это своего рода показывает, что иногда практичность превосходит чистоту IMO:)

...