¿Qué es tan difícil sobre los punteros / recursividad? [cerrado]

20

En los peligros de las escuelas de Java, Joel analiza su experiencia en Penn y la dificultad de las "fallas de segmentación". Él dice

[los valores predeterminados son difíciles hasta que usted] "respire profundamente y realmente intente forzar a su mente a trabajar en dos niveles diferentes de abstracción simultáneamente".

Dada una lista de causas comunes de segfaults, no entiendo cómo tenemos que trabajar en 2 niveles de abstracción.

Por alguna razón, Joel considera que estos conceptos son fundamentales para la capacidad de abstracción de un programador. No quiero asumir demasiado. Entonces, ¿qué es tan difícil acerca de los punteros / recursividad? Los ejemplos serían buenos.

P.Brian.Mackey
fuente
31
Deja de preocuparte por lo que Joel pueda pensar de ti. Si encuentra la recursión fácil, eso es bueno. No todos los demás lo hacen.
FrustratedWithFormsDesigner
66
La recursión es fácil por definición (función que se llama a sí mismo), pero saber cuándo usarlo y cómo hacerlo funcionar es la parte difícil.
JeffO
99
Solicita un trabajo en Fog Creek y cuéntanos cómo te va. Todos estamos muy interesados ​​en tu autopromoción.
Joel Etherton
44
@ P.Brian.Mackey: No estamos malentendidos. La pregunta realmente no pregunta nada. Es descarada autopromoción. Si quiere saber qué pregunta Joel sobre los punteros / recursividad, pregúntele a: [email protected]
Joel Etherton
19
¿Duplicado de esta pregunta ?
ozz

Respuestas:

38

Primero me di cuenta de que los consejos y la recursión eran difíciles en la universidad. Había tomado un par de cursos típicos de primer año (uno era C y Assembler, el otro estaba en Scheme). Ambos cursos comenzaron con cientos de estudiantes, muchos de los cuales tenían años de experiencia en programación de nivel secundario (típicamente BASIC y Pascal, en esos días). Pero tan pronto como se introdujeron los punteros en el curso C, y se introdujo la recursividad en el curso Scheme, una gran cantidad de estudiantes, tal vez incluso la mayoría, quedaron completamente desconcertados. Estos eran niños que habían escrito MUCHO código antes y no tenían ningún problema, pero cuando golpearon punteros y recursividad, también golpearon una pared en términos de su capacidad cognitiva.

Mi hipótesis es que los punteros y la recursividad son los mismos, ya que requieren que mantenga dos niveles de abstracción en su cabeza al mismo tiempo. Hay algo acerca de los múltiples niveles de abstracción que requiere un tipo de aptitud mental que es muy posible que algunas personas nunca tengan.

  • Con los punteros, los "dos niveles de abstracción" son "datos, dirección de datos, dirección de dirección de datos, etc." o lo que tradicionalmente llamamos "valor frente a referencia". Para el estudiante no capacitado, es muy difícil ver la diferencia entre la dirección de x y x .
  • Con la recursividad, los "dos niveles de abstracción" comprenden cómo es posible que una función se llame a sí misma. Un algoritmo recursivo es a veces lo que la gente llama "programación por ilusiones" y es muy, muy poco natural pensar en un algoritmo en términos de "caso base + caso inductivo" en lugar de la lista más natural de pasos que debe seguir para resolver un problema. ". Para el estudiante no capacitado que mira un algoritmo recursivo, el algoritmo parece plantear la pregunta .

También estaría perfectamente dispuesto a aceptar que es posible enseñar consejos y / o recurrencia a cualquiera ... No tengo ninguna evidencia de una manera u otra. Sé que, empíricamente, ser capaz de comprender realmente estos dos conceptos es un muy, muy buen predictor de la capacidad de programación general y que, en el curso normal de la formación de CS de pregrado, estos dos conceptos son algunos de los mayores obstáculos.

Joel Spolsky
fuente
44
"Muy, muy poco natural pensar en un algoritmo en términos de" caso base + caso inductivo "". Creo que de ninguna manera es antinatural, es solo que los niños no están siendo entrenados en consecuencia.
Ingo
14
si fuera natural, no necesitarías ser entrenado. : P
Joel Spolsky
1
Buen punto :), pero no necesitamos entrenamiento en matemáticas, lógica, física, etc., todo lo cual es, en un sentido más amplio, más natural. Curiosamente, pocos programadores tienen problemas con la sintaxis de los idiomas, pero está lleno de recurrencia.
Ingo
1
En mi universidad, el primer curso comenzó con programación funcional y recursividad casi de inmediato, mucho antes de introducir mutaciones y cosas por el estilo. Descubrí que algunos de los estudiantes sin experiencia entendían la recursión mejor que aquellos con alguna experiencia. Dicho esto, los mejores de la clase estaban formados por personas con mucha experiencia.
Tikhon Jelvis
2
Creo que la incapacidad para comprender los punteros y la recursividad está relacionada con a) el nivel general del coeficiente intelectual yb) la mala educación matemática.
quant_dev
23

La recursión no es solo "una función que se llama a sí misma". No vas a apreciar realmente por qué la recursividad es difícil hasta que te encuentres sacando cuadros de pila para descubrir qué salió mal con tu analizador de descenso recursivo. A menudo tendrá funciones recursivas recíprocas (la función A llama a la función B, que llama a la función C, que puede llamar a la función A). Puede ser muy difícil descubrir qué salió mal cuando tienes N stackframes en lo profundo de una serie de funciones recursivas mutuamente.

En cuanto a los punteros, nuevamente, el concepto de punteros es bastante simple: una variable que almacena una dirección de memoria. Pero, de nuevo, cuando algo falla con su estructura de datos complicada de void**punteros que apuntan a diferentes nodos, verá por qué puede ser complicado a medida que lucha por descubrir por qué uno de sus punteros apunta a una dirección basura.

Charles Salvia
fuente
1
La implementación de un analizador recursivo decente fue cuando realmente sentí que tenía algo de control sobre la recursividad. Los punteros son fáciles de entender a un alto nivel como dijiste; No es hasta que entras en los aspectos básicos de las implementaciones que tratan con punteros que ves por qué son complicados.
Chris
La recursión mutua entre muchas funciones es esencialmente la misma que goto.
starblue
2
@starblue, en realidad no, ya que cada stackframe crea nuevas instancias de variables locales.
Charles Salvia
Tienes razón, solo la recursión de la cola es la misma que goto.
starblue
3
@wnoise int a() { return b(); }puede ser recursivo, pero depende de la definición de b. Así que no es tan simple como parece ...
alternativa
14

Java admite punteros (se llaman referencias) y admite recursividad. En la superficie, su argumento parece inútil.

De lo que realmente está hablando es de la capacidad de depurar. Se garantiza que un puntero Java (err, referencia) apunta a un objeto válido. El puntero de CA no lo es. Y el truco en la programación en C, suponiendo que no use herramientas como valgrind , es averiguar exactamente dónde atornilló un puntero (rara vez se encuentra en el punto que se encuentra en un stacktrace).

Luego
fuente
55
Los punteros per se son un detalle. Usar referencias en Java no es más complicado que usar variables locales en C. Incluso mezclarlas de la manera en que lo hacen las implementaciones de Lisp (un átomo puede ser un número entero de tamaño limitado, un carácter o un puntero) no es difícil. Se vuelve más difícil cuando el lenguaje permite que el mismo tipo de datos sea local o referenciado, con diferente sintaxis, y realmente complicado cuando el lenguaje permite la aritmética de puntero.
David Thornley
@David - um, ¿qué tiene esto que ver con mi respuesta?
Anon
1
Tu comentario sobre los punteros de soporte de Java.
David Thornley
"donde jodiste un puntero (rara vez en el punto que se encuentra en una traza de pila)". Si tienes la suerte de conseguir un stacktrace.
Omega Centauri
55
Estoy de acuerdo con David Thornley; Java no admite punteros a menos que pueda hacer un puntero a un puntero a un puntero a un puntero a un int. ¿Qué tal vez supongo que podría hacer haciendo de 4 a 5 clases cada una de las cuales haga referencia a otra cosa, pero eso es realmente puntero o es una solución fea?
alternativa el
12

El problema con los punteros y la recursividad no es que sean necesariamente difíciles de entender, sino que se les enseña mal, especialmente con respecto a lenguajes como C o C ++ (principalmente porque los idiomas mismos se están enseñando mal). Cada vez que escucho (o leo) que alguien dice "una matriz es solo un puntero" muero un poco por dentro.

Del mismo modo, cada vez que alguien usa la función Fibonacci para ilustrar la recursión, quiero gritar. Es un mal ejemplo porque la versión iterativa no es más difícil de escribir y funciona al menos tan bien o mejor que la recursiva, y no te da una idea real de por qué una solución recursiva sería útil o deseable. Quicksort, transversal del árbol, etc., son ejemplos mucho mejores del por qué y cómo de la recursividad.

Tener que manipular los punteros es un artefacto de trabajar en un lenguaje de programación que los expone. Generaciones de programadores de Fortran estaban construyendo listas y árboles, pilas y colas sin necesidad de un tipo de puntero dedicado (o asignación de memoria dinámica), y nunca escuché a nadie acusar a Fortran de ser un lenguaje de juguete.

John Bode
fuente
Estoy de acuerdo, había tenido años / décadas de Fortran antes de ver indicadores reales, por lo que ya había estado usando mi propia forma de hacer lo mismo, antes de tener la oportunidad de dejar que el lenguaje / compilador lo hiciera por mí. También creo que la sintaxis de C con respecto a punteros / direcciones es muy confusa, aunque el concepto de un valor almacenado en una dirección es muy simple.
Omega Centauri
Si tiene un enlace a Quicksort implementado en Fortran IV, me encantaría verlo. No digo que no se pueda hacer; de hecho, lo implementé en BASIC hace unos 30 años, pero me interesaría verlo.
Anon
Nunca trabajé en Fortran IV, pero implementé algunos algoritmos recursivos en la implementación VAX / VMS de Fortran 77 (había un gancho que le permitía guardar el objetivo de un goto como un tipo especial de variable, para que pudiera escribir GOTO target) . Sin embargo, creo que tuvimos que construir nuestras propias pilas de tiempo de ejecución. Esto fue hace tanto tiempo que ya no puedo recordar los detalles.
John Bode
8

Hay varias dificultades con los punteros:

  1. Aliasing La posibilidad de cambiar el valor de un objeto usando diferentes nombres / variables.
  2. No localidad La posibilidad de cambiar el valor de un objeto en un contexto diferente del que se declara (esto también ocurre con los argumentos pasados ​​por referencia).
  3. No coincidencia de por vida La vida útil de un puntero puede ser diferente de la vida útil del objeto al que apunta, y eso puede conducir a referencias no válidas (SEGFAULTS) o basura.
  4. Puntero aritmético . Algunos lenguajes de programación permiten la manipulación de punteros como enteros, y eso significa que los punteros pueden apuntar a cualquier parte (incluidos los lugares más inesperados cuando hay un error). Para utilizar la aritmética de puntero correctamente, un programador debe conocer los tamaños de memoria de los objetos señalados, y eso es algo más en lo que pensar.
  5. Conversiones de tipos La capacidad de lanzar un puntero de un tipo a otro permite sobrescribir la memoria de un objeto diferente del previsto.

Es por eso que un programador debe pensar más a fondo cuando usa punteros (no sé acerca de los dos niveles de abstracción ). Este es un ejemplo de los errores típicos cometidos por un novato:

Pair* make_pair(int a, int b)
{
    Pair p;
    p.a = a;
    p.b = b;
    return &p;
}

Tenga en cuenta que un código como el anterior es perfectamente razonable en lenguajes que no tienen un concepto de punteros, sino uno de nombres (referencias), objetos y valores, como los lenguajes de programación funcionales y los lenguajes con recolección de basura (Java, Python). .

La dificultad con las funciones recursivas ocurre cuando las personas sin suficientes conocimientos matemáticos (donde la recursividad es común y se requiere conocimiento) intentan abordarlas pensando que la función se comportará de manera diferente dependiendo de cuántas veces se haya llamado antes . Ese problema se agrava porque las funciones recursivas se pueden crear de una manera en la que hay que pensar de esa manera para comprenderlas.

Piense en funciones recursivas con punteros que se pasan, como en una implementación de procedimiento de un árbol rojo-negro en el que la estructura de datos se modifica en el lugar; Es algo más difícil de pensar que una contraparte funcional .

No se menciona en la pregunta, pero el otro problema importante con el que los principiantes tienen dificultades es la concurrencia .

Como otros han mencionado, hay un problema adicional no conceptual con algunas construcciones de lenguaje de programación: es que incluso si entendemos, los errores simples y honestos con esas construcciones pueden ser extremadamente difíciles de depurar.

Apalala
fuente
El uso de esa función devolverá un puntero válido, pero la variable está en un alcance superior al alcance que llamó a la función, por lo que el puntero podría (se supondrá que) se invalidará cuando se use malloc.
derecha el
44
@ Radek S: No, no lo hará. Devolverá un puntero inválido que en algunos entornos funciona por un tiempo hasta que algo más lo sobrescribe. (En la práctica, esta será la pila, no el montón. No malloc()es más probable que cualquier otra función que lo haga)
Wnoise
1
@Radeck En la función de muestra, el puntero señala a la memoria que el lenguaje de programación (C en este caso) garantiza que se liberará una vez que la función regrese. Por lo tanto, el puntero devuelto apunta a basura . Los idiomas con recolección de basura mantienen vivo el objeto siempre que se haga referencia a él en cualquier contexto.
Apalala
Por cierto, Rust tiene punteros pero sin estos problemas. (cuando no está en un contexto inseguro)
Sarge Borsch
2

Los punteros y la recursión son dos bestias separadas y hay diferentes razones que califican a cada una como "difícil".

En general, los punteros requieren un modelo mental diferente que la asignación de variable pura. Cuando tengo una variable de puntero, es solo eso: un puntero a otro objeto, los únicos datos que contiene es la dirección de memoria a la que apunta. Entonces, por ejemplo, si tengo un puntero int32 y le asigno un valor directamente, no estoy cambiando el valor de int, estoy apuntando a una nueva dirección de memoria (hay muchos trucos geniales que puedes hacer con esto ) Aún más interesante es tener un puntero a un puntero (esto es lo que sucede cuando pasa una variable Ref como parámetro de función en C #, la función puede asignar un objeto completamente diferente al parámetro y ese valor aún estará en el alcance cuando la función salidas

La recursión da un pequeño salto mental cuando aprende por primera vez porque está definiendo una función en términos de sí misma. Es un concepto salvaje cuando lo encuentras por primera vez, pero una vez que captas la idea, se convierte en una segunda naturaleza.

Pero volvamos al tema en cuestión. El argumento de Joel no se trata de punteros o recursividad en sí mismos, sino más bien del hecho de que los estudiantes están siendo retirados más de cómo funcionan realmente las computadoras. Esta es la Ciencia en Informática. Hay una diferencia clara entre aprender a programar y aprender cómo funcionan los programas. No creo que se trate tanto de "Lo aprendí de esta manera, así que todos deberían tener que aprenderlo de esta manera" como él, argumentando que muchos programas de CS se están convirtiendo en escuelas de comercio glorificadas.

Michael Brown
fuente
1

Le doy a P. Brian un +1, porque siento que lo hace: la recursividad es un concepto tan fundamental que el que tiene las más mínimas dificultades debería considerar buscar un trabajo en mac donalds, pero incluso si hay recidiva:

make a burger:
   put a cold burger on the grill
   wait
   flip
   wait
   hand the fried burger over to the service personel
   unless its end of shift: make a burger

Seguramente, la falta de comprensión también tiene que ver con nuestras escuelas. Aquí uno debería introducir números naturales como lo hicieron Peano, Dedekind y Frege, para que no tengamos muchas dificultades más tarde.

Ingo
fuente
66
Esa es la recusación de la cola, que posiblemente se repita.
Michael K
66
Lo siento, para mí, el bucle es posiblemente una recursión de cola :)
Ingo
3
@Ingo: :) ¡Fanático funcional!
Michael K
1
@Michael - jeje, de hecho !, pero creo que se puede argumentar que la recursividad es el concepto más fundamental.
Ingo
@Ingo: Podría, de hecho (su ejemplo lo demuestra bien). Sin embargo, por alguna razón, los humanos tienen dificultades con eso en la programación; parece que queremos ese extra goto toppor alguna razón IME.
Michael K
1

No estoy de acuerdo con Joel en que el problema es pensar en múltiples niveles de abstracción per se, creo que es más que los punteros y la recursividad son dos buenos ejemplos de problemas que requieren un cambio en el modelo mental que tienen las personas sobre cómo funcionan los programas.

Los punteros son, creo, el caso más simple para ilustrar. Tratar con punteros requiere un modelo mental de ejecución de programas que explique la forma en que los programas funcionan realmente con direcciones de memoria y datos. Mi experiencia ha sido que muchas veces los programadores ni siquiera han pensado en esto antes de aprender sobre los punteros. Incluso si lo saben en sentido abstracto, no lo han adoptado en su modelo cognitivo de cómo funciona un programa. Cuando se introducen los punteros, se requiere un cambio fundamental en la forma en que piensan sobre cómo funciona el código.

La recursión es problemática porque hay dos bloques conceptuales para comprender. El primero es a nivel de máquina, y al igual que los punteros, se puede superar desarrollando una buena comprensión de cómo se almacenan y ejecutan realmente los programas. El otro problema con la recursividad es, creo, que las personas tienen una tendencia natural a tratar de deconstruir un problema recursivo en uno no recursivo, lo que enturbia la comprensión de una función recursiva como gestalt. Este es un problema con personas que tienen una base matemática insuficiente o un modelo mental que no vincula la teoría matemática con el desarrollo de programas.

La cuestión es que no creo que los indicadores y la recursividad sean las dos únicas áreas problemáticas para las personas atrapadas en un modelo mental insuficiente. El paralelismo parece ser otra área en la que algunas personas simplemente se quedan atrapadas y tienen dificultades para adaptar su modelo mental para tener en cuenta, es solo que a menudo los punteros y la recursividad son fáciles de evaluar en una entrevista.

Cercerilla
fuente
1
  DATA    |     CODE
          |
 pointer  |   recursion    SELF REFERENTIAL
----------+---------------------------------
 objects  |   macro        SELF MODIFYING
          |
          |

El concepto de datos y código autorreferenciales subyace a la definición de punteros y recursividad, respectivamente. Desafortunadamente, la exposición generalizada a los lenguajes de programación imperativos ha llevado a los estudiantes de ciencias de la computación a creer que deben comprender la implementación a través del comportamiento operativo de sus tiempos de ejecución cuando deben confiar este misterio al aspecto funcional del lenguaje. Sumar todos los números hasta cien parece una simple cuestión de comenzar con uno y agregarlo al siguiente en la secuencia y hacerlo al revés con la ayuda de funciones circulares autorreferenciales parece perverso e incluso peligroso para muchos no acostumbrados a la seguridad de funciones puras.

El concepto de datos y código auto modificables subyace a la definición de objetos (es decir, datos inteligentes) y macros, respectivamente. Menciono estos, ya que son aún más difíciles de entender, especialmente cuando se espera una comprensión operativa del tiempo de ejecución de una combinación de los cuatro conceptos, por ejemplo, una macro que genera un conjunto de objetos que implementa un analizador recursivo decente con la ayuda de un árbol de punteros. . En lugar de rastrear paso a paso toda la operación del estado del programa a través de cada capa de abstracción a la vez, los programadores imperativos deben aprender a confiar en que sus variables solo se asignan una vez dentro de las funciones puras y que las invocaciones repetidas de la misma función pura con los mismos argumentos siempre producen el mismo resultado (es decir, transparencia referencial), incluso en un lenguaje que también admite funciones impuras, como Java. Correr en círculos después del tiempo de ejecución es un esfuerzo infructuoso. La abstracción debería simplificarse.

Poco competitivo
fuente
-1

Muy similar a la respuesta de Anon.
Además de las dificultades cognitivas para los novatos, tanto los punteros como la recursividad son muy poderosos y se pueden usar de forma críptica.

La desventaja de un gran poder es que te dan un gran poder para arruinar tu programa de manera sutil.
Almacenar un valor falso en una variable normal es bastante malo, pero almacenar algo falso en un puntero puede causar que ocurran todo tipo de cosas catastróficas demoradas.
Y lo que es peor, esos efectos pueden cambiar a medida que intenta diagnosticar / depurar cuál es la causa del comportamiento extraño del programa.

Del mismo modo con la recursividad. Puede ser una forma muy poderosa de organizar cosas complicadas, al introducir el truco en la estructura de datos ocultos (pila).
Pero, si se hace algo sutilmente incorrecto, puede ser difícil darse cuenta de lo que está sucediendo.

Omega Centauri
fuente