Почему «застежка-молния» игнорирует свисающий хвост коллекции?

12

C # , Scala, Haskell, Lisp и Python ведут себя одинаково zip: если одна коллекция длиннее, хвост молча игнорируется.

Это может быть и исключение, но я не слышал ни о каком языке, использующем этот подход.

Это озадачивает меня. Кто-нибудь знает причину, почему так zipзадумано? Я предполагаю, что для новых языков это сделано, потому что другие языки делают это таким образом. Но какова была основная причина?

Я задаю здесь фактический, исторический вопрос, а не о том, нравится ли это кому-то или это хороший или плохой подход.

Обновление : если бы меня спросили, что делать, я бы сказал - сгенерировать исключение, почти так же, как индексирование массива (несмотря на то, что «старые» языки делали все возможное, как обрабатывать индекс за пределами границ, UB, расширять массив, и т.д).

greenoldman
источник
10
Если бы он не игнорировал хвост, который имеет один функтор, использование бесконечных последовательностей было бы более громоздким. Особенно, если получить длину бесконечного диапазона было дорого / запутано / невозможно.
Дедупликатор
2
Вы, кажется, думаете, что это неожиданно и странно. Я нахожу это очевидным и, действительно, неизбежным. Что бы вы хотели, чтобы происходило, когда вы застегивали коллекции неравной длины?
Килиан Фот
@KilianFoth, получи исключение.
Гринольдман
@ Дедупликатор, хороший. С бесшумным опусканием хвоста вы можете выразить, zipWithIndexпредоставляя генератор натуральных чисел Теперь, только недостающая часть информации - то , что было его причиной? :-) (кстати. Пожалуйста, перепостите свой комментарий как ответ, спасибо).
Гринольдман
1
В Python есть файл itertools.izip_longest, который эффективно автоматически дополняет законченные вводы с помощью Nones. Я часто выбираю его, когда использую zip; Я не могу вспомнить причины, по которым стоит выбор. В Python уже есть enumerate () для случая @ greenoldman, который я часто использую.
StarWeaver

Ответы:

11

Это почти всегда то, что вы хотите, и когда это не так, вы можете сделать заполнение самостоятельно.

Основная проблема заключается в ленивой семантике, когда вы не знаете длину при первом запуске zip, поэтому вы не можете просто выбросить исключение в начале. Вам нужно будет сначала вернуть все общие элементы, а затем сгенерировать исключение, которое не будет очень полезным.

Это также вопрос стиля. Императивные программисты привыкли вручную проверять граничные условия повсюду. Функциональные программисты предпочитают конструкции, которые не могут потерпеть неудачу в дизайне Исключения крайне редки. Если у функции есть способ вернуть разумное значение по умолчанию, функциональные программисты примут его. Композиционность - король.

Карл Билефельдт
источник
Я спрашиваю об исторических причинах, а не о том, что я могу сделать. Второй абзац - вы не правы, посмотрите, как zipв настоящее время реализовано. Исключением броска является просто изменение «стоп-доходности» на «бросок». Третий абзац - возврат пустого элемента для выхода за границы не может завершиться неудачей, но все же я сомневаюсь, что любой разработчик FP проголосовал бы за это, это хороший дизайн.
Гринольдман
3
Мой второй абзац относится не ко всем реализациям, только к действительно ленивым. Если вы zipобъединяете две бесконечные последовательности, вы не знаете размера в начале. На третий абзац я сказал разумный дефолт. Возвращение пустым в этом случае не было бы разумно, тогда как отбрасывание хвоста очевидно.
Карл Билефельдт
Ах, наконец, я понял вашу точку зрения - с использованием исключения на ленивом языке это не техническая замена, это полностью изменение поведения, потому что вам нужно сгенерировать исключение в самом начале, в то время как вы можете игнорировать хвост, когда это удобно.
Greenoldman
3
+1 это также отличный ответ: «Функциональные программисты предпочитают конструкции, которые не могут потерпеть неудачу при разработке», так красноречиво говорится о том, что является основным мотиватором, стоящим за большинством проектных решений, принимаемых функциональными программистами. У императивных программистов есть правило, которое им нравится: «Говори, не спрашивай», FP доводит это до N-й степени, делая упор на непрерывном сообщении инструкций, не требуя проверки результатов до самого последнего момента, поэтому мы стараемся обеспечить промежуточные шаги не может потерпеть неудачу, потому что Composability является королем. Очень хорошо сказано.
Джимми Хоффа
12

Потому что нет очевидного способа завершить хвост. Любой выбор того, как это сделать, приведет к неочевидному хвосту.

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

Если zip сделал это для вас, вы не могли бы знать, какие значения он заполнял интуитивно. Был ли цикл в списке? Это повторило бесполезное значение? Какое среднее значение для вашего типа?

Нет никакого смысла в том, что делает zip, который можно использовать, чтобы интуитивно понять, как удлинится хвост, поэтому единственное разумное, что нужно сделать, это работать с доступными значениями, а не придумывать что-то, чего ваш потребитель может не ожидать.


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

Хотя помните, что эта и многие другие общие функции стиля FP являются общими, потому что они просты и обобщены, так что вы можете настроить свой код, чтобы использовать их и получить желаемое поведение. Например, в C # вы могли бы просто

IEnumerable<Tuple<T, U>> ZipDefaults(IEnumerable<T> first, IEnumerable<U> second)
{
    return first.Count() < second.Count()
        ? first.Concat(Enumerable.Repeat(default(T), second.Count() - first.Count())).Zip(second)
        : first.Zip(second.Concat(Enumerable.Repeat(default(U), first.Count() - second.count())))
}

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

Джимми Хоффа
источник
Хорошо, но это только когда вы заставляете коллекции делать что-то, чтобы соответствовать другим - сравните это с индексацией коллекции (массива). Вы могли бы начать думать, должен ли я расширять и массив, если у меня есть индекс за пределами? Или, может быть, молча проигнорировать запрос. Но в течение некоторого времени существует распространенное представление об исключении. То же самое и здесь - если у вас нет соответствующей коллекции, выведите исключение. Почему этот подход не был принят?
Гринольдман
2
zipмог бы заполнить нулями, что часто является интуитивно понятным решением. Рассмотрим тип zip :: [a] -> [b] -> [(Maybe a, Maybe b)]. Конечно, тип результата немного ^ H ^ H, довольно непрактичный, но он позволил бы легко реализовать любое другое поведение (сокращение, исключение) поверх него.
Амон
1
@amon: Это совсем не интуитивно, это глупо. Это просто потребовало бы нулевой проверки каждого аргумента.
DeadMG
4
@amon не у каждого типа есть нуль, это то, что я имел в виду mempty, у объектов есть нуль, чтобы заполнить пространство, но вы хотите, чтобы он придумал такую ​​вещь для int и других типов? Конечно, C # имеет, default(T)но не все языки, и даже для C # это действительно очевидное поведение? Я так не думаю
Джимми Хоффа
1
@amon Вероятно, было бы более полезно вернуть неиспользованную часть длинного списка. Вы можете использовать это, чтобы проверить, были ли они равными по длине после факта, если вам нужно, и все еще можете повторно застегнуть молнию или сделать что-то с неиспользованным хвостом, не пересекая список.
Доваль