¿Existe realmente la inmutabilidad en la programación funcional?

9

Aunque trabajo como programador en mi vida diaria y uso todos los lenguajes de moda (Python, Java, C, etc.) todavía no tengo una visión clara de qué es la programación funcional. Por lo que he leído, una propiedad de los lenguajes funcionales es que las estructuras de datos son inmutables . Para mí, esto solo plantea muchas preguntas. Pero primero escribiré un poco de lo que entiendo de inmutabilidad y, si me equivoco, no dude en corregirme.

Mi comprensión de la inmutabilidad:

  • Cuando un programa se inicia, tiene estructuras de datos fijas con datos fijos.
  • No se pueden agregar nuevos datos a estas estructuras.
  • No hay variables en el código.
  • Simplemente puede "copiar" los datos ya calculados o los datos calculados actualmente
  • Debido a todo lo anterior, la inmutabilidad agrega una enorme complejidad espacial a un programa

Mis preguntas:

  1. Si se supone que las estructuras de datos permanecen como son (inmutables), ¿cómo demonios alguien agrega un nuevo elemento en una lista?
  2. ¿Cuál es el punto de tener un programa que no puede obtener datos nuevos? Supongamos que tiene un sensor conectado a su computadora que desea alimentar datos al programa. ¿Significaría eso que no podemos almacenar los datos entrantes en ningún lado?
  3. ¿Cómo es la programación funcional buena para el aprendizaje automático en ese caso? Dado que el aprendizaje automático se basa en el supuesto de actualizar la "percepción" de las cosas por parte del programa, almacenando así nuevos datos.
Pithikos
fuente
2
No estoy de acuerdo con usted cuando dice que no hay variables en el código funcional. Hay variables en el sentido matemático de "Una cantidad que puede asumir cualquiera de un conjunto de valores". No son mutables , claro, pero tampoco lo son en matemáticas.
Édouard
1
Creo que su confusión se debe a que está pensando en lenguajes funcionales de manera abstracta. Simplemente tome cualquier programa en Haskell, por ejemplo, un programa que lea una lista de números de la consola, lo ordene rápidamente y lo muestre, y descubra cómo funciona y cómo refuta sus sospechas. No hay forma de aclarar las cosas sin mirar ejemplos de programas reales en lugar de filosofar. Encontrarás muchos programas en cualquier tutorial de Haskell.
jkff
@jkff ¿Qué intentas decir? Que Haskel tiene características no funcionales. La pregunta no es sobre Haskell, sino sobre la programación funcional. ¿O estás afirmando que todo eso es funcional? ¿Cómo? Entonces, ¿qué debería estar mal con filosofar, como usted dice? ¿De qué manera la abstracción es confusa? La pregunta de OP es muy sensata.
babou
@babou Estoy tratando de decir que la mejor manera de entender cómo un lenguaje de programación puramente funcional puede implementar algoritmos y estructuras de datos de manera eficiente es mirar ejemplos de algoritmos y estructuras de datos implementados de manera eficiente en un lenguaje de programación funcional. Me parece que OP estaba tratando de entender cómo es conceptualmente posible: creo que la forma más rápida de entender eso es mirar ejemplos, en lugar de leer una explicación conceptual, sin importar cuán detallado sea.
jkff
Una forma de ver la programación funcional es decir que está programando sin efectos secundarios. Puede hacerlo en su idioma "de moda" de elección. Simplemente no evite todas las reasignaciones: por ejemplo, en Java, todas sus variables serán finales y todos sus métodos serán de solo lectura.
reinierpost

Respuestas:

10

Cuando un programa se inicia, tiene estructuras de datos fijas con datos fijos.

Esto es un poco un error. Tiene una forma fija y un conjunto fijo de reglas de reescritura, pero estas reglas de reescritura pueden explotar en algo mucho más grande. Por ejemplo, la expresión [1..100000000] en Haskell está representada por una cantidad muy pequeña de código, pero su forma normal es masiva.

No se pueden agregar nuevos datos a estas estructuras.

Si y no. El subconjunto puramente funcional de un lenguaje como Haskell o ML no puede obtener datos del mundo exterior, pero cualquier lenguaje para programación práctica tiene un mecanismo para insertar datos del mundo exterior en el subconjunto puramente funcional. En Haskell, esto se hace con mucho cuidado, pero en ML puedes hacerlo cuando quieras.

No hay variables en el código.

Esto es bastante cierto, pero no confunda esto con la idea de que no se puede nombrar nada. Nombra expresiones útiles todo el tiempo y las reutiliza constantemente. También tanto ML como Haskell, todos los Lisp que he probado, e híbridos como Scala, tienen un medio para crear variables. Simplemente no se usan comúnmente. Y nuevamente los subconjuntos puramente funcionales de tales lenguajes no los tienen.

Simplemente puede "copiar" los datos ya calculados o los datos calculados actualmente

Puede realizar el cálculo por reducción a la forma normal. Lo mejor que puede hacer es probablemente escribir programas en un lenguaje funcional para ver cómo realmente realizan los cálculos.

Por ejemplo, "suma [1..1000]" no es un cálculo que quiero realizar, pero Haskell lo hace con bastante facilidad. Le dimos una pequeña expresión que tenía significado para nosotros y Haskell nos dio el número correspondiente. Entonces definitivamente realiza el cálculo.

Si se supone que las estructuras de datos permanecen como son (inmutables), ¿cómo demonios alguien agrega un nuevo elemento en una lista?

No agrega un nuevo elemento a una lista, crea una nueva lista a partir de la anterior. Debido a que el antiguo no puede ser mutado, es perfectamente seguro usarlo en la nueva lista, o en cualquier otro lugar que desee. Se pueden compartir muchos más datos de forma segura en este esquema.

¿Cuál es el punto de tener un programa que no puede obtener datos nuevos? Supongamos que tiene un sensor conectado a su computadora que desea alimentar datos al programa. ¿Significaría eso que no podemos almacenar los datos entrantes en ningún lado?

En cuanto a la entrada del usuario, cualquier lenguaje de programación práctico tiene una forma de obtener la entrada del usuario. Esto pasa. Sin embargo, hay un subconjunto completamente funcional de estos idiomas en el que escribe la mayor parte de su código y cosecha las ventajas de esta manera.

¿Cómo es la programación funcional buena para el aprendizaje automático en ese caso? Dado que el aprendizaje automático se basa en el supuesto de actualizar la "percepción" de las cosas por parte del programa, almacenando así nuevos datos.

Este sería el caso del aprendizaje activo, pero la mayoría del aprendizaje automático con el que he trabajado (trabajo como un mono de código en un grupo de aprendizaje automático y lo he hecho durante algunos años) tiene un proceso de aprendizaje único en el que se cargan todos los datos de entrenamiento En seguida. Pero para el aprendizaje activo no puedes hacer cosas 100% puramente funcionales. Tendrá que leer algunos datos del mundo exterior.

Jake
fuente
Siento que ignoraste convenientemente lo que podría ser posiblemente el punto más importante en la publicación de @ Pithikos, que es el problema del espacio: los programas funcionales usan más espacio que los imperativos (no puedes escribir algoritmos in situ y tal)
user541686
2
Esto simplemente no es cierto. La falta de mutación se compensa en gran medida al compartir y, para colmo, la diferencia de tamaño a la que se refiere es extremadamente pequeña con los compiladores modernos. La mayoría del código en las listas de Haskell está eficazmente en su lugar o no usa memoria en absoluto.
Jake
1
Creo que tergiversas un poco ML. Sí, las E / S pueden ocurrir en cualquier lugar, pero la forma en que se introduce nueva información en las estructuras existentes está estrictamente controlada.
dfeuer
@Pithikos, hay variables por todo el lugar; son simplemente diferentes de lo que estás acostumbrado, como indicó Édouard. Y las cosas se asignan continuamente y se recolecta la basura. Una vez que ingrese a la programación funcional, tendrá una mejor idea de cómo funciona.
dfeuer
1
Es cierto que existen algoritmos que no tienen una implementación puramente funcional con la misma complejidad de tiempo que la implementación imperativa más conocida, por ejemplo, la estructura de datos Union-Find (y, um, arrays :)) Me imagino que también hay casos como este para el espacio complejidad. Pero estas son excepciones: los algoritmos / estructuras de datos más comunes tienen implementaciones con una complejidad equivalente de tiempo y espacio. Es una cuestión subjetiva de estilo de programación y (en un factor constante) de calidad del compilador.
jkff
4

La inmutabilidad o la mutabilidad no son conceptos que tengan sentido en la programación funcional.

El contexto computacional.

Esta es una muy buena pregunta que es un seguimiento interesante (no un duplicado) de otra reciente: ¿Cuál es la diferencia entre asignación, valoración y enlace de nombre?

En lugar de responder a sus declaraciones una por una, estoy tratando de darle una descripción estructurada de lo que está en juego.

Hay varios asuntos a considerar para responderle, que incluyen:

  • ¿Qué es un modelo de cálculo y qué conceptos tienen sentido para un modelo dado?

  • ¿Cuál es el significado de las palabras que está utilizando y cómo depende del contexto?

El estilo de programación funcional parece tonto porque lo ves con un ojo de programador imperativo. Pero es un paradigma diferente, y sus conceptos y percepción imperativos son extraños, fuera de lugar. Los compiladores no tienen tales prejuicios.

Pero la conclusión final es que es posible escribir programas de una manera puramente funcional, incluso para el aprendizaje automático, aunque la programación funcional no tiene el concepto de almacenar datos. Parece que no estoy de acuerdo en este punto con otras respuestas.

En la esperanza, algunos estarán interesados ​​a pesar de la extensión de esta respuesta.

Paradigmas computacionales

La pregunta es sobre la programación funcional (también conocida como programación aplicativa), un modelo específico de computación, cuyo representante teórico y más simple es el cálculo lambda.

Si se mantiene en un nivel teórico, hay muchos modelos de computación: la máquina de Turing (TM), la máquina de RAM y otros , el cálculo lambda, la lógica combinatoria, la teoría de funciones recursivas, los sistemas semi-Thue, etc. Se ha demostrado que los modelos son equivalentes en términos de lo que pueden abordar, y esa es la esencia de la tesis de Church-Turing .

Un concepto importante es reducir los modelos entre sí, que es la base para establecer las equivalencias que conducen a la tesis de Church-Turing. Visto desde la perspectiva de los programadores, reducir un modelo a otro es más o menos lo que generalmente se llama un compilador. Si toma la programación lógica como su modelo de computación, es bastante diferente del modelo proporcionado por la PC que compró en una tienda, y el compilador traduce los programas escritos en lenguaje de programación lógica al modelo computacional representado por su PC (más o menos la computadora RAM).

β

En la práctica, los lenguajes de programación que usamos tienden a mezclar conceptos de diferentes orígenes teóricos, tratando de hacerlo para que partes seleccionadas de un programa puedan beneficiarse de las propiedades de algún modelo cuando sea apropiado. Del mismo modo, las personas que construyen sistemas pueden elegir diferentes idiomas para diferentes componentes, para adaptarse mejor al idioma para la tarea en cuestión.

Por lo tanto, rara vez ve un paradigma de programación en estado puro en un lenguaje de programación. Los lenguajes de programación todavía se clasifican de acuerdo con el paradigma dominante, pero las propiedades del lenguaje pueden verse afectadas cuando se involucran conceptos de otros paradigmas, a menudo borrando las distinciones y los problemas conceptuales.

Por lo general, los lenguajes como Haskell y ML o CAML se consideran funcionales, pero pueden permitir un comportamiento imperativo ... De lo contrario, ¿por qué uno hablaría del " subconjunto puramente funcional "?

Entonces uno puede afirmar que puede hacer esto o aquello en mi lenguaje de programación funcional, pero en realidad no responde una pregunta sobre programación funcional cuando se basa en lo que se puede considerar extra funcional.

Las respuestas deberían estar más precisamente relacionadas con un paradigma específico, sin los extras.

¿Qué es una variable?

Otro problema es el uso de la terminología. En matemáticas, una variable es una entidad que representa un valor indeterminado en algún dominio. Se utiliza para diversos fines. Utilizado en una ecuación, puede representar cualquier valor tal que se verifique la ecuación. Esta visión se utiliza en la programación lógica bajo el nombre de " variable lógica ", probablemente porque la variable nombre ya tenía otro significado cuando se desarrolló la programación lógica.

En la programación imperativa tradicional, una variable se entiende como algún tipo de contenedor (o ubicación de memoria) que puede memorizar la representación de un valor y posiblemente reemplazar su valor actual por otro).

En la programación funcional, una variable tiene el mismo propósito que tiene en matemáticas como un marcador de posición para algún valor, aún no se ha proporcionado. En la programación imperativa tradicional, este papel es desempeñado por la constante (no debe confundirse con literales cuyo valor determinado se expresa con una notación específica de ese dominio de valores, como 123, verdadero, ["abdcz", 3.14]).

Las variables de cualquier tipo, así como las constantes, pueden representarse mediante identificadores.

La variable imperativa puede cambiar su valor y esa es la base de la mutabilidad. La variable funcional no puede.

Los lenguajes de programación generalmente permiten construir entidades más grandes a partir de las más pequeñas en el lenguaje.

Los lenguajes imperativos permiten que tales construcciones incluyan variables y eso es lo que le da datos mutables.

Como leer un programa

Un programa es fundamentalmente una descripción abstracta de su algoritmo, es un lenguaje, ya sea un diseño pragmático o un lenguaje paradigmáticamente puro.

En principio, puede tomar cada declaración de lo que se supone que significa abstractamente. Luego, el compilador lo traducirá a una forma apropiada para que la computadora lo ejecute, pero ese no es su problema en primera aproximación.

Por supuesto, la realidad es un poco más dura, y a menudo es bueno tener una idea de lo que sucede para evitar estructuras que el compilador no sabrá cómo manejar para una ejecución eficiente. Pero eso ya es optimización ... para qué compiladores puede ser muy bueno, a menudo mejor que los programadores.

Programación funcional y mutabilidad.

La mutabilidad se basa en la existencia de variables imperativas que pueden contener valores, que se cambiarán por asignación. Como estos no existen en la programación funcional, todo puede verse como inmutable.

La programación funcional se ocupa exclusivamente de valores.

Sus primeras cuatro afirmaciones sobre la inmutabilidad son en su mayoría correctas, pero describen con visión imperativa algo que no es imperativo. Es un poco como describir con colores en un mundo donde todos son ciegos. Está utilizando conceptos ajenos a la programación funcional.

Solo tiene valores puros, y una matriz de enteros es un valor puro. Para obtener otra matriz que difiera solo en un elemento, debe usar un valor de matriz diferente. Cambiar un elemento es solo un concepto que no existe en este contexto. Puede tener una función que tiene una matriz y algunos índices como argumento, y devuelve un resultado que es una matriz casi idéntica que difiere solo donde lo indican los índices. Pero sigue siendo un valor de matriz independiente. Cómo se representan estos valores no es su problema. Tal vez ellos "comparten" mucho en la traducción imperativa para la computadora ... pero ese es el trabajo del compilador ... y usted ni siquiera quiere saber para qué tipo de arquitectura de máquina está compilando.

No copie valores (no tiene sentido, es un concepto extraño). Solo usa valores que existen en los dominios que ha definido en su programa. O los describe (como literales) o son el resultado de aplicar una función a otros valores. Puede asignarles un nombre (definiendo así una constante) para asegurarse de que se use el mismo valor en diferentes lugares del programa. Tenga en cuenta que la aplicación de la función no debe percibirse como un cálculo sino como el resultado de la aplicación a los argumentos dados. Escribir 5+2o escribir 7equivale a lo mismo. Lo cual es consistente con el párrafo anterior.

No hay variables imperativas. Ninguna asignación es posible. Puede vincular nombres solo a valores (para formar constantes), a diferencia de los lenguajes imperativos donde puede vincular nombres a variables asignables.

Si eso tiene un costo en complejidad no está totalmente claro. Por un lado, la referencia a la complejidad se refiere a paradigmas imperativos. No se define como tal para la programación funcional, a menos que elija leer su programa funcional como un imperativo, que no es la intención del diseñador. De hecho, la vista funcional está pensada para que no se preocupe por tales problemas y se concentre en lo que se está calculando. Es un poco como especificación versus implementación.

El compilador debe ocuparse de la implementación y ser lo suficientemente inteligente como para adaptar lo que se debe hacer al hardware que lo hará, sea lo que sea.

No digo que los programadores nunca se preocupen por eso. Tampoco estoy diciendo que los lenguajes de programación y la tecnología de compilación sean tan maduros como quisiéramos que fueran.

Respondiendo las preguntas

  1. No modifica el valor existente (concepto alienígena), pero calcula nuevos valores que difieren donde se desee, posiblemente al tener un elemento adicional, es una lista.

  2. El programa puede obtener nuevos datos. El punto es cómo expresas eso en el idioma. Por ejemplo, puede considerar que el programa funciona con un valor específico, posiblemente de tamaño ilimitado, que se denomina flujo de entrada. Es un valor que se supone que debe estar sentado allí (ya sea que ya se conozca por completo o no, no es su problema). Luego tiene una función que devuelve un par compuesto por el primer elemento de la secuencia y el resto de la secuencia.

    Puede usar eso para construir redes de componentes de comunicación de una manera puramente aplicativa (corutinas)

  3. El aprendizaje automático es solo otro problema cuando tiene que aumentar datos y modificar valores. En la programación funcional no hace eso: solo calcula nuevos valores que difieren adecuadamente de acuerdo con los datos de entrenamiento. La máquina resultante también funcionará. Lo que le preocupa es calcular el tiempo y la eficiencia del espacio. Pero, nuevamente, ese es un tema diferente, que idealmente debería ser tratado por el compilador.

Observaciones finales

Está bastante claro, a partir de los comentarios u otras respuestas, que los lenguajes prácticos de programación funcional no son puramente funcionales. Eso es una reflexión sobre el hecho de que nuestra tecnología aún debe mejorarse, especialmente en lo que respecta a la compilación.

¿Es posible escribir en un estilo puramente aplicativo? La respuesta se conoce desde hace unos 40 años y es "sí". El mismo propósito de la semántica denotacional, tal como apareció en la década de 1970, era precisamente traducir (compilar) lenguajes a un estilo puramente funcional, considerado mejor matemáticamente y, por lo tanto, considerado una mejor base para definir la semántica de los programas.

El aspecto interesante de esto es que la estructura de programación imperativa, incluidas las variables, se puede traducir a un estilo funcional mediante la introducción de dominios de valores apropiados, como un almacén de datos. Y a pesar del estilo funcional, sigue siendo sorprendentemente similar al código de los compiladores reales escritos en un estilo imperativo.

babou
fuente
0

Es un error pensar que los programas funcionales no pueden almacenar datos, y no creo que la respuesta de Jakes realmente lo haya explicado muy bien.

Los programas funcionales son, como cualquier programa, realmente funciones de asignación de enteros a enteros. Cualquier programa imperativo que opere en estructuras de datos mutables tiene una contraparte funcional. Este es solo otro medio para lograr el mismo fin.

La forma funcional de almacenar datos de experimentos de alguna fuente sería llamar a la función de almacenamiento con la estructura de datos como argumento y generar una concatenación de la estructura de datos existente y los nuevos datos y, por lo tanto, los datos se almacenan sin la noción de estructuras de datos mutables.

Desde mi propia experiencia , creo que el concepto de estructuras de datos inmutables está llevando a los desarrolladores convencionales a pensar que hay ciertas cosas que no son prácticas o incluso imposibles de hacer en un entorno funcional. Este no es el caso.

Jeppe Hartmund
fuente
"Los programas funcionales son, como cualquier programa, realmente funciones de asignación de enteros a enteros". ¿Cómo es, por ejemplo, Minecraft, realmente una función de asignación de enteros a enteros?
David Richerby
Fácilmente. Cada byte se puede interpretar como un entero binario. Un estado en una computadora es una colección de bytes. Un programa, incluso Minecraft, manipula el estado de una computadora, mapeándolo de un estado a otro.
Jeppe Hartmund
La entrada del usuario no parece encajar en este mundo.
David Richerby
La entrada del usuario es parte del estado de una computadora. No solo existe en su pantalla.
Jeppe Hartmund