Por que 'zip' ignora a cauda pendente da coleção?

12

C # , Scala, Haskell, Lisp e Python têm o mesmo zipcomportamento: se uma coleção for maior, a cauda será ignorada silenciosamente.

Também poderia ser uma exceção, mas não ouvi nenhum idioma usando essa abordagem.

Isso me intriga. Alguém sabe o motivo pelo qual zipé projetado dessa maneira? Eu acho que para novos idiomas, isso é feito porque outros idiomas fazem dessa maneira. Mas qual foi o principal motivo?

Estou fazendo aqui uma pergunta factual e histórica, não se alguém gosta, ou se é uma abordagem boa ou ruim.

Atualização : se me perguntassem o que fazer, eu diria - lance uma exceção, de maneira semelhante à indexação de uma matriz (apesar de linguagens "antigas" fazerem todo tipo de mágica, como lidar com índices fora dos limites, UB, expandir matriz, etc).

greenoldman
fonte
10
Se não ignorasse a cauda de um functor, usar sequências infinitas seria mais complicado. Especialmente se obter o comprimento do intervalo não infinito fosse caro / complicado / impossível.
Deduplicator 02/03
2
Você parece pensar que isso é inesperado e estranho. Acho óbvio e, de fato, inevitável. O que você gostaria que acontecesse ao compactar coleções de tamanho desigual?
Kilian Foth
@KilianFoth, receba uma exceção.
greenoldman
@Duplicator, nice. Com a queda silenciosa da cauda, ​​você pode expressar naturalmente o zipWithIndexfornecimento de gerador de números naturais. Agora, a peça faltando apenas de informação - o que era ele o motivo? :-) (btw. por favor, repense seu comentário como resposta, obrigado).
greenoldman
1
O Python possui itertools.izip_longest, que efetivamente autopads conclui entradas com Nones. Eu o escolho sobre o zip com frequência quando realmente uso o zip; Não me lembro mais das razões por trás de qualquer escolha. Python já enumerou () para o caso de @ greenoldman, que eu uso frequentemente.
StarWeaver 4/03/15

Respostas:

11

É quase sempre o que você deseja e, quando não é, pode fazer o preenchimento sozinho.

O principal problema é com a semântica preguiçosa que você não sabe o tamanho quando inicia o primeiro zip, portanto não pode simplesmente lançar uma exceção no início. Você precisaria primeiro retornar todos os elementos comuns e depois lançar uma exceção, o que não seria muito útil.

Também é uma questão de estilo. Programadores imperativos estão acostumados a verificar manualmente as condições de contorno em todo o lugar. Programadores funcionais preferem construções que não podem falhar por design. Exceções são extremamente raras. Se houver uma maneira de uma função retornar um padrão razoável, os programadores funcionais a aceitarão. Composability é rei.

Karl Bielefeldt
fonte
Estou perguntando sobre razões históricas, não o que posso fazer. Segundo parágrafo - você está errado, veja como zipé implementado atualmente. A exceção de lançamento é simplesmente alterar "stop yield" para "throw". Terceiro parágrafo - retornar um elemento vazio para alcançar fora dos limites não pode falhar, mas, no entanto, duvido que qualquer desenvolvedor de FP votaria como um bom design.
greenoldman
3
Meu segundo parágrafo não se aplica a todas as implementações, apenas as verdadeiramente preguiçosas. Se você zipduas sequências infinitas juntas, não sabe o tamanho no início. No terceiro parágrafo, eu disse padrão razoável . Retornar vazio neste caso não seria razoável, ao passo que deixar cair o rabo obviamente é.
Karl Bielefeldt
Ah, entendo finalmente o seu ponto - lançar exceção em linguagem preguiçosa não é uma substituição técnica, é completamente uma mudança de comportamento, porque você precisa lançar uma exceção logo no início, enquanto pode ignorar a cauda sempre que for conveniente.
greenoldman
3
Com +1, essa também é uma ótima resposta: "Os programadores funcionais preferem construções que não podem falhar pelo design". Isso indica de forma eloquente qual é o maior motivador por trás da maioria das decisões de design que os programadores funcionais tomam. Programadores imperativos têm uma regra que eles gostam que diz "Diga, não pergunte", o FP leva isso até o enésimo grau, concentrando-se em permitir instruções contínuas sem exigir verificação de resultados até o último momento absoluto, por isso tentamos garantir etapas intermediárias não pode falhar, porque a composabilidade é rei. Muito bem dito.
Jimmy Hoffa
12

Porque não há uma maneira óbvia de completar a cauda. Qualquer escolha sobre como fazê-lo resultaria em uma cauda não óbvia.

O truque é aumentar explicitamente sua lista mais curta para corresponder à duração da maior com os valores esperados.

Se o zip fez isso por você, você não sabia quais valores estavam preenchendo intuitivamente. Ciclou a lista? Repetiu um valor de mempty? O que é um valor de mempty para o seu tipo?

Não há nenhuma implicação no que o zip faz que alguém poderia usar para intuir a maneira como a cauda seria alongada; portanto, a única coisa razoável a fazer é trabalhar com os valores disponíveis, em vez de criar alguns que o consumidor não pode esperar.


Lembre-se também de que você está se referindo a uma função conhecida muito específica com semântica específica conhecida. Mas isso não significa que você não pode fazer uma função semelhante, mas um pouco diferente . Só porque existe uma função comum que faz isso x, não significa que você não pode decidir para o propósito que deseja fazer xe y.

Embora lembre-se do motivo pelo qual essas e muitas outras funções comuns do estilo FP são comuns, é porque elas são simples e generalizadas, para que você possa ajustar seu código para usá-las e obter o comportamento desejado. Por exemplo, em C # você poderia apenas

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())))
}

Ou outras coisas simples. As abordagens de FP tornam as modificações muito fáceis, porque você pode reutilizar peças e ter implementações tão pequenas quanto acima, que criar suas próprias versões modificadas das coisas é extremamente simples.

Jimmy Hoffa
fonte
Ok, mas é apenas quando você força as coleções a fazer algo para corresponder a outras - compare com a indexação da coleção (matriz). Você poderia começar a pensar que eu deveria expandir e organizar se eu tiver um índice fora dos limites? Ou talvez silenciosamente ignore a solicitação. Mas, por algum tempo, existe uma noção comum de exceção. O mesmo aqui - se você não tiver uma coleção correspondente, lance uma exceção. Por que essa abordagem não foi adotada?
greenoldman
2
zippoderia preencher nulos, o que geralmente é uma solução intuitiva. Considere o tipo zip :: [a] -> [b] -> [(Maybe a, Maybe b)]. É verdade que o tipo de resultado é um pouco ^ H ^ H bastante impraticável, mas permitiria implementar facilmente qualquer outro comportamento (atalho, exceção) em cima dele.
amon
1
@ amon: Isso não é nada intuitivo, é bobagem. Exigiria apenas a verificação nula de todos os argumentos.
DeadMG
4
@ amon nem todo tipo tem um nulo, é isso que eu quis dizer com mempty, os objetos têm nulo para preencher o espaço, mas você quer que ele tenha que criar uma coisa dessas para int e outros tipos também? Claro, o C # possui, default(T)mas nem todos os idiomas, e mesmo para o C # esse comportamento é realmente óbvio ? Acho que não
Jimmy Hoffa
1
@ amon Provavelmente seria mais útil retornar a parte não consumida da lista mais longa. Você pode usá-lo para verificar se eles tinham o mesmo comprimento após o fato, se necessário, e ainda pode voltar a zipar ou fazer algo com a cauda não consumida sem precisar percorrer a lista.
Doval