La respuesta de @ sacundim es mayormente cierta, pero también hay algunas otras ideas importantes sobre el intercambio de diseños de idiomas y requisitos prácticos.
Objetos y referencias
Estos lenguajes generalmente exigen (o asumen) objetos que tienen extensiones dinámicas no vinculadas (o en el lenguaje de C, duración , aunque no exactamente lo mismo debido a las diferencias de significado de los objetos entre estos lenguajes, ver más abajo) de forma predeterminada, evitando referencias de primera clase ( por ejemplo, punteros de objetos en C) y comportamiento impredecible en las reglas semánticas (por ejemplo, comportamiento indefinido de ISO C relacionado con la semántica).
Además, la noción de objetos (de primera clase) en dichos lenguajes es conservadoramente restrictiva: no se especifican y garantizan de forma predeterminada propiedades "locativas". Esto es completamente diferente en algunos lenguajes similares a ALGOL cuyos objetos no tienen extensiones dinámicas no vinculadas (por ejemplo, en C y C ++), donde los objetos básicamente significan algunos tipos de "almacenamiento con tipo", generalmente junto con ubicaciones de memoria.
Codificar el almacenamiento dentro de los objetos tiene algunos beneficios adicionales, como poder adjuntar efectos computacionales deterministas a lo largo de su vida útil, pero es otro tema.
Problemas de simulación de estructuras de datos
Sin referencias de primera clase, las listas enlazadas individualmente no pueden simular muchas estructuras de datos tradicionales (ansiosas / mutables) de manera efectiva y portátil, debido a la naturaleza de la representación de estas estructuras de datos y las limitadas operaciones primitivas en estos idiomas. (Por el contrario, en C, puede derivar listas vinculadas con bastante facilidad incluso en un programa estrictamente conforme ). Y tales estructuras de datos alternativas como matrices / vectores tienen algunas propiedades superiores en comparación con las listas enlazadas en la práctica. Es por eso que R 5 RS introduce nuevas operaciones primitivas.
Pero existen diferencias en los tipos de vectores / matrices frente a listas doblemente vinculadas. A menudo se supone una matriz con O (1) complejidad de tiempo de acceso y menos sobrecarga de espacio, que son excelentes propiedades que no comparten las listas. (Aunque estrictamente hablando, ninguno de los dos está garantizado por ISO C, pero los usuarios casi siempre lo esperan y ninguna implementación práctica violaría estas garantías implícitas demasiado obviamente). , mientras que la iteración hacia atrás / adelante también es compatible con una matriz o un vector (junto con índices enteros) con incluso menos sobrecarga. Por lo tanto, una lista doblemente vinculada no funciona mejor en general. Peor aún, El rendimiento sobre la eficiencia de caché y la latencia en la asignación dinámica de memoria de las listas es catastróficamente peor que el rendimiento de las matrices / vectores cuando se utiliza el asignador predeterminado proporcionado por el entorno de implementación subyacente (por ejemplo, libc). Entonces, sin un tiempo de ejecución muy específico e "inteligente" que optimice en gran medida tales creaciones de objetos, los tipos de matriz / vector a menudo se prefieren a las listas vinculadas. (Por ejemplo, usando ISO C ++, hay una advertencia questd::vectordebería preferirse std::listpor defecto.) Por lo tanto, introducir nuevas primitivas para soportar específicamente (doblemente) listas enlazadas definitivamente no es tan beneficioso como para soportar estructuras de datos de matriz / vector en la práctica.
Para ser justos, las listas aún tienen algunas propiedades específicas mejores que las matrices / vectores:
- Las listas están basadas en nodos. Eliminar elementos de las listas no invalida la referencia a otros elementos en otros nodos. (Esto también es cierto para algunas estructuras de datos de árbol o gráfico). OTOH, las matrices / vectores pueden hacer referencias a la posición final que se invalida (con una reasignación masiva en algunos casos).
- Las listas pueden empalmarse en O (1) tiempo. La reconstrucción de nuevas matrices / vectores con las actuales es mucho más costosa.
Sin embargo, estas propiedades no son demasiado importantes para un lenguaje con soporte integrado de listas enlazadas individualmente, que ya es capaz de tal uso. Aunque todavía existen diferencias, en los lenguajes con extensiones dinámicas obligatorias de objetos (lo que generalmente significa que hay un recolector de basura que mantiene alejadas las referencias colgantes), la invalidación también puede ser menos importante, dependiendo de los intentos. Entonces, los únicos casos en que las listas doblemente vinculadas pueden ganar son:
- Se necesitan tanto la garantía de no reasignación como los requisitos de iteración bidireccional. (Si el rendimiento del acceso a elementos es importante y el conjunto de datos es lo suficientemente grande, en su lugar elegiría árboles de búsqueda binarios o tablas hash).
- Se necesitan operaciones de empalme bidireccionales eficientes. Esto es considerablemente raro. (Solo cumplo los requisitos solo para implementar algo como registros de historial lineal en un navegador).
Inmutabilidad y alias
En un lenguaje puro como Haskell, los objetos son inmutables. El objeto de Scheme a menudo se usa sin mutación. Tal hecho hace posible mejorar efectivamente la eficiencia de la memoria con la internación de objetos : el intercambio implícito de múltiples objetos con el mismo valor sobre la marcha.
Esta es una estrategia agresiva de optimización de alto nivel en el diseño del lenguaje. Sin embargo, esto implica problemas de implementación. En realidad, introduce alias implícitos a las celdas de almacenamiento subyacentes. Hace que el análisis de alias sea más difícil. Como resultado, es probable que haya menos posibilidades de eliminar la sobrecarga de referencias que no sean de primera clase, incluso los usuarios nunca las tocan. En lenguajes como Scheme, una vez que la mutación no se descarta totalmente, esto también interfiere en el paralelismo. Sin embargo, puede estar bien en un lenguaje perezoso (que de todos modos ya tiene problemas de rendimiento causados por thunks).
Para la programación de propósito general, tal elección del diseño del lenguaje puede ser problemática. Pero con algunos patrones comunes de codificación funcional, los lenguajes parecen funcionar bien.