Estaba leyendo el artículo aquí: http://www.paulgraham.com/avg.html y la parte sobre la "paradoja del blub" fue particularmente interesante. Como alguien que codifica principalmente en c ++ pero tiene exposición a otros idiomas (principalmente Haskell), conozco algunas cosas útiles en estos idiomas que son difíciles de replicar en c ++. La pregunta es principalmente para las personas que son competentes tanto en c ++ como en algún otro idioma, ¿hay alguna característica o idioma de lenguaje poderoso que utilice en un lenguaje que sería difícil de conceptualizar o implementar si solo estuviera escribiendo en c ++?
En particular, esta cita me llamó la atención:
Por inducción, los únicos programadores en condiciones de ver todas las diferencias de poder entre los distintos idiomas son aquellos que entienden el más poderoso. (Esto es probablemente lo que Eric Raymond quiso decir con Lisp para hacerte un mejor programador). No puedes confiar en las opiniones de los demás, debido a la paradoja de Blub: están satisfechos con el lenguaje que usan, porque dicta el cómo piensan sobre los programas.
Si resulta que soy el equivalente del programador "Blub" en virtud del uso de c ++, esto plantea la siguiente pregunta: ¿Hay algún concepto o técnica útil que haya encontrado en otros idiomas que le hubiera resultado difícil de conceptualizar si hubiera estado escribiendo o "pensando" en c ++?
Por ejemplo, el paradigma de programación lógica que se ve en lenguajes como Prolog y Mercury se puede implementar en c ++ usando la biblioteca castor, pero finalmente encuentro que conceptualmente estoy pensando en términos de código Prolog y traduciendo al equivalente de c ++ al usar esto. Como una forma de ampliar mi conocimiento de programación, estoy tratando de descubrir si hay otros ejemplos similares de expresiones útiles / poderosas que se expresen de manera más eficiente en otros lenguajes que no conozco como desarrollador de c ++. Otro ejemplo que viene a la mente es el sistema macro en lisp, generar el código del programa desde dentro del programa parece tener muchos beneficios para algunos problemas. Esto parece ser difícil de implementar y pensar desde dentro de c ++.
Esta pregunta no pretende ser un debate "c ++ vs lisp" ni ningún tipo de debate de tipo guerras lingüísticas. Hacer una pregunta como esta es la única forma en que puedo ver posible descubrir cosas que no sé sobre las que no sé.
fuente
there are things that other languages can do that Lisp can't
- Es poco probable, ya que Lisp está completo en Turing. ¿Quizás quisiste decir que hay algunas cosas que no son prácticas para hacer en Lisp? Podría decir lo mismo sobre cualquier lenguaje de programación.Respuestas:
Bueno, desde que mencionaste a Haskell:
La coincidencia de patrones. Creo que la coincidencia de patrones es mucho más fácil de leer y escribir. Considere la definición de mapa y piense cómo se implementaría en un lenguaje sin coincidencia de patrones.
El sistema de tipos. A veces puede ser un dolor, pero es extremadamente útil. Tienes que programar con él para comprenderlo realmente y cuántos errores atrapa. Además, la transparencia referencial es maravillosa. Solo se hace evidente después de programar en Haskell durante un tiempo cuántos errores son causados por la administración del estado en un lenguaje imperativo.
Programación funcional en general. Usando mapas y pliegues en lugar de iteración. Recursividad Se trata de pensar en un nivel superior.
Evaluación perezosa. Nuevamente, se trata de pensar en un nivel superior y dejar que el sistema maneje la evaluación.
Cabal, paquetes y módulos. Para mí, tener paquetes de descarga de Cabal es mucho más conveniente que encontrar el código fuente, escribir un archivo MAKE, etc. Ser capaz de importar solo ciertos nombres es mucho mejor que esencialmente tener todos los archivos fuente volcados juntos y luego compilados.
fuente
Maybe
(para C ++ verstd::optional
), se trata de tener que marcar explícitamente las cosas como opcional / anulable / tal vez.Memoize!
Intenta escribirlo en C ++. No con C ++ 0x.
Demasiado engorroso? Bien, pruébalo con C ++ 0x.
Vea si puede superar esta versión en tiempo de compilación de 4 líneas (o 5 líneas, lo que sea: P) en D:
Todo lo que necesita hacer para llamarlo es algo como:
También puede probar algo similar en Scheme, aunque es un poco más lento porque ocurre en tiempo de ejecución y porque la búsqueda aquí es lineal en lugar de hash (y bueno, porque es Scheme):
fuente
C ++ es un lenguaje multiparadigma, lo que significa que trata de admitir muchas formas de pensar. A veces, una característica de C ++ es más incómoda o menos fluida que la implementación de otro lenguaje, como es el caso de la programación funcional.
Dicho esto, no puedo pensar en la cabeza de una función nativa del lenguaje C ++ que hace lo que hace
yield
Python o JavaScript.Otro ejemplo es la programación concurrente . C ++ 0x tendrá algo que decir al respecto, pero el estándar actual no, y la concurrencia es una forma completamente nueva de pensar.
Además, el desarrollo rápido, incluso la programación de shell, es algo que nunca aprenderá si nunca abandona el dominio de la programación C ++.
fuente
setjmp
ylongjmp
. No tengo idea de cuánto se rompe, pero supongo que las excepciones serían las primeras en desaparecer. Ahora, si me disculpa, necesito volver a leer Modern C ++ Design para sacar eso de mi cabeza.Las rutinas son una característica de lenguaje inmensamente útil que apuntalan muchos de los beneficios más tangibles de otros lenguajes sobre C ++. Básicamente proporcionan pilas adicionales para que las funciones se puedan interrumpir y continuar, proporcionando facilidades similares a las de una tubería al lenguaje que alimenta fácilmente los resultados de las operaciones a través de filtros a otras operaciones. Es maravilloso, y en Ruby lo encontré muy intuitivo y elegante. La evaluación perezosa también se relaciona con esto.
La introspección y la compilación / ejecución / evaluación de código en tiempo de ejecución / lo que sea son características enormemente poderosas de las que carece C ++.
fuente
Habiendo implementado un sistema de álgebra computacional en Lisp y C ++, puedo decirte que la tarea fue mucho más fácil en Lisp, a pesar de que era un novato en el lenguaje. Esta naturaleza simplista de todo lo que se enumera simplifica una gran cantidad de algoritmos. Por supuesto, la versión C ++ fue muchísimo más rápida. Sí, podría haber hecho la versión lisp más rápida, pero el código no sería tan lispy. La creación de secuencias de comandos es otra cosa que siempre será más fácil, por ejemplo, el lisp. Se trata de utilizar la herramienta adecuada para el trabajo.
fuente
¿Qué queremos decir cuando decimos que un idioma es "más poderoso" que otro? Cuando decimos que un lenguaje es "expresivo"? O "rico"? Creo que queremos decir que un lenguaje gana poder cuando su campo de visión se reduce lo suficiente como para que sea fácil y natural describir un problema, realmente una transición de estado, ¿no? - Que vive dentro de esa vista. Sin embargo, ese lenguaje es considerablemente menos poderoso, menos expresivo y menos útil cuando nuestro campo de visión se amplía.
Cuanto más "poderoso" y "expresivo" es el lenguaje, más limitado es su uso. Entonces, tal vez "poderoso" y "expresivo" son las palabras incorrectas para una herramienta de utilidad limitada. Tal vez "apropiado" o "abstracto" son mejores palabras para tales cosas.
Comencé en programación escribiendo un montón de cosas de bajo nivel: controladores de dispositivos con sus rutinas de interrupción; programas incrustados; código del sistema operativo El código era íntimo con el hardware y lo escribí todo en lenguaje ensamblador. No diríamos que ensamblador es en lo más abstracto, pero fue y es el lenguaje más poderoso y expresivo de todos. Puedo expresar cualquier problema en lenguaje ensamblador; es tan poderoso que puedo hacer lo que quiera con cualquier máquina.
Y toda mi comprensión posterior del lenguaje de nivel superior le debe todo a mi experiencia con el ensamblador. Todo lo que aprendí más tarde fue fácil porque, como ven, todo, no importa cuán abstracto, al final debe adaptarse al hardware.
Es posible que desee olvidarse de niveles de abstracción cada vez más altos, es decir, campos de visión cada vez más estrechos. Siempre puedes recoger eso más tarde. Es muy fácil de aprender, es cuestión de días. Sería mejor, en mi opinión, aprender el lenguaje del hardware 1 , acercarse lo más posible al hueso.
1 Quizás no del todo pertinente, pero
car
ycdr
tome sus nombres del hardware: el primer Lisp se ejecutó en una máquina que tenía un Registro de decremento real y un Registro de dirección real. ¿Qué tal eso?fuente
Matrices asociativas
Una forma típica de procesar datos es:
La herramienta adecuada para ello es la matriz asociativa .
Realmente no me gusta la sintaxis de matriz asociativa de JavaScript, porque no puedo crear, digamos un [x] [y] [z] = 8 , primero tengo que crear un [x] y un [x] [y] .
De acuerdo, en C ++ (y en Java) hay una buena cartera de clases de contenedor, Map , Multimap , en absoluto, pero si quiero escanear, tengo que hacer un iterador, y cuando quiero insertar un nuevo elemento de nivel profundo, yo Hay que crear todos los niveles superiores, etc. Incómodo.
No digo que no haya matrices asociativas utilizables en C ++ (y Java), pero los lenguajes de script sin tipo (o de tipo no estricto) superan a los compilados, porque son lenguajes de script sin tipo.
Descargo de responsabilidad: no estoy familiarizado con C # y otros idiomas .NET, AFAIK tienen un buen manejo de matriz asociativa.
fuente
dict
(por ejemplox = {0: 5, 1: "foo", None: 500e3}
, tenga en cuenta que no hay requisitos para que las claves o los valores sean del mismo tipo). Intentar hacer algo asía[x][y][z] = 8
es difícil porque el lenguaje tiene que mirar hacia el futuro para ver si vas a establecer un valor o crear otro nivel; la expresióna[x][y]
en sí misma no te dice.No aprendo Java, C \ C ++, Assembly y Java Script. Yo uso C ++ para ganarse la vida.
Sin embargo, tendría que decir que me gusta más la programación de ensamblaje y la programación de C. Esto está en línea principalmente con la programación imperativa.
Sé que los Paradigmas de programación son importantes para clasificar los tipos de datos y otorgar conceptos abstractos de programación más altos para permitir poderosos patrones de diseño y formalización de código. Aunque en cierto sentido, cada Paradigms es una colección de patrones y colecciones para abstraer la capa de hardware subyacente para que no tenga que pensar en el EAX o IP internamente dentro de la máquina.
Mi único problema con esto es que permite que la noción de personas y los conceptos de cómo funciona la máquina se conviertan en afirmaciones ambiguas e ideológicas de lo que está sucediendo. Este pan es todo tipo de abstracciones maravillosas además de resúmenes para algún objetivo ideológico del programador.
Al final del día, es mejor tener una buena mentalidad clara y los límites de lo que es la CPU y cómo funcionan las computadoras bajo el capó. Lo único que le importa a la CPU es ejecutar una serie de instrucciones que mueven los datos dentro y fuera de la memoria a un registro y realiza una instrucción. No tiene ningún concepto de tipo de datos, ni ningún concepto de programación superior. Solo mueve datos.
Se vuelve más complejo cuando agrega paradigmas de programación a la mezcla porque nuestra visión del mundo es completamente diferente.
fuente
C ++ hace que muchos enfoques sean intratables. Diría que la mayoría de la programación es difícil de conceptualizar si te limitas a C ++. Aquí hay algunos ejemplos de problemas que se resuelven mucho más fácilmente en formas que C ++ dificulta.
Registrar asignaciones y convenciones de llamadas
Mucha gente piensa en C ++ como un lenguaje básico de bajo nivel, pero realmente no lo es. Al abstraer detalles importantes de la máquina, C ++ dificulta la conceptualización de aspectos prácticos como la asignación de registros y las convenciones de llamadas.
Para conocer conceptos como estos, recomiendo probar la programación en lenguaje ensamblador y consultar este artículo sobre la calidad de generación de código ARM .
Generación de código en tiempo de ejecución
Si solo conoce C ++, entonces probablemente piense que las plantillas son el todo y el final de la metaprogramación. No lo son De hecho, son una herramienta objetivamente mala para la metaprogramación. Cualquier programa que manipule otro programa es un metaprograma, que incluye intérpretes, compiladores, sistemas de álgebra computacional y demostradores de teoremas. La generación de código en tiempo de ejecución es una característica útil para esto.
Recomiendo activar una implementación de Scheme y jugar
EVAL
para aprender sobre la evaluación metacircular.Manipulando árboles
Los árboles están en todas partes en la programación. En el análisis tiene árboles de sintaxis abstracta. En los compiladores tienes IR que son árboles. En gráficos y programación GUI tienes árboles de escena.
Este "Analizador JSON ridículamente simple para C ++" pesa solo 484 LOC, que es muy pequeño para C ++. Ahora compárelo con mi propio analizador JSON simple que pesa solo 60 LOC de F #. La diferencia se debe principalmente a que los tipos de datos algebraicos de ML y la coincidencia de patrones (incluidos los patrones activos) hacen que sea mucho más fácil manipular los árboles.
Echa un vistazo a los árboles rojo-negros en OCaml también.
Estructuras de datos puramente funcionales.
La falta de GC en C ++ hace que sea prácticamente imposible adoptar algunos enfoques útiles. Las estructuras de datos puramente funcionales son una de esas herramientas.
Por ejemplo, vea este comparador de expresiones regulares de 47 líneas en OCaml. La brevedad se debe en gran medida al uso extensivo de estructuras de datos puramente funcionales. En particular, el uso de diccionarios con claves que son conjuntos. Eso es realmente difícil de hacer en C ++ porque los diccionarios y conjuntos stdlib son mutables, pero no puede mutar las claves de un diccionario o puede romper la colección.
La programación lógica y los búferes de deshacer son otros ejemplos prácticos en los que las estructuras de datos puramente funcionales hacen que algo difícil en C ++ sea realmente fácil en otros lenguajes.
La cola llama
C ++ no solo no garantiza las llamadas de cola, sino que RAII está fundamentalmente en desacuerdo porque los destructores se interponen en el camino de una llamada en posición de cola. Las llamadas de cola le permiten realizar un número ilimitado de llamadas de función utilizando solo una cantidad limitada de espacio de pila. Esto es excelente para implementar máquinas de estado, incluidas las máquinas de estado extensibles y es una excelente tarjeta para "salir de la cárcel" en muchas circunstancias incómodas.
Por ejemplo, revise esta implementación del problema de mochila 0-1 usando el estilo de paso de continuación con la memorización en F # de la industria financiera. Cuando tiene llamadas de cola, el estilo de paso de continuación puede ser una solución obvia, pero C ++ lo hace intratable.
Concurrencia
Otro ejemplo obvio es la programación concurrente. Aunque esto es completamente posible en C ++, es extremadamente propenso a errores en comparación con otras herramientas, sobre todo comunicando procesos secuenciales como se ve en lenguajes como Erlang, Scala y F #.
fuente
Esta es una vieja pregunta, pero como nadie la ha mencionado, agregaré comprensiones de listas (y ahora dict). Es fácil escribir una línea en Haskell o Python que resuelva el problema de Fizz-Buzz. Intenta hacer eso en C ++.
Si bien C ++ hizo movimientos masivos a la modernidad con C ++ 11, es un poco difícil llamarlo un lenguaje "moderno". C ++ 17 (que aún no se ha lanzado) está haciendo aún más movimientos para llegar a los estándares modernos, siempre que "moderno" signifique "no del milenio anterior".
Incluso las comprensiones más simples que uno puede escribir en una línea en Python (y obedeciendo el límite de longitud de línea de 79 caracteres de Guido) se convierten en muchas líneas de códigos cuando se traducen a C ++, y algunas de esas líneas de código C ++ son bastante complicadas.
fuente
Una biblioteca compilada que llama a una devolución de llamada, que es una función miembro definida por el usuario de una clase definida por el usuario.
Esto es posible en Objective-C, y hace que la programación de la interfaz de usuario sea muy sencilla. Puede decirle a un botón: "Por favor, llame a este método para este objeto cuando lo presione", y el botón lo hará. Puede usar cualquier nombre de método para la devolución de llamada que desee, no está congelado en el código de la biblioteca, no tiene que heredar de un adaptador para que funcione, ni el compilador desea resolver la llamada en tiempo de compilación, e, igualmente importante, puede decirle a dos botones que llamen a dos métodos diferentes del mismo objeto.
Todavía no he visto una forma similarmente flexible de definir una devolución de llamada en ningún otro idioma (¡aunque estaría muy interesado en saber de ellos!). El equivalente más cercano en C ++ probablemente esté pasando una función lambda que realiza la llamada requerida, que nuevamente restringe el código de la biblioteca para que sea una plantilla.
Es esta característica de Objective-C la que me ha enseñado a valorar la capacidad de un lenguaje para transmitir cualquier tipo de objetos / funciones / cualquier-concepto-importante-que-el lenguaje contenga libremente, junto con el poder de salvarlos. variables Cualquier punto en un lenguaje que defina cualquier tipo de concepto, pero que no proporcione un medio para almacenarlo (o una referencia a él) en todos los tipos de variables disponibles, es un obstáculo significativo y probablemente una fuente de información muy fea, código duplicado Desafortunadamente, los lenguajes de programación barrocos tienden a exhibir varios de estos puntos:
En C ++ no puede escribir el tipo de un VLA, ni almacenar un puntero en él. Esto prohíbe efectivamente las matrices multidimensionales verdaderas de tamaño dinámico (que están disponibles en C desde C99).
En C ++ no puede escribir el tipo de lambda. Ni siquiera puedes escribirlo. Por lo tanto, no hay forma de pasar una lambda o almacenar una referencia a ella en un objeto. Las funciones de Lambda solo se pueden pasar a plantillas.
En Fortran no puede escribir el tipo de una lista de nombres. Simplemente no hay forma de pasar una lista de nombres a ningún tipo de rutina. Entonces, si tiene un algoritmo complejo que debería ser capaz de manejar dos listas de nombres diferentes, no tiene suerte. No puede simplemente escribir el algoritmo una vez y pasarle las listas de nombres relevantes.
Estos son solo algunos ejemplos, pero ve el punto en común: cada vez que ve una restricción así por primera vez, generalmente no le importará porque parece una idea muy loca hacer lo prohibido. Sin embargo, cuando realiza una programación seria en ese lenguaje, finalmente llega al punto en que esta restricción precisa se convierte en una verdadera molestia.
fuente
I have not seen a similarly flexible way to define a callback in any other language yet (though I'd be very interested to hear about them!)
Lo que acaba de describir suena exactamente como la forma en que funciona el código de interfaz de usuario controlado por eventos en Delphi. (Y en .NET WinForms, que estuvo fuertemente influenciado por Delphi.)std::vector
. Si bien es un poco menos eficiente debido a que no utiliza la asignación de la pila, es funcionalmente isomorfo a un VLA, por lo que realmente no cuenta como un problema de tipo "blub": los programadores de C ++ pueden ver cómo funciona y simplemente decir: "ah sí , C hace eso más eficientemente que C ++ ".std::function
está.object::method
y se convertirá en una instancia. de cualquier interfaz que el código de recepción espere. C # tiene delegados. Todo lenguaje funcional de objetos tiene esta característica porque es básicamente el punto de sección transversal de los dos paradigmas.