Al leer el famoso SICP, descubrí que los autores parecen bastante reacios a presentar la declaración de asignación al Esquema en el Capítulo 3. Leí el texto y comprendo por qué se sienten así.
Como Scheme es el primer lenguaje de programación funcional del que conozco algo, me sorprende que haya algunos lenguajes de programación funcionales (no Scheme, por supuesto) que pueden funcionar sin asignaciones.
Usemos el ejemplo que ofrece el libro, el bank account
ejemplo. Si no hay una declaración de asignación, ¿cómo se puede hacer esto? ¿Cómo cambiar la balance
variable? Lo pregunto porque sé que hay algunos llamados lenguajes funcionales puros y, según la teoría completa de Turing, esto también se puede hacer.
Aprendí C, Java, Python y uso muchas tareas en cada programa que escribí. Entonces es realmente una experiencia reveladora. Realmente espero que alguien pueda explicar brevemente cómo se evitan las asignaciones en esos lenguajes de programación funcionales y qué profundo impacto (si lo hay) tiene en estos lenguajes.
El ejemplo mencionado anteriormente está aquí:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
Esto cambió el balance
por set!
. Para mí, se parece mucho a un método de clase para cambiar el miembro de la clase balance
.
Como dije, no estoy familiarizado con los lenguajes de programación funcionales, por lo que si digo algo malo sobre ellos, no dudes en señalarlo.
set!
u otras funciones que terminen con a!
. Una vez que te sientas cómodo con eso, la transición a FP pura debería ser más fácil.Respuestas:
No puede cambiar las variables sin algún tipo de operador de asignación.
No exactamente. Si un idioma es Turing completo, eso significa que puede calcular cualquier cosa que cualquier otro lenguaje completo de Turing pueda calcular. No significa que tenga que tener todas las características que tienen otros idiomas.
No es una contradicción que un lenguaje de programación completo de Turing no tenga forma de cambiar el valor de una variable, siempre que para cada programa que tenga variables mutables, pueda escribir un programa equivalente que no tenga variables mutables (donde "equivalente" significa que calcula lo mismo). Y, de hecho, cada programa se puede escribir de esa manera.
Con respecto a su ejemplo: en un lenguaje puramente funcional, simplemente no podría escribir una función que devuelva un saldo de cuenta diferente cada vez que se llame. Pero aún podría reescribir cada programa, que utiliza dicha función, de una manera diferente.
Como solicitó un ejemplo, consideremos un programa imperativo que utiliza su función de extracción / extracción (en pseudocódigo). Este programa le permite al usuario retirarse de una cuenta, depositar en ella o consultar la cantidad de dinero en la cuenta:
Aquí hay una manera de escribir el mismo programa sin usar variables mutables (no me molestaré con IO transparente referencial porque la pregunta no era sobre eso):
La misma función también podría escribirse sin usar la recursión mediante el uso de un pliegue sobre la entrada del usuario (que sería más idiomático que la recursividad explícita), pero aún no sé si está familiarizado con los pliegues, así que lo escribí en un de manera que no use nada que aún no conozca.
fuente
newBalance = startingBalance + sum(deposits) - sum(withdrawals)
.Tienes razón en que se parece mucho a un método en un objeto. Eso es porque eso es esencialmente lo que es. La
lambda
función es un cierre que atrae la variable externabalance
a su alcance. Tener múltiples cierres que se cierran sobre las mismas variables externas y tener múltiples métodos en el mismo objeto son dos abstracciones diferentes para hacer exactamente lo mismo, y cualquiera de las dos puede implementarse en términos de la otra si comprende ambos paradigmas.La forma en que los lenguajes funcionales puros manejan el estado es haciendo trampa. Por ejemplo, en Haskell si desea leer la entrada de una fuente externa, (que no es determinista, por supuesto, y no necesariamente dará el mismo resultado dos veces si la repite), utiliza un truco de mónada para decir "hemos obtuve esta otra variable simulada que representa el estado de todo el resto del mundo , y no podemos examinarla directamente, pero leer la entrada es una función pura que toma el estado del mundo exterior y devuelve la entrada determinista que ese estado exacto siempre se representará, más el nuevo estado del mundo exterior ". (Esa es una explicación simplificada, por supuesto. Leer sobre la forma en que funciona realmente te romperá el cerebro).
O, en el caso de su problema de cuenta bancaria, en lugar de asignar un nuevo valor a la variable, puede devolver el nuevo valor como resultado de la función, y luego la persona que llama tiene que lidiar con él en un estilo funcional, generalmente recreando cualquier información que hace referencia a ese valor con una nueva versión que contiene el valor actualizado. (Esta no es una operación tan voluminosa como podría parecer si sus datos están configurados con el tipo correcto de estructura de árbol).
fuente
b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))
, puede hacerb = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));
dondewithdraw
simplemente se define comowithdraw(balance, amount) = balance - amount
.Los "operadores de asignación múltiple" son un ejemplo de una característica del lenguaje que, en términos generales, tiene efectos secundarios y es incompatible con algunas propiedades útiles de los lenguajes funcionales (como la evaluación diferida).
Sin embargo, eso no significa que la asignación en general sea incompatible con un estilo de programación funcional puro (ver esta discusión, por ejemplo), ni tampoco que no se pueda construir una sintaxis que permita acciones que parecen tareas en general, pero se implementan sin efectos secundarios. Sin embargo, crear ese tipo de sintaxis y escribir programas eficientes en él lleva mucho tiempo y es difícil.
En su ejemplo específico, tiene razón: ¡el conjunto! El operador es una tarea. Es no un operador libre de efectos secundarios, y es un lugar donde se rompe Esquema con un enfoque puramente funcional a la programación.
En última instancia, cualquier lenguaje puramente funcional va a tener que romper con el enfoque puramente funcional en algún momento - la gran mayoría de los programas útiles hacerlo tiene efectos secundarios. La decisión de dónde hacerlo suele ser una cuestión de conveniencia, y los diseñadores de lenguaje intentarán darle al programador la mayor flexibilidad para decidir dónde romper con un enfoque puramente funcional, según corresponda para su programa y dominio del problema.
fuente
En un lenguaje puramente funcional, uno programaría un objeto de cuenta bancaria como una función de transformador de flujo. El objeto se considera como una función desde un flujo infinito de solicitudes de los propietarios de la cuenta (o de quien sea) hasta un flujo potencialmente infinito de respuestas. La función comienza con un saldo inicial y procesa cada solicitud en el flujo de entrada para calcular un nuevo saldo, que luego se retroalimenta a la llamada recursiva para procesar el resto del flujo. (Recuerdo que SICP discute el paradigma del transformador de flujo en otra parte del libro).
Una versión más elaborada de este paradigma se llama "programación reactiva funcional" discutida aquí en StackOverflow .
La ingenua manera de hacer transformadores de corriente tiene algunos problemas. Es posible (de hecho, bastante fácil) escribir programas con errores que mantengan todas las solicitudes antiguas, desperdiciando espacio. Más en serio, es posible hacer que la respuesta a la solicitud actual dependa de solicitudes futuras. Las soluciones a estos problemas se están trabajando actualmente. Neel Krishnaswami es la fuerza detrás de ellos.
Descargo de responsabilidad : no pertenezco a la iglesia de la programación funcional pura. De hecho, no pertenezco a ninguna iglesia :-)
fuente
No es posible hacer que un programa sea 100% funcional si se supone que debe hacer algo útil. (Si no se necesitan efectos secundarios, entonces todo el pensamiento podría haberse reducido a un tiempo de compilación constante) Al igual que el ejemplo de retirada, puede hacer que la mayoría de los procedimientos funcionen, pero eventualmente necesitará procedimientos que tengan efectos secundarios (entrada del usuario, salida a la consola). Dicho esto, puede hacer que la mayor parte de su código sea funcional y esa parte será fácil de probar, incluso automáticamente. Luego crea un código imperativo para hacer la entrada / salida / base de datos / ... que necesitaría depuración, pero mantener la mayor parte del código limpio no será demasiado trabajo. Usaré su ejemplo de retirada:
Es posible hacer lo mismo en casi cualquier idioma y producir los mismos resultados (menos errores), aunque es posible que tenga que establecer variables temporales dentro de un procedimiento e incluso mutar cosas, pero eso no importa demasiado mientras el procedimiento en realidad actúa funcional (solo los parámetros determinan el resultado). Creo que te conviertes en un mejor programador en cualquier idioma después de haber programado un poco de LISP :)
fuente
La asignación es una operación incorrecta porque divide el espacio de estado en dos partes, antes de la asignación y después de la asignación. Esto causa dificultades para rastrear cómo se cambian las variables durante la ejecución del programa. Las siguientes cosas en lenguajes funcionales están reemplazando las asignaciones:
fuente