f# functional-programming pointfree

f# - ¿Cuáles son las ventajas y desventajas del estilo "sin puntos" en la programación funcional?



functional-programming pointfree (3)

Sé que en algunos idiomas (¿Haskell?) El esfuerzo es lograr un estilo sin puntos, o nunca referirse explícitamente a los argumentos de las funciones por su nombre. Este es un concepto muy difícil de dominar, pero podría ayudarme a comprender cuáles son las ventajas (o incluso desventajas) de ese estilo. ¿Alguien puede explicar?


Aunque me atrae el concepto sin puntos y lo uso para algunas cosas, y estoy de acuerdo con todos los aspectos positivos mencionados anteriormente, encontré estas cosas como negativas (algunas se detallan más arriba):

  1. La notación más corta reduce la redundancia; en una composición muy estructurada (estilo ramda.js, o sin puntos en Haskell, o en cualquier otro lenguaje concatenativo), la lectura del código es más compleja que escanear linealmente un conjunto de enlaces const y usar un marcador de símbolos para ver qué enlace entra en qué otro cálculo aguas abajo. Además de la estructura árbol contra lineal, la pérdida de nombres de símbolos descriptivos hace que la función sea difícil de captar intuitivamente. Por supuesto, tanto la estructura de árbol como la pérdida de enlaces nombrados también tienen muchos aspectos positivos, por ejemplo, las funciones se sentirán más generales, no vinculadas a algún dominio de aplicación a través de los nombres de símbolo elegidos, y la estructura de árbol estará semánticamente incluso si los enlaces están distribuidos, y pueden ser comprendidos secuencialmente (lisp let / let * style).

  2. Sin puntos es más simple cuando solo fluye o compone una serie de funciones, ya que esto también resulta en una estructura lineal que los humanos encontramos fácil de seguir. Sin embargo, enhebrar algunos cálculos provisionales a través de múltiples destinatarios es tedioso. Hay todo tipo de envoltura en tuplas, lentes y otros mecanismos meticulosos para hacer solo un cálculo accesible, que de lo contrario sería simplemente el uso múltiple de algún enlace de valor. Por supuesto, la parte repetida se puede extraer como una función separada y tal vez sea una buena idea de todos modos, pero también hay argumentos para algunas funciones no cortas e incluso si se extrae, sus argumentos tendrán que enhebrarse de alguna manera a través de ambas aplicaciones, y luego puede haber una necesidad de memorizar la función para no repetir el cálculo. Uno usará un montón de converge , lens , memoize , useWidth , etc.

  3. JavaScript específico: más difícil de depurar por casualidad. Con un flujo lineal de encuadernaciones, es fácil agregar un punto de quiebre donde sea. Con el estilo sin puntos, incluso si se agrega un punto de interrupción de alguna manera, el flujo de valores es difícil de leer, por ejemplo. no puede simplemente consultar o desplazarse sobre alguna variable en la consola de desarrollo. Además, como sin puntos no es nativo en JS, las funciones de biblioteca de ramda.js o similares oscurecerán un poco la pila, especialmente con el currying obligatorio.

  4. Codifique la fragilidad, especialmente en sistemas de tamaño no trivial y en producción. Si entra una nueva pieza de requisitos, entonces entran en juego las desventajas anteriores (por ejemplo, es más difícil leer el código para el siguiente mantenedor que puede ser usted mismo algunas semanas después, y también más difícil rastrear el flujo de datos para la inspección). Pero lo más importante es que incluso un nuevo requisito aparentemente pequeño e inocente puede requerir una estructura completamente diferente del código. Se puede argumentar que es bueno que sea una representación nítida de lo nuevo, pero reescribir grandes franjas de código sin puntos consume mucho tiempo y no hemos mencionado las pruebas. Por lo tanto, se cree que la codificación basada en asignación léxica, menos estructurada y más flexible se puede reutilizar más rápidamente. Especialmente si la codificación es exploratoria, y en el dominio de datos humanos con convenciones extrañas (tiempo, etc.) que rara vez se pueden capturar con precisión del 100% y siempre puede haber una próxima solicitud para manejar algo más preciso o más según las necesidades del cliente, cualquiera que sea el método que conduzca a un pivoteo más rápido importa mucho.


Creo que el propósito es ser conciso y expresar cálculos en línea como una composición de funciones en lugar de pensar en entablar discusiones. Ejemplo simple (en F #) - dado:

let sum = List.sum let sqr = List.map (fun x -> x * x)

Usado como:

> sum [3;4;5] 12 > sqr [3;4;5] [9;16;25]

Podríamos expresar una función de "suma de cuadrados" como:

let sumsqr x = sum (sqr x)

Y use como:

> sumsqr [3;4;5] 50

O podríamos definirlo por tuberías x a través de:

let sumsqr x = x |> sqr |> sum

Escrito de esta manera, es obvio que x se pasa solo para ser "enhebrado" a través de una secuencia de funciones. La composición directa se ve mucho mejor:

let sumsqr = sqr >> sum

Esto es más conciso y es una forma diferente de pensar sobre lo que estamos haciendo; componer funciones en lugar de imaginar el proceso de discusiones fluyendo. No estamos describiendo cómo funciona sumsqr . Estamos describiendo lo que es .

PD: Una forma interesante de familiarizarse con la composición es intentar programar en un lenguaje concatenativo como Forth, Joy, Factor, etc. Se puede pensar que no son más que composición (Forth : sumsqr sqr sum ; ) en la que el el espacio entre las palabras es el operador de la composición .

PPS: Quizás otros podrían comentar sobre las diferencias de rendimiento. Me parece que la composición puede reducir la presión de GC al hacer más obvio para el compilador que no hay necesidad de producir valores intermedios como en el pipeline; ayudando a que el llamado problema de "deforestación" sea más manejable.


El estilo sin puntos es considerado por algunos autores como el mejor estilo de programación funcional. Para decirlo simplemente, una función de tipo t1 -> t2 describe una transformación de un elemento de tipo t1 a otro elemento de tipo t2 . La idea es que las funciones "puntuales" (escritas usando variables explícitas) enfaticen los elementos (cuando escribe /x -> ... x ... , está describiendo lo que está sucediendo con el elemento x ), mientras que "sin puntos" las funciones (expresadas sin usar variables) enfatizan la transformación en sí misma, como una composición de transformaciones más simples. Los defensores del estilo sin puntos argumentan que las transformaciones deberían ser el concepto central, y que la notación puntual, aunque fácil de usar, nos distrae de este noble ideal.

La programación funcional sin puntos ha estado disponible durante mucho tiempo. Ya lo sabían los lógicos que han estudiado la lógica combinatoria desde el trabajo seminal de Moses Schönfinkel en 1924, y ha sido la base para el primer estudio sobre lo que se convertiría en inferencia tipo ML por Robert Feys y ... Haskell Curry en la década de 1950.

La idea de crear funciones a partir de un conjunto expresivo de combinadores básicos es muy atractiva y se ha aplicado en varios dominios, como los lenguajes de manipulación de matriz derivados de APL , o las bibliotecas de combinador de analizadores, como Parsec de Haskell. Un notable defensor de la programación sin puntos es John Backus . En su discurso de 1978 "¿Se puede liberar la programación de Von Neumann Style?", Escribió:

La expresión lambda (con sus reglas de sustitución) es capaz de definir todas las posibles funciones computables de todos los tipos posibles y de cualquier cantidad de argumentos. Esta libertad y poder tiene sus desventajas, así como sus ventajas obvias. Es análogo al poder de las declaraciones de control sin restricciones en los lenguajes convencionales: con la libertad irrestricta viene el caos. Si uno constantemente inventa nuevas formas de combinación para adaptarse a la ocasión, como se puede hacer en el cálculo lambda, uno no se familiarizará con el estilo o las propiedades útiles de las pocas formas de combinación que son adecuadas para todos los propósitos. Del mismo modo que la programación estructurada evita muchas instrucciones de control para obtener programas con una estructura más simple, mejores propiedades y métodos uniformes para comprender su comportamiento, la programación funcional evita la expresión lambda, la sustitución y múltiples tipos de funciones. Por lo tanto, logra programas construidos con formas funcionales familiares con propiedades útiles conocidas. Estos programas son tan estructurados que su comportamiento a menudo puede ser entendido y probado mediante el uso mecánico de técnicas algebraicas similares a las utilizadas para resolver problemas de álgebra en la escuela secundaria.

Entonces aquí están. La principal ventaja de la programación sin puntos es que fuerzan un estilo de combinador estructurado que hace que el razonamiento ecuacional sea natural. El razonamiento ecuacional ha sido especialmente publicitado por los proponentes del movimiento "Squiggol" (ver [1] [2]), y de hecho usan una buena cantidad de combinators sin puntos y reglas de cálculo / reescritura / razonamiento.

Finalmente, una de las causas de la popularidad de la programación sin puntos entre los haskellites es su relación con la teoría de categorías . En la teoría de categorías, los morfismos (que podrían verse como "transformaciones entre objetos") son el objeto básico de estudio y computación. Si bien los resultados parciales permiten que el razonamiento en categorías específicas se realice en un estilo puntual, la forma común de construir, examinar y manipular flechas sigue siendo el estilo sin puntos, y otras sintaxis tales como diagramas de cuerdas también exhiben este "pointfreeness". Existen vínculos bastante estrechos entre las personas que abogan por los métodos de "álgebra de la programación" y los usuarios de categorías en la programación (por ejemplo, los autores del papel de banano [2] son ​​/ fueron categoristas incondicionales).

Puede que te interese la página de Pointfree de la wiki de Haskell.

La desventaja del estilo sin puntos es bastante obvio: puede ser realmente doloroso de leer. La razón por la que aún nos encanta usar variables, a pesar de los numerosos horrores del sombreado, la equivalencia alfa, etc., es que es una notación tan natural de leer y pensar. La idea general es que una función compleja (en un lenguaje de referencia transparente) es como un sistema de plomería complejo: las entradas son los parámetros, entran en algunas tuberías, se aplican a las funciones internas, se duplican ( /x -> (x,x) u olvidado ( /x -> () , la tubería no lleva a ninguna parte), etc. Y la notación variable está muy implícita sobre toda esa maquinaria: le da un nombre a la entrada, y nombres en las salidas (o cálculos auxiliares), pero no tiene que describir todo el plan de plomería, donde las tuberías pequeñas no serán un obstáculo para las más grandes, etc. La cantidad de tuberías dentro de algo tan pequeño como /(f,x,y) -> ((x,y), fxy) es increíble. Puedes seguir cada variable individualmente o leer cada nodo de plomería intermedio, pero nunca tienes que ver toda la maquinaria junta. Cuando utilizas un estilo sin puntos, todo es explícito, tienes que escribir todo y mirarlo luego, y algunas veces es simplemente feo.

PD: esta visión de la plomería está estrechamente relacionada con los lenguajes de programación de la pila, que son probablemente los lenguajes de programación menos útiles (apenas) en uso. Recomendaría intentar hacer algo de programación en ellos solo para sentirlo (como recomendaría la programación lógica). Ver Factor , Cat o el venerable Forth .