¿Por qué 'zip' ignora la cola colgante de la colección?

12

C # , Scala, Haskell, Lisp y Python tienen el mismo zipcomportamiento: si una colección es más larga, la cola se ignora en silencio.

También podría ser una excepción, pero no escuché ningún lenguaje usando este enfoque.

Esto me desconcierta. ¿Alguien sabe la razón por la cual zipestá diseñado de esa manera? Supongo que para los nuevos idiomas, se hace porque otros idiomas lo hacen de esta manera. Pero, ¿cuál fue la razón raíz?

Estoy haciendo una pregunta basada en hechos históricos, no si a alguien le gusta o si es un enfoque bueno o malo.

Actualización : si me preguntaran qué hacer, diría: arroje una excepción, de manera muy similar a indexar una matriz (a pesar de que los idiomas "antiguos" hicieron todo tipo de magia, cómo manejar el índice fuera de límites, UB, expandir matriz, etc)

Greenoldman
fuente
10
Si no ignora la cola que tiene un functor, usar secuencias infinitas sería más engorroso. Especialmente si obtener la longitud del rango no infinito era costoso / complicado / imposible.
Deduplicador
2
Pareces pensar que esto es inesperado y extraño. Lo encuentro obvio y, de hecho, inevitable. ¿Qué haría usted quiere que suceda cuando usted zip colecciones de longitud desigual?
Kilian Foth
@KilianFoth, obtén una excepción.
Greenoldman
@Deduplicator, agradable. Con la caída de cola silenciosa, puede expresar de forma zipWithIndexnatural proporcionando un generador de números naturales. Ahora, la pieza única que falta de información - lo que era que la razón? :-) (por cierto, vuelve a publicar tu comentario como respuesta, gracias).
greenoldman
1
Python tiene itertools.izip_longest, que efectivamente autocompila entradas terminadas con Nones. Lo elijo sobre zip con frecuencia cuando realmente uso zip; Sin embargo, no puedo recordar las razones detrás de ninguna opción. Python ya ha enumerado () para el caso de @ greenoldman, que uso a menudo.
StarWeaver

Respuestas:

11

Casi siempre es lo que desea, y cuando no lo es, puede hacerlo usted mismo.

El problema principal es con la semántica perezosa que no conoce la longitud cuando inicia por primera vez zip, por lo que no puede lanzar una excepción al comienzo. Debería devolver primero todos los elementos comunes, luego lanzar una excepción, que no sería muy útil.

También es un problema de estilo. Los programadores imperativos están acostumbrados a verificar manualmente las condiciones de contorno en todo el lugar. Los programadores funcionales prefieren construcciones que no pueden fallar por diseño. Las excepciones son extremadamente raras. Si hay una manera para que una función devuelva un valor predeterminado razonable, los programadores funcionales lo tomarán. La componibilidad es el rey.

Karl Bielefeldt
fuente
Pregunto por razones históricas, no por lo que puedo hacer. Segundo párrafo: estás equivocado, mira cómo zipse implementa actualmente. Lanzar una excepción es simplemente cambiar "detener el rendimiento" a "lanzar". Tercer párrafo: devolver el elemento vacío para llegar fuera del límite no puede fallar, pero dudo que cualquier desarrollador de FP vote que sea un buen diseño.
greenoldman
3
Mi segundo párrafo no se aplica a todas las implementaciones, solo a las realmente flojas. Si tienes zipdos secuencias infinitas juntas, no sabes el tamaño al principio. En el tercer párrafo, dije incumplimiento razonable . Volver vacío en este caso no sería razonable, mientras que dejar caer la cola obviamente lo es.
Karl Bielefeldt
Ah, finalmente entiendo su punto: lanzar una excepción en lenguaje perezoso no es un reemplazo técnico, es un cambio de comportamiento completo, porque necesita lanzar una excepción desde el principio, mientras que puede ignorar la cola siempre que sea conveniente.
greenoldman
3
+1 esta también es una gran respuesta, "Los programadores funcionales prefieren construcciones que no pueden fallar por diseño", así que elocuentemente establece cuál es el mayor motivador detrás de la mayoría de las decisiones de diseño que toman los programadores funcionales. Los programadores imperativos tienen una regla que les gusta que dice "Dile, no preguntes", FP lleva esto al enésimo grado al enfocarse en permitir contar continuamente las instrucciones sin requerir la verificación de resultados hasta el último momento absoluto, por lo que tratamos de asegurar pasos intermedios no puede fallar, porque la capacidad de composición es el rey. Muy bien dicho.
Jimmy Hoffa
12

Porque no hay una forma obvia de completar la cola. Cualquier elección sobre cómo hacerlo daría como resultado una cola no obvia.

El truco es alargar explícitamente su lista más corta para que coincida con la longitud de la más larga con los valores que espera.

Si zip lo hiciera por usted, no podría saber qué valores estaba completando intuitivamente. ¿Ciclo la lista? ¿Repitió un valor mempty? ¿Cuál es un valor mempty para tu tipo?

No hay ninguna implicación en lo que hace zip que uno podría usar para intuir la forma en que se alargaría la cola, por lo que lo único razonable es trabajar con los valores disponibles en lugar de inventar algo que su consumidor no puede esperar.


También recuerde que se refiere a una función conocida muy específica con una semántica bien conocida específica. Pero eso no significa que no pueda hacer una función similar pero ligeramente diferente . El hecho de que hay una función común que hace x, no significa que no se puede decidir para su propósito dado que quiere hacer xy y.

Aunque recuerde que esta y muchas otras funciones comunes de estilo FP son comunes, es porque son simples y generalizadas para que pueda modificar su código para usarlas y obtener el comportamiento que desea. Por ejemplo, en C # podrías simplemente

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

U otras cosas simples. Los enfoques de FP hacen que las modificaciones sean tan fáciles porque puede reutilizar piezas y tener implementaciones tan pequeñas como las anteriores para que crear sus propias versiones modificadas de cosas sea extremadamente simple.

Jimmy Hoffa
fuente
Está bien, pero solo cuando fuerza a las colecciones a hacer algo para que coincida con otras, compárelo con indexar la colección (matriz). ¿Podrías empezar a pensar si debería expandir y agrupar si tengo un índice fuera de los límites? O tal vez ignore en silencio la solicitud. Pero por algún tiempo hay una noción común de lanzar una excepción. Lo mismo aquí: si no tienes una colección coincidente, lanza una excepción. ¿Por qué no se tomó este enfoque?
greenoldman
2
zippodría completar nulos, que a menudo es una solución intuitiva. Considera el tipo zip :: [a] -> [b] -> [(Maybe a, Maybe b)]. Por supuesto, el tipo de resultado es un poco ^ H ^ H bastante poco práctico, pero permitiría implementar fácilmente cualquier otro comportamiento (atajo, excepción) encima.
amon
1
@amon: Eso no es intuitivo en absoluto, es una tontería. Solo requeriría una comprobación nula de cada argumento.
DeadMG
44
@amon no todos los tipos tienen un valor nulo, eso es lo que quise decir con que los memptyobjetos tienen un valor nulo para llenar el espacio, pero ¿quieres que tenga que tener algo así para int y otros tipos también? Claro, C # tiene default(T)pero no todos los lenguajes lo hacen, e incluso para C # ¿es ese comportamiento realmente obvio ? No lo creo
Jimmy Hoffa
1
@amon Probablemente sería más útil devolver la parte no consumida de la lista más larga. Puede usar eso para verificar si tenían la misma longitud después del hecho si lo necesita, y aún puede volver a cerrar o hacer algo con la cola no consumida sin volver a recorrer la lista.
Doval