“Es fácil razonar sobre”: ¿qué significa eso? [cerrado]

47

He escuchado muchas veces cuando otros desarrolladores usan esa frase para "anunciar" algunos patrones o desarrollar mejores prácticas. La mayoría de las veces esta frase se usa cuando se habla de los beneficios de la programación funcional.

La frase "Fácil de razonar sobre" se ha utilizado tal como está, sin ninguna explicación o ejemplo de código. Entonces, para mí, se convierte en la próxima palabra "buzz", que los desarrolladores más "experimentados" usan en sus charlas.

Pregunta: ¿Puede proporcionar algunos ejemplos de "No es fácil razonar acerca de", por lo que puede compararse con ejemplos "Fácil de razonar sobre"?

    
pregunta Fabio 20.06.2017 - 11:06

11 respuestas

58

En mi opinión, la frase "fácil de razonar" se refiere al código que es fácil de "ejecutar en tu cabeza".

Cuando se mira un fragmento de código, si es breve, está claramente escrito, con buenos nombres y una mínima mutación de valores, entonces trabajar mentalmente a través de lo que hace el código es una tarea (relativamente) fácil.

Una pieza larga de código con nombres pobres, variables que cambian constantemente de valor y bifurcaciones complicadas normalmente requerirá, por ejemplo, un lápiz y una hoja de papel para ayudar a mantener un registro del estado actual. Por lo tanto, tal código no se puede trabajar fácilmente solo en tu cabeza, por lo que no es fácil razonar sobre ese código.

    
respondido por el David Arno 20.06.2017 - 11:13
46

Es fácil razonar sobre un mecanismo o pieza de código cuando necesita tener en cuenta algunas cosas para predecir lo que hará, y las cosas que debe tener en cuenta están fácilmente disponibles.

Las funciones verdaderas sin efectos secundarios y sin estado son fáciles de razonar porque la salida está completamente determinada por la entrada, que se encuentra en los parámetros.

Por el contrario, un objeto con estado es mucho más difícil de razonar, porque debe tener en cuenta en qué estado se encuentra el objeto cuando se llama a un método, lo que significa que tiene que pensar qué otras situaciones podrían llevar al objeto. estar en un estado particular.

Lo que es peor aún son las variables globales: para razonar sobre el código que lee una variable global, debe comprender en qué lugar del código se puede establecer esa variable y por qué, y puede que ni siquiera sea fácil encontrar todos esos lugares.

Lo más difícil de razonar es la programación de subprocesos múltiples con estado compartido, porque no solo tiene un estado, sino que tiene varios hilos que lo cambian al mismo tiempo, por lo que debe razonar sobre lo que hace un código cuando lo ejecuta Un hilo tiene que permitir la posibilidad de que en cada punto de ejecución, algún otro hilo (¡o varios de ellos!) se esté ejecutando en casi cualquier otra parte del código y cambie los datos en los que está trabajando justo debajo de su los ojos En teoría, eso se puede manejar con mutexes / monitores / secciones críticas / como se llame, pero en la práctica ningún simple humano puede hacerlo de manera confiable a menos que limite drásticamente el estado compartido y / o el paralelismo a muy pequeños Secciones del código.

    
respondido por el Michael Borgwardt 20.06.2017 - 14:41
9

En el caso de la programación funcional, el significado de "fácil de razonar" es principalmente que es determinista. Con eso, me refiero a que una entrada dada siempre conducirá a la misma salida. Puedes hacer lo que quieras con el programa, siempre que no toques esa parte del código, no se romperá.

Por otro lado, la OO suele ser más difícil de razonar porque la "salida" producida depende del estado interno de cada objeto involucrado. La forma típica en que se manifiesta son los efectos secundarios inesperados: cuando se cambia una parte del código, una parte aparentemente no relacionada se rompe.

... la desventaja de la programación funcional es, por supuesto, que en la práctica, gran parte de lo que se quiere hacer es IO y estado de administración.

Sin embargo, hay muchas otras cosas que son más difíciles de razonar, y estoy de acuerdo con @Kilian en que la concurrencia es un buen ejemplo. Sistemas distribuidos también.

    
respondido por el dagnelies 20.06.2017 - 13:24
6

Evitar una discusión más amplia y abordar la pregunta específica:

  

¿Puede proporcionar algunos ejemplos de "No es fácil de razonar", por lo que puede compararse con los ejemplos de "Fácil de razonar"?

Lo remito a "La historia de Mel, un programador real" , una pieza del folclore de programadores que data de 1983 y por lo tanto cuenta como 'leyenda', para nuestra profesión.

Cuenta la historia de un programador que escribe código que prefiere las técnicas arcanas siempre que sea posible, incluido el código autorreferencial y el modificador personal, y la explotación deliberada de errores de la máquina:

  

un bucle infinito aparente, de hecho, había sido codificado de tal manera que   tomar ventaja de un error de desbordamiento de acarreo. Añadiendo 1 a una instrucción   que se decodificó como "Carga desde la dirección x" que normalmente daba como resultado "Carga desde   dirección x + 1 ". Pero cuando x ya era la dirección más alta posible, no   solo se redujo la dirección a cero, pero se llevó un 1 a la   bits desde los cuales se leería el código de operación, cambiando el código de operación de   "cargar de" a "saltar a" para que la instrucción completa cambie de   "cargar desde la última dirección" hasta "saltar a la dirección cero".

Este es un ejemplo de código "difícil de razonar".

Por supuesto, Mel no estaría de acuerdo ...

    
respondido por el AakashM 21.06.2017 - 13:58
5

Puedo dar un ejemplo, y uno muy común.

Considere el siguiente código C #.

// items is List<Item>
var names = new List<string>();
for (var i = 0; i < items.Count; i++)
{
    var item = items[i];
    var mangled = MyMangleFunction(item.Name);
    if (mangled.StartsWith("foo"))
    {
        names.Add(mangled);
    }
}

Ahora considera esta alternativa.

// items is List<Item>
var names = items
    .Select(item => MyMangleFunction(item.Name))
    .Where(s => s.StartsWith("foo"))
    .ToList();

En el segundo ejemplo, sé exactamente lo que hace este código de un vistazo. Cuando veo Select , sé que una lista de elementos se está convirtiendo en una lista de otra cosa. Cuando veo Where , sé que ciertos elementos se están filtrando. De un vistazo, puedo entender qué es names y hacer un uso efectivo de él.

Cuando veo un bucle for , no tengo idea de lo que está pasando hasta que realmente leo el código. Y a veces tengo que rastrearlo para asegurarme de que he tenido en cuenta todos los efectos secundarios. Tengo que hacer un poco de trabajo para llegar a comprender qué son los nombres (más allá de la definición de tipo) y cómo usarlos de manera efectiva. Por lo tanto, el primer ejemplo es más difícil de razonar que el segundo.

En última instancia, ser fácil de razonar aquí también depende de la comprensión de los métodos LINQ Select y Where . Si no los conoce, el segundo código es más difícil de razonar inicialmente. Pero solo pagas el costo para entenderlos una vez. Usted paga el costo para comprender un bucle for cada vez que usa uno y otra vez cada vez que cambia. A veces vale la pena pagar el costo, pero por lo general es mucho más importante ser "más fácil de razonar".

    
respondido por el Kasey Speakman 21.06.2017 - 17:09
2

Una frase relacionada es (parafraseando),

  

No es suficiente que el código tenga " sin errores obvios ": en su lugar, debería tener " obviamente no hay errores ".

Un ejemplo de relativamente "fácil de razonar" podría ser RAII .

Otro ejemplo podría ser evitar abrazo mortal : si puede mantener un bloqueo y adquirir otro bloqueo, y hay muchos de bloqueos, es difícil estar seguro de que no hay un escenario en el que pueda ocurrir un abrazo mortal. Agregar una regla como "solo hay un bloqueo (global)" o "no se le permite adquirir un segundo bloqueo mientras se mantiene un primer bloqueo", hace que el sistema sea relativamente fácil de razonar.

    
respondido por el ChrisW 20.06.2017 - 19:48
2

El quid de la programación es el análisis de casos. Alan Perlis comentó esto en Epigram # 32: Los programadores no deben medirse por su ingenio y su lógica, sino por la integridad del análisis de su caso.

Una situación es fácil de razonar sobre si el análisis de caso es fácil. Esto significa que hay pocos casos a considerar o, en su defecto, pocos casos especiales ; puede haber algunos espacios grandes de casos, pero que se colapsan debido a algunas regularidades, o sucumben a una técnica de razonamiento como la inducción.

Una versión recursiva de un algoritmo, por ejemplo, suele ser más fácil de razonar que una versión imperativa, porque no contribuye con casos superfluos que surgen a través de la mutación de las variables de estado de apoyo que no aparecen en la versión recursiva . Además, la estructura de la recursión es tal que se ajusta a un patrón matemático de prueba por inducción. No tenemos que considerar las complejidades como las variantes de bucle y las precondiciones estrictas más débiles y todo eso.

Otro aspecto de esto es la estructura del espacio del caso. Es más fácil razonar acerca de una situación que tiene una división plana, o en su mayoría plana en casos comparados con una situación jerárquica de casos: casos con subcasos y subcasos, etc.

Una propiedad de los sistemas que simplifica el razonamiento es ortogonalidad : esta es la propiedad de que los casos que rigen los subsistemas permanecen independientes cuando se combinan esos subsistemas. Ninguna combinación da lugar a "casos especiales". Si algo de cuatro casos se combina con algo de tres casos ortogonalmente, hay doce casos, pero idealmente cada caso es una combinación de dos casos que permanecen independientes. En cierto sentido, no hay realmente doce casos; las combinaciones son simplemente "fenómenos de caso emergente" de los que no tenemos que preocuparnos. Lo que esto significa es que todavía tenemos cuatro casos en los que podemos pensar sin considerar los otros tres en el otro subsistema, y viceversa. Si algunas de las combinaciones tienen que ser especialmente identificadas y dotadas de lógica adicional, entonces el razonamiento es más difícil. En el peor de los casos, cada combinación tiene un manejo especial, y luego hay doce nuevos casos, que se suman a los cuatro y tres originales.

    
respondido por el Kaz 21.06.2017 - 03:56
0

Claro. Tomar concurrencia:

Secciones críticas aplicadas por mutexes: fáciles de entender porque solo hay un principio (dos subprocesos de ejecución no pueden ingresar a la sección crítica simultáneamente), pero son propensos a la ineficiencia y al interbloqueo.

Modelos alternativos, por ejemplo, Programación sin bloqueo o actores: potencialmente mucho más elegante y potente, pero tremendamente difícil de entender, porque ya no puedes confiar en conceptos (aparentemente) fundamentales como "ahora escribe este valor en ese lugar" .

Ser fácil de razonar es el aspecto uno de un método. Pero elegir qué método utilizar requiere considerar todos los aspectos de todos en combinación.

    
respondido por el Kilian Foth 20.06.2017 - 11:12
0

Limitemos la tarea al razonamiento formal. Porque el razonamiento humorístico o inventivo o poético tiene diferentes leyes.

Aun así, la expresión está poco definida y no se puede establecer de manera estricta. Pero no significa que deba permanecer tan oscuro para nosotros. Imaginemos que una estructura es pasar una prueba y obtener marcas para diferentes puntos. Las buenas calificaciones para CADA punto significan que la estructura es conveniente en todos los aspectos y, por lo tanto, es "fácil de razonar".

La estructura "Fácil de razonar sobre" debería obtener buenas calificaciones para lo siguiente:

  • Los términos internos tienen nombres razonables, fácilmente distinguibles y definidos. Si los elementos tienen alguna jerarquía, la diferencia entre los nombres principales y secundarios debería ser diferente de la diferencia entre los nombres de los hermanos.
  • El número de tipos de elementos estructurales es bajo
  • Los tipos de elementos estructurales usados son cosas fáciles a las que estamos acostumbrados.
  • Los elementos apenas comprensibles (recursiones, meta pasos, geometría de 4+ dimensiones ...) están aislados, no directamente combinados entre sí. (Por ejemplo, si intenta pensar en un cambio de regla recursional para 1,2,3,4 ... n ... cubos dimensionales, será muy complicado. Pero si transfiere cada una de estas reglas a alguna fórmula dependiendo de n, tendrá una fórmula por separado para cada n-cubo y una regla de recursión por separado para dicha fórmula. Y es que dos estructuras por separado se pueden pensar fácilmente)
  • Los tipos de elementos estructurales son obviamente diferentes (por ejemplo, al no usar matrices mixtas a partir de 0 y de 1)

¿Es la prueba subjetiva? Sí, naturalmente lo es. Pero la expresión en sí misma es subjetiva, también. Lo que es fácil para una persona, no lo es para otra. Por lo tanto, las pruebas deben ser diferentes para los diferentes dominios.

    
respondido por el Gangnus 20.06.2017 - 16:31
0

La idea de que es posible razonar acerca de los lenguajes funcionales proviene de su historia, específicamente ML que se desarrolló como un lenguaje de programación análogo a las construcciones que la lógica para funciones computables usó para razonar. La mayoría de los lenguajes funcionales están más cerca de los cálculos de programación formal que los imperativos, por lo que la traducción del código a la entrada de un sistema de sistema de razonamiento es menos onerosa.

Para un ejemplo de un sistema de razonamiento, en el cálculo pi, cada ubicación de memoria mutable en un lenguaje imperativo debe representarse como un proceso paralelo separado, mientras que una secuencia de operaciones funcionales es un proceso único. Cuarenta años después de LFC Prover, estamos trabajando con GB de RAM, por lo que tener cientos de procesos es un problema menor. He utilizado el cálculo de pi para eliminar puntos muertos potenciales de unos pocos cientos de líneas de C ++, a pesar de que la representación tiene cientos de el razonador procesó el espacio de estado en aproximadamente 3 GB y solucionó un error intermitente. Esto hubiera sido imposible en los años 70 o habría requerido una supercomputadora a principios de los 90, mientras que el espacio estatal de un programa de lenguaje funcional de tamaño similar era lo suficientemente pequeño como para razonar en ese entonces.

De las otras respuestas, la frase se está convirtiendo en una frase de moda, aunque la ley de Moore erosiona gran parte de la dificultad que dificulta la razón acerca de los lenguajes imperativos.

    
respondido por el Pete Kirkham 21.06.2017 - 23:27
-2

Fácil de razonar es un término culturalmente específico, por lo que es tan difícil encontrar ejemplos concretos. Es un término que está anclado a las personas que deben hacer el razonamiento.

"Fácil de razonar acerca de" es en realidad una frase muy autodescriptiva. Si uno está mirando el código y quiere razonar lo que hace, es fácil =)

Está bien, rompiéndolo. Si estás buscando en el código, normalmente quieres que haga algo. Quieres asegurarte de que hace lo que crees que debería hacer. Así que desarrollas teorías sobre lo que debería estar haciendo el código, y luego razonas al respecto para tratar de argumentar por qué el código realmente funciona. Intenta pensar en el código como un ser humano (en lugar de en una computadora) e intenta racionalizar los argumentos sobre lo que puede hacer el código.

El peor de los casos para "fácil de razonar" es cuando la única forma de entender lo que hace el código es ir línea por línea a través del código como una máquina de Turing para todas las entradas. En este caso, la única manera de razonar cualquier cosa sobre el código es convertirse en una computadora y ejecutarla en su cabeza. Estos ejemplos del peor de los casos se ven fácilmente en concursos de programación obsoletos, como estas 3 líneas de PERL que descifran RSA:

#!/bin/perl -sp0777i<X+d*lMLa^*lN%0]dsXx++lMlN/dsM0<j]dsj
$/=unpack('H*',$_);$_='echo 16dio\U$k"SK$/SM$n\EsN0p[lN*1
lK[d2%Sa2/d0$^Ixp"|dc';s/\W//g;$_=pack('H*',/((..)*)$/)

En cuanto a fácil de razonar, de nuevo, el término es altamente cultural. Tienes que considerar:

  • ¿Qué habilidades tiene el razonador? ¿Cuánta experiencia?
  • ¿Qué tipo de preguntas podría tener el razonador sobre el código?
  • ¿Qué tan seguro debe estar el razonador?

Cada uno de estos afecta a "fácil de razonar" de manera diferente. Tomemos como ejemplo las habilidades del razonador. Cuando comencé en mi empresa, me recomendaron que desarrollara mis scripts en MATLAB porque es "fácil de razonar". ¿Por qué? Bueno, todos en la compañía sabían MATLAB. Si escogiera un idioma diferente, sería más difícil para mí entenderme. No importa que la legibilidad de MATLAB sea atroz para algunas tareas, simplemente porque no fue diseñado para ellas. Más tarde, a medida que mi carrera progresaba, Python se hizo más y más popular. De repente, el código MATLAB se volvió "difícil de razonar" y Python era el idioma de preferencia para escribir código sobre el que era fácil razonar.

También considera qué idomas puede tener el lector. Si puede confiar en que su lector reconozca una FFT en una sintaxis particular, es "más fácil razonar sobre" el código si se adhiere a esa sintaxis. Les permite ver el archivo de texto como un lienzo sobre el que pintó un FFT, en lugar de tener que meterse en los detalles esenciales. Si está utilizando C ++, descubra cuánto se sienten cómodos sus lectores con la biblioteca std . ¿Cuánto les gusta la programación funcional? Algunos de los modismos que salen de las bibliotecas de contenedores dependen mucho del estilo idomático que prefiera.

También es importante comprender qué tipo de preguntas le pueden interesar al lector. ¿Sus lectores están más preocupados por la comprensión superficial del código, o están buscando errores en las entrañas?

La certeza que debe tener el lector es realmente interesante. En muchos casos, el razonamiento confuso es en realidad suficiente para que el producto salga por la puerta. En otros casos, como el software de vuelo FAA, el lector querrá tener un razonamiento rígido. Me encontré con un caso en el que defendí el uso de RAII para una tarea en particular, porque "puedes configurarlo y olvidarte de él ... hará lo correcto". Me dijeron que estaba equivocado sobre eso. Los que iban a razonar en este código no eran el tipo de personas que "solo quieren olvidarse de los detalles". Para ellos, RAII era más como un chad colgante, obligándolos a pensar en todas las cosas que pueden suceder cuando dejas el alcance. Los que estaban leyendo ese código en realidad preferían las llamadas a funciones explícitas al final del alcance para que pudieran estar seguros de que el programador lo pensó.

    
respondido por el Cort Ammon 20.06.2017 - 19:54