En informática , la programación funcional es un paradigma de programación en el que los programas se construyen aplicando y componiendo funciones . Es un paradigma de programación declarativa en el que las definiciones de funciones son árboles de expresiones que asignan valores a otros valores, en lugar de una secuencia de declaraciones imperativas que actualizan el estado de ejecución del programa.
En la programación funcional, las funciones se consideran ciudadanos de primera clase , lo que significa que se pueden vincular a nombres (incluidos los identificadores locales ), pasar como argumentos y devolver desde otras funciones, al igual que cualquier otro tipo de datos . Esto permite escribir programas en un estilo declarativo y componible , donde las funciones pequeñas se combinan de manera modular .
La programación funcional a veces se considera sinónimo de programación puramente funcional , un subconjunto de la programación funcional que trata todas las funciones como funciones matemáticas deterministas o funciones puras . Cuando se llama a una función pura con algunos argumentos dados, siempre devolverá el mismo resultado y no puede verse afectada por ningún estado mutable u otros efectos secundarios . Esto contrasta con los procedimientos impuros , comunes en la programación imperativa , que pueden tener efectos secundarios (como modificar el estado del programa o tomar la entrada de un usuario). Los defensores de la programación puramente funcional afirman que al restringir los efectos secundarios, los programas pueden tener menos errores , ser más fáciles de depurar y probar , y ser más adecuados para la verificación formal . [1] [2]
La programación funcional tiene sus raíces en el ámbito académico , evolucionando a partir del cálculo lambda , un sistema formal de computación basado únicamente en funciones. La programación funcional ha sido históricamente menos popular que la programación imperativa, pero muchos lenguajes funcionales se utilizan hoy en día en la industria y la educación, incluidos Common Lisp , Scheme , [3] [4 ] [5] [6] Clojure , Wolfram Language , [7] [8] Racket , [9] Erlang , [10] [11] [12] Elixir , [13] OCaml , [14] [15] Haskell , [16] [17] y F# . [18] [19] Lean es un lenguaje de programación funcional que se utiliza comúnmente para verificar teoremas matemáticos. [20] La programación funcional también es clave para algunos lenguajes que han tenido éxito en dominios específicos, como JavaScript en la Web, [21] R en estadística, [22] [23] J , K y Q en análisis financiero y XQuery / XSLT para XML . [24] [25] Los lenguajes declarativos específicos de dominio como SQL y Lex / Yacc utilizan algunos elementos de la programación funcional, como no permitir valores mutables . [26] Además, muchos otros lenguajes de programación admiten la programación en un estilo funcional o han implementado características de la programación funcional, como C++11 , C# , [27] Kotlin , [28] Perl , [29] PHP , [30] Python , [31] Go , [32] Rust , [33] Raku , [34] Scala , [35] y Java (desde Java 8) . [36]
El cálculo lambda , desarrollado en la década de 1930 por Alonzo Church , es un sistema formal de computación construido a partir de la aplicación de funciones . En 1937, Alan Turing demostró que el cálculo lambda y las máquinas de Turing son modelos equivalentes de computación, [37] mostrando que el cálculo lambda es Turing completo . El cálculo lambda forma la base de todos los lenguajes de programación funcional. Una formulación teórica equivalente, la lógica combinatoria , fue desarrollada por Moses Schönfinkel y Haskell Curry en las décadas de 1920 y 1930. [38]
Posteriormente, Church desarrolló un sistema más débil, el cálculo lambda de tipos simples , que extendió el cálculo lambda asignando un tipo de datos a todos los términos. [39] Esto constituye la base de la programación funcional de tipos estáticos.
El primer lenguaje de programación funcional de alto nivel , Lisp , fue desarrollado a finales de los años 1950 para la serie de ordenadores científicos IBM 700/7000 por John McCarthy mientras estaba en el Instituto Tecnológico de Massachusetts (MIT). [40] Las funciones de Lisp se definieron utilizando la notación lambda de Church, extendida con una construcción de etiquetas para permitir funciones recursivas . [41] Lisp introdujo por primera vez muchas características paradigmáticas de la programación funcional, aunque los primeros Lisp eran lenguajes multiparadigma e incorporaron soporte para numerosos estilos de programación a medida que evolucionaban nuevos paradigmas. Los dialectos posteriores, como Scheme y Clojure , y sus derivados como Dylan y Julia , buscaron simplificar y racionalizar Lisp en torno a un núcleo funcional limpio, mientras que Common Lisp fue diseñado para preservar y actualizar las características paradigmáticas de los numerosos dialectos más antiguos que reemplazó. [42]
El lenguaje de procesamiento de información (IPL), de 1956, se cita a veces como el primer lenguaje de programación funcional basado en computadora. [43] Es un lenguaje de estilo ensamblador para manipular listas de símbolos. Tiene una noción de generador , que equivale a una función que acepta una función como argumento y, dado que es un lenguaje de nivel ensamblador, el código puede ser datos, por lo que se puede considerar que IPL tiene funciones de orden superior. Sin embargo, se basa en gran medida en la estructura de lista mutante y características imperativas similares.
Kenneth E. Iverson desarrolló APL a principios de la década de 1960, descrito en su libro de 1962 A Programming Language ( ISBN 9780471430148 ). APL fue la principal influencia en FP de John Backus . A principios de la década de 1990, Iverson y Roger Hui crearon J. A mediados de la década de 1990, Arthur Whitney , que había trabajado anteriormente con Iverson, creó K , que se utiliza comercialmente en las industrias financieras junto con su descendiente Q.
A mediados de la década de 1960, Peter Landin inventó la máquina SECD , [44] la primera máquina abstracta para un lenguaje de programación funcional, [45] describió una correspondencia entre ALGOL 60 y el cálculo lambda , [46] [47] y propuso el lenguaje de programación ISWIM . [48]
John Backus presentó la FP en su conferencia del Premio Turing de 1977 "¿Puede la programación liberarse del estilo von Neumann ? Un estilo funcional y su álgebra de programas". [49] Define los programas funcionales como construidos de manera jerárquica por medio de "formas de combinación" que permiten un "álgebra de programas"; en lenguaje moderno, esto significa que los programas funcionales siguen el principio de composicionalidad . [ cita requerida ] El artículo de Backus popularizó la investigación sobre programación funcional, aunque enfatizó la programación a nivel de función en lugar del estilo de cálculo lambda ahora asociado con la programación funcional.
El lenguaje ML de 1973 fue creado por Robin Milner en la Universidad de Edimburgo , y David Turner desarrolló el lenguaje SASL en la Universidad de St Andrews . También en Edimburgo en la década de 1970, Burstall y Darlington desarrollaron el lenguaje funcional NPL . [50] NPL se basó en las ecuaciones de recursión de Kleene y se introdujo por primera vez en su trabajo sobre la transformación de programas. [51] Burstall, MacQueen y Sannella luego incorporaron la verificación de tipos polimórficos de ML para producir el lenguaje Hope . [52] ML eventualmente se desarrolló en varios dialectos, los más comunes de los cuales ahora son OCaml y ML estándar .
En la década de 1970, Guy L. Steele y Gerald Jay Sussman desarrollaron Scheme , como se describe en los documentos Lambda y en el libro de texto Structure and Interpretation of Computer Programs de 1985. Scheme fue el primer dialecto de Lisp que utilizó el alcance léxico y requirió optimización de llamadas finales , características que fomentan la programación funcional.
En la década de 1980, Per Martin-Löf desarrolló la teoría de tipos intuicionista (también llamada teoría de tipos constructiva ), que asociaba programas funcionales con pruebas constructivas expresadas como tipos dependientes . Esto condujo a nuevos enfoques para la demostración interactiva de teoremas y ha influido en el desarrollo de lenguajes de programación funcional posteriores. [ cita requerida ]
El lenguaje funcional perezoso Miranda , desarrollado por David Turner, apareció inicialmente en 1985 y tuvo una fuerte influencia en Haskell . Como Miranda era un lenguaje propietario, Haskell comenzó a consensuar en 1987 la creación de un estándar abierto para la investigación en programación funcional; desde 1990 se han estado realizando lanzamientos de implementaciones.
Más recientemente, se ha encontrado uso en nichos como el CAD paramétrico en el lenguaje OpenSCAD construido sobre el marco CGAL , aunque su restricción en la reasignación de valores (todos los valores se tratan como constantes) ha generado confusión entre los usuarios que no están familiarizados con la programación funcional como concepto. [53]
La programación funcional continúa utilizándose en entornos comerciales. [54] [55] [56]
Hay varios conceptos [57] y paradigmas que son específicos de la programación funcional y, por lo general, ajenos a la programación imperativa (incluida la programación orientada a objetos ). Sin embargo, los lenguajes de programación suelen adaptarse a varios paradigmas de programación, por lo que los programadores que utilizan lenguajes "mayoritariamente imperativos" pueden haber utilizado algunos de estos conceptos. [58]
Las funciones de orden superior son funciones que pueden tomar otras funciones como argumentos o devolverlas como resultados. En cálculo, un ejemplo de función de orden superior es el operador diferencial , que devuelve la derivada de una función .
Las funciones de orden superior están estrechamente relacionadas con las funciones de primera clase en el sentido de que tanto las funciones de orden superior como las de primera clase permiten que las funciones sean argumentos y resultados de otras funciones. La distinción entre ambas es sutil: "de orden superior" describe un concepto matemático de funciones que operan sobre otras funciones, mientras que "de primera clase" es un término informático para las entidades del lenguaje de programación que no tienen restricciones en su uso (por lo tanto, las funciones de primera clase pueden aparecer en cualquier parte del programa en la que otras entidades de primera clase, como los números, pueden hacerlo, incluso como argumentos de otras funciones y como sus valores de retorno).
Las funciones de orden superior permiten la aplicación parcial o currificación , una técnica que aplica una función a sus argumentos uno a la vez, y cada aplicación devuelve una nueva función que acepta el siguiente argumento. Esto permite que un programador exprese de manera sucinta, por ejemplo, la función sucesora como el operador de suma parcialmente aplicado al número natural uno.
Las funciones (o expresiones) puras no tienen efectos secundarios (memoria o E/S). Esto significa que las funciones puras tienen varias propiedades útiles, muchas de las cuales se pueden utilizar para optimizar el código:
Aunque la mayoría de los compiladores para lenguajes de programación imperativos detectan funciones puras y realizan la eliminación de subexpresiones comunes para llamadas a funciones puras, no siempre pueden hacer esto para bibliotecas precompiladas, que generalmente no exponen esta información, impidiendo así optimizaciones que involucran esas funciones externas. Algunos compiladores, como gcc , agregan palabras clave adicionales para que un programador marque explícitamente las funciones externas como puras, para habilitar tales optimizaciones. Fortran 95 también permite que las funciones se designen como puras . [59] C++11 agregó constexpr
una palabra clave con semántica similar.
La iteración (bucles) en lenguajes funcionales se logra generalmente mediante recursión . Las funciones recursivas se invocan a sí mismas, permitiendo que una operación se repita hasta que alcance el caso base . En general, la recursión requiere mantener una pila , que consume espacio en una cantidad lineal a la profundidad de la recursión. Esto podría hacer que la recursión sea prohibitivamente cara de usar en lugar de bucles imperativos. Sin embargo, un compilador puede reconocer y optimizar una forma especial de recursión conocida como recursión de cola en el mismo código utilizado para implementar la iteración en lenguajes imperativos. La optimización de la recursión de cola se puede implementar transformando el programa en un estilo de paso de continuación durante la compilación, entre otros enfoques.
El estándar del lenguaje Scheme requiere que las implementaciones admitan la recursión de cola adecuada, lo que significa que deben permitir un número ilimitado de llamadas de cola activas. [60] [61] La recursión de cola adecuada no es simplemente una optimización; es una característica del lenguaje que asegura a los usuarios que pueden usar la recursión para expresar un bucle y que hacerlo sería seguro para el espacio. [62] Además, al contrario de su nombre, tiene en cuenta todas las llamadas de cola, no solo la recursión de cola. Si bien la recursión de cola adecuada generalmente se implementa convirtiendo el código en bucles imperativos, las implementaciones pueden implementarla de otras maneras. Por ejemplo, Chicken mantiene intencionalmente una pila y deja que la pila se desborde . Sin embargo, cuando esto sucede, su recolector de basura reclamará espacio, [63] lo que permite un número ilimitado de llamadas de cola activas aunque no convierta la recursión de cola en un bucle.
Los patrones comunes de recursión se pueden abstraer utilizando funciones de orden superior, siendo los catamorfismos y anamorfismos (o "pliegues" y "despliegues") los ejemplos más obvios. Estos esquemas de recursión desempeñan un papel análogo al de las estructuras de control integradas, como los bucles en los lenguajes imperativos .
La mayoría de los lenguajes de programación funcional de propósito general permiten la recursión sin restricciones y son completos de Turing , lo que hace que el problema de detención sea indecidible , puede causar falta de solidez en el razonamiento ecuacional y generalmente requiere la introducción de inconsistencia en la lógica expresada por el sistema de tipos del lenguaje . Algunos lenguajes de propósito especial como Coq solo permiten la recursión bien fundada y son fuertemente normalizadores (los cálculos no terminantes solo se pueden expresar con flujos infinitos de valores llamados codatos ). Como consecuencia, estos lenguajes no son completos de Turing y expresar ciertas funciones en ellos es imposible, pero aún pueden expresar una amplia clase de cálculos interesantes al tiempo que evitan los problemas introducidos por la recursión sin restricciones. La programación funcional limitada a la recursión bien fundada con algunas otras restricciones se llama programación funcional total . [64]
Los lenguajes funcionales se pueden clasificar según si utilizan evaluación estricta (ansiosa) o no estricta (perezosa) , conceptos que hacen referencia a cómo se procesan los argumentos de una función cuando se evalúa una expresión. La diferencia técnica está en la semántica denotacional de las expresiones que contienen cálculos fallidos o divergentes. En la evaluación estricta, la evaluación de cualquier término que contenga un subtérmino fallido falla. Por ejemplo, la expresión:
longitud de impresión ([2+1, 3*2, 1/0, 5-4])
falla en la evaluación estricta debido a la división por cero en el tercer elemento de la lista. En la evaluación perezosa, la función length devuelve el valor 4 (es decir, la cantidad de elementos en la lista), ya que al evaluarla no se intenta evaluar los términos que componen la lista. En resumen, la evaluación estricta siempre evalúa completamente los argumentos de la función antes de invocar la función. La evaluación perezosa no evalúa los argumentos de la función a menos que sus valores sean necesarios para evaluar la llamada a la función en sí.
La estrategia de implementación habitual para la evaluación perezosa en lenguajes funcionales es la reducción de grafos . [65] La evaluación perezosa se utiliza de forma predeterminada en varios lenguajes funcionales puros, incluidos Miranda , Clean y Haskell .
Hughes 1984 defiende la evaluación perezosa como un mecanismo para mejorar la modularidad del programa a través de la separación de preocupaciones , al facilitar la implementación independiente de productores y consumidores de flujos de datos. [2] Launchbury 1993 describe algunas dificultades que introduce la evaluación perezosa, particularmente al analizar los requisitos de almacenamiento de un programa, y propone una semántica operacional para ayudar en dicho análisis. [66] Harper 2009 propone incluir tanto la evaluación estricta como la perezosa en el mismo lenguaje, utilizando el sistema de tipos del lenguaje para distinguirlas. [67]
Especialmente desde el desarrollo de la inferencia de tipos Hindley-Milner en la década de 1970, los lenguajes de programación funcional han tendido a utilizar el cálculo lambda tipado , rechazando todos los programas inválidos en tiempo de compilación y arriesgando errores falsos positivos , en oposición al cálculo lambda no tipado , que acepta todos los programas válidos en tiempo de compilación y arriesga errores falsos negativos , utilizado en Lisp y sus variantes (como Scheme ), ya que rechazan todos los programas inválidos en tiempo de ejecución cuando la información es suficiente para no rechazar programas válidos. El uso de tipos de datos algebraicos hace que la manipulación de estructuras de datos complejas sea conveniente; la presencia de una fuerte verificación de tipos en tiempo de compilación hace que los programas sean más confiables en ausencia de otras técnicas de confiabilidad como el desarrollo impulsado por pruebas , mientras que la inferencia de tipos libera al programador de la necesidad de declarar manualmente los tipos al compilador en la mayoría de los casos.
Algunos lenguajes funcionales orientados a la investigación, como Coq , Agda , Cayenne y Epigram, se basan en la teoría de tipos intuicionista , que permite que los tipos dependan de términos. Dichos tipos se denominan tipos dependientes . Estos sistemas de tipos no tienen inferencia de tipos decidible y son difíciles de entender y programar. [68] [69] [70] [71] Pero los tipos dependientes pueden expresar proposiciones arbitrarias en lógica de orden superior . A través del isomorfismo de Curry-Howard , entonces, los programas bien tipados en estos lenguajes se convierten en un medio para escribir pruebas matemáticas formales a partir de las cuales un compilador puede generar código certificado . Si bien estos lenguajes son principalmente de interés en la investigación académica (incluida la matemática formalizada ), también han comenzado a usarse en ingeniería. Compcert es un compilador para un subconjunto del lenguaje C que está escrito en Coq y verificado formalmente. [72]
Se puede implementar una forma limitada de tipos dependientes, denominada tipos de datos algebraicos generalizados (GADT), de manera que proporcione algunos de los beneficios de la programación con tipos dependientes y al mismo tiempo evite la mayoría de sus inconvenientes. [73] Los GADT están disponibles en el compilador Glasgow Haskell , en OCaml [74] y en Scala [75] , y se han propuesto como complementos a otros lenguajes, incluidos Java y C#. [76]
Los programas funcionales no tienen instrucciones de asignación, es decir, el valor de una variable en un programa funcional nunca cambia una vez definida. Esto elimina cualquier posibilidad de efectos secundarios porque cualquier variable puede reemplazarse con su valor real en cualquier punto de la ejecución. Por lo tanto, los programas funcionales son referencialmente transparentes. [77]
Consideremos la declaración de asignación de Cx=x * 10
, que cambia el valor asignado a la variable x
. Digamos que el valor inicial de x
era 1
, entonces dos evaluaciones consecutivas de la variable x
dan como resultado 10
y 100
respectivamente. Claramente, reemplazar x=x * 10
con 10
o 100
le da al programa un significado diferente, por lo que la expresión no es referencialmente transparente. De hecho, las declaraciones de asignación nunca son referencialmente transparentes.
Ahora, consideremos otra función como es transparente, ya que no cambia implícitamente la entrada x y, por lo tanto, no tiene efectos secundarios . Los programas funcionales utilizan exclusivamente este tipo de función y, por lo tanto, son referencialmente transparentes.int plusone(int x) {return x+1;}
Las estructuras de datos puramente funcionales suelen representarse de una manera diferente a sus contrapartes imperativas . [78] Por ejemplo, la matriz con tiempos de acceso y actualización constantes es un componente básico de la mayoría de los lenguajes imperativos, y muchas estructuras de datos imperativas, como la tabla hash y el montón binario , se basan en matrices. Las matrices pueden reemplazarse por mapas o listas de acceso aleatorio, que admiten una implementación puramente funcional, pero tienen tiempos de acceso y actualización logarítmicos . Las estructuras de datos puramente funcionales tienen persistencia , una propiedad de mantener las versiones anteriores de la estructura de datos sin modificar. En Clojure, las estructuras de datos persistentes se utilizan como alternativas funcionales a sus contrapartes imperativas. Los vectores persistentes, por ejemplo, utilizan árboles para la actualización parcial. Llamar al método insert dará como resultado la creación de algunos nodos, pero no de todos. [79]
La programación funcional es muy diferente de la programación imperativa . Las diferencias más significativas surgen del hecho de que la programación funcional evita los efectos secundarios , que se utilizan en la programación imperativa para implementar el estado y la E/S. La programación funcional pura evita por completo los efectos secundarios y proporciona transparencia referencial.
En la programación imperativa antigua, rara vez se utilizan funciones de orden superior. Un programa imperativo tradicional podría utilizar un bucle para recorrer y modificar una lista. Por otro lado, un programa funcional probablemente utilizaría una función de "mapa" de orden superior que toma una función y una lista, generando y devolviendo una nueva lista aplicando la función a cada elemento de la lista.
Los dos ejemplos siguientes (escritos en JavaScript ) logran el mismo efecto: multiplican todos los números pares de una matriz por 10 y los suman todos, almacenando la suma final en la variable "resultado".
Bucle imperativo tradicional:
const numList = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]; deje que resultado = 0 ; para ( deje que i = 0 ; i < numList . length ; i ++ ) { si ( numList [ i ] % 2 === 0 ) { resultado += numList [ i ] * 10 ; } }
Programación funcional con funciones de orden superior:
const resultado = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] .filter ( n => n % 2 === 0 ) .map ( a => a * 10 ) .reduce ( ( a , b ) = > a + b , 0 ) ;
A veces, las abstracciones que ofrece la programación funcional pueden llevar al desarrollo de un código más robusto que evite ciertos problemas que pueden surgir al construir sobre una gran cantidad de código complejo e imperativo, como errores de un dígito (ver la décima regla de Greenspun ).
Hay tareas (por ejemplo, mantener el saldo de una cuenta bancaria) que a menudo parecen implementarse de manera más natural con el estado. La programación funcional pura realiza estas tareas, y las tareas de E/S como aceptar la entrada del usuario e imprimir en la pantalla, de una manera diferente.
El lenguaje de programación funcional puro Haskell las implementa utilizando mónadas , derivadas de la teoría de categorías . [80] Las mónadas ofrecen una forma de abstraer ciertos tipos de patrones computacionales, incluyendo (pero no limitado a) el modelado de cálculos con estado mutable (y otros efectos secundarios como E/S) de manera imperativa sin perder pureza. Si bien las mónadas existentes pueden ser fáciles de aplicar en un programa, dadas las plantillas y ejemplos apropiados, muchos estudiantes las encuentran difíciles de entender conceptualmente, por ejemplo, cuando se les pide que definan nuevas mónadas (lo que a veces es necesario para ciertos tipos de bibliotecas). [81]
Los lenguajes funcionales también simulan estados al pasar estados inmutables. Esto se puede hacer haciendo que una función acepte el estado como uno de sus parámetros y devuelva un nuevo estado junto con el resultado, dejando el estado anterior sin cambios. [82]
Los lenguajes funcionales impuros suelen incluir un método más directo para gestionar el estado mutable. Clojure , por ejemplo, utiliza referencias gestionadas que se pueden actualizar aplicando funciones puras al estado actual. Este tipo de enfoque permite la mutabilidad y, al mismo tiempo, promueve el uso de funciones puras como la forma preferida de expresar los cálculos. [ cita requerida ]
Se han desarrollado métodos alternativos, como la lógica de Hoare y la unicidad, para rastrear los efectos secundarios en los programas. Algunos lenguajes de investigación modernos utilizan sistemas de efectos para hacer explícita la presencia de efectos secundarios. [ cita requerida ]
Los lenguajes de programación funcional son típicamente menos eficientes en su uso de CPU y memoria que los lenguajes imperativos como C y Pascal . [83] Esto está relacionado con el hecho de que algunas estructuras de datos mutables como las matrices tienen una implementación muy sencilla utilizando el hardware actual. Se puede acceder a las matrices planas de manera muy eficiente con CPU profundamente segmentadas, precargarlas de manera eficiente a través de cachés (sin persecución compleja de punteros) o manejarlas con instrucciones SIMD. Tampoco es fácil crear sus contrapartes inmutables de propósito general igualmente eficientes. Para lenguajes puramente funcionales, la desaceleración del peor caso es logarítmica en el número de celdas de memoria utilizadas, porque la memoria mutable puede representarse mediante una estructura de datos puramente funcional con tiempo de acceso logarítmico (como un árbol equilibrado). [84] Sin embargo, tales desaceleraciones no son universales. Para programas que realizan cálculos numéricos intensivos, los lenguajes funcionales como OCaml y Clean son solo ligeramente más lentos que C según The Computer Language Benchmarks Game . [85] Para los programas que manejan matrices grandes y bases de datos multidimensionales , se diseñaron lenguajes funcionales de matrices (como J y K ) con optimizaciones de velocidad.
La inmutabilidad de los datos puede, en muchos casos, llevar a la eficiencia de la ejecución al permitir que el compilador haga suposiciones que no son seguras en un lenguaje imperativo, aumentando así las oportunidades de expansión en línea . [86] Incluso si la copia involucrada que puede parecer implícita cuando se trata con estructuras de datos inmutables persistentes puede parecer computacionalmente costosa, algunos lenguajes de programación funcional, como Clojure, resuelven este problema implementando mecanismos para compartir memoria de manera segura entre datos formalmente inmutables . [87] Rust se distingue por su enfoque de la inmutabilidad de datos que involucra referencias inmutables [88] y un concepto llamado tiempos de vida. [89]
Los datos inmutables con separación de identidad y estado y los esquemas de nada compartido también pueden ser potencialmente más adecuados para la programación concurrente y paralela en virtud de reducir o eliminar el riesgo de ciertos peligros de concurrencia, ya que las operaciones concurrentes suelen ser atómicas y esto permite eliminar la necesidad de bloqueos. Así es como java.util.concurrent
se implementan, por ejemplo, las clases, donde algunas de ellas son variantes inmutables de las clases correspondientes que no son adecuadas para el uso concurrente. [90] Los lenguajes de programación funcional a menudo tienen un modelo de concurrencia que, en lugar de estado compartido y sincronización, aprovecha los mecanismos de paso de mensajes (como el modelo de actor , donde cada actor es un contenedor para el estado, el comportamiento, los actores secundarios y una cola de mensajes). [91] [92] Este enfoque es común en Erlang / Elixir o Akka .
La evaluación perezosa también puede acelerar el programa, incluso asintóticamente, mientras que puede ralentizarlo como máximo por un factor constante (sin embargo, puede introducir fugas de memoria si se usa incorrectamente). Launchbury 1993 [66] analiza cuestiones teóricas relacionadas con las fugas de memoria de la evaluación perezosa, y O'Sullivan et al. 2008 [93] dan algunos consejos prácticos para analizarlas y solucionarlas. Sin embargo, las implementaciones más generales de la evaluación perezosa que hacen un uso extensivo de código y datos desreferenciados tienen un rendimiento deficiente en procesadores modernos con tuberías profundas y cachés de varios niveles (donde una falla de caché puede costar cientos de ciclos) [ cita requerida ] .
Es posible que algunos lenguajes de programación funcional no optimicen abstracciones como funciones de orden superior como " map " o " filter " de manera tan eficiente como las operaciones imperativas subyacentes. Considere, como ejemplo, las siguientes dos formas de verificar si 5 es un número par en Clojure :
( par? 5 ) ( . igual a ( mod 5 2 ) 0 )
Cuando se realizó una evaluación comparativa con la herramienta Criterium en una PC Ryzen 7900X GNU/Linux en un Leiningen REPL 2.11.2, ejecutándose en Java VM versión 22 y Clojure versión 1.11.1, la primera implementación, que se implementa como:
( defn even? "Devuelve verdadero si n es par, lanza una excepción si n no es un entero" { :added "1.0" :static true } [ n ] ( if ( entire? n ) ( zero? ( bit-and ( clojure.lang.RT/uncheckedLongCast n ) 1 )) ( throw ( IllegalArgumentException. ( str "El argumento debe ser un entero: " n )))))
tiene un tiempo de ejecución medio de 4,76 ms, mientras que el segundo, en el que .equals
se invoca directamente el método Java subyacente , tiene un tiempo de ejecución medio de 2,8 μs, aproximadamente 1700 veces más rápido. Parte de esto se puede atribuir a la comprobación de tipos y al manejo de excepciones implicados en la implementación de even?
, así que tomemos como ejemplo la biblioteca lo para Go , que implementa varias funciones de orden superior comunes en lenguajes de programación funcional utilizando genéricos . En un punto de referencia proporcionado por el autor de la biblioteca, la llamada map
es un 4% más lenta que un for
bucle equivalente y tiene el mismo perfil de asignación , [94] lo que se puede atribuir a varias optimizaciones del compilador, como la inserción en línea . [95]
Una característica distintiva de Rust son las abstracciones de costo cero . Esto significa que su uso no impone ninguna sobrecarga adicional en tiempo de ejecución. Esto se logra gracias a que el compilador utiliza un desenrollado de bucle , donde cada iteración de un bucle, ya sea imperativo o que utilice iteradores, se convierte en una instrucción de ensamblaje independiente , sin la sobrecarga del código de control del bucle. Si una operación iterativa escribe en una matriz, los elementos de la matriz resultante se almacenarán en registros específicos de la CPU , lo que permite un acceso en tiempo constante en tiempo de ejecución. [96]
Es posible utilizar un estilo funcional de programación en lenguajes que tradicionalmente no se consideran lenguajes funcionales. [97] Por ejemplo, tanto D [98] como Fortran 95 [59] admiten explícitamente funciones puras.
JavaScript , Lua , [99] Python y Go [100] tuvieron funciones de primera clase desde su inicio. [101] Python tuvo soporte para " lambda ", " map ", " reduce " y " filter " en 1994, así como cierres en Python 2.2, [102] aunque Python 3 relegó "reduce" al functools
módulo de biblioteca estándar. [103] Se han introducido funciones de primera clase en otros lenguajes principales como PHP 5.3, Visual Basic 9 , C# 3.0, C++11 y Kotlin . [28] [ cita requerida ]
En PHP, las clases anónimas , los cierres y las lambdas son totalmente compatibles. Se están desarrollando bibliotecas y extensiones de lenguaje para estructuras de datos inmutables con el fin de facilitar la programación en el estilo funcional.
En Java , las clases anónimas a veces se pueden usar para simular cierres; [104] sin embargo, las clases anónimas no siempre son reemplazos adecuados para los cierres porque tienen capacidades más limitadas. [105] Java 8 admite expresiones lambda como reemplazo para algunas clases anónimas. [106]
En C# , las clases anónimas no son necesarias, ya que los cierres y las lambdas son totalmente compatibles. Se están desarrollando bibliotecas y extensiones de lenguaje para estructuras de datos inmutables con el fin de facilitar la programación en el estilo funcional de C#.
Muchos patrones de diseño orientados a objetos se pueden expresar en términos de programación funcional: por ejemplo, el patrón de estrategia simplemente dicta el uso de una función de orden superior, y el patrón de visitante corresponde aproximadamente a un catamorfismo o pliegue .
De manera similar, la idea de datos inmutables de la programación funcional a menudo se incluye en lenguajes de programación imperativos, [107] por ejemplo la tupla en Python, que es una matriz inmutable, y Object.freeze() en JavaScript. [108]
La programación lógica puede considerarse como una generalización de la programación funcional, en la que las funciones son un caso especial de relaciones. [109] Por ejemplo, la función, madre(X) = Y, (cada X tiene sólo una madre Y) puede representarse mediante la relación madre(X, Y). Mientras que las funciones tienen un patrón estricto de entrada-salida de argumentos, las relaciones pueden consultarse con cualquier patrón de entradas y salidas. Considere el siguiente programa lógico:
madre ( charles , elizabeth ). madre ( harry , diana ).
Se puede consultar el programa, como si fuera un programa funcional, para generar madres a partir de niños:
?- madre ( harry , X ). X = diana . ?- madre ( charles , X ). X = elizabeth .
Pero también se puede consultar al revés , para generar hijos:
?- madre ( X , elizabeth ). X = charles . ?- madre ( X , diana ). X = harry .
Incluso se puede utilizar para generar todas las instancias de la relación madre:
?- madre ( X , Y ). X = Carlos , Y = Isabel . X = harry , Y = diana .
En comparación con la sintaxis relacional, la sintaxis funcional es una notación más compacta para funciones anidadas. Por ejemplo, la definición de abuela materna en sintaxis funcional se puede escribir en la forma anidada:
abuela_materna ( X ) = madre ( madre ( X )).
La misma definición en notación relacional debe escribirse en forma no anidada:
abuela_materna ( X , Y ) :- madre ( X , Z ), madre ( Z , Y ).
Aquí :-
significa si y ,
significa y .
Sin embargo, la diferencia entre ambas representaciones es simplemente sintáctica. En Ciao Prolog, las relaciones se pueden anidar, como las funciones en la programación funcional: [110]
abuelo ( a) ( X ) := padre ( a) ( padre ( a) ( X )). padre ( a ) (X ) := madre ( X ). padre ( a) ( X ) := padre ( a) ( X ).madre ( charles ) := elizabeth . padre ( charles ) := phillip . madre ( harry ) := diana . padre ( harry ) := charles .?- abuelo ( X , Y ). X = harry , Y = elizabeth . X = harry , Y = phillip .
Ciao transforma la notación funcional en forma relacional y ejecuta el programa lógico resultante utilizando la estrategia de ejecución estándar de Prolog.
Emacs , una familia de editores de texto altamente extensible, utiliza su propio dialecto Lisp para escribir complementos. El autor original de la implementación más popular de Emacs, GNU Emacs y Emacs Lisp, Richard Stallman considera a Lisp uno de sus lenguajes de programación favoritos. [111]
Helix, desde la versión 24.03, admite la vista previa de AST como S-expresiones , que también son la característica principal de la familia de lenguajes de programación Lisp. [112]
Las hojas de cálculo pueden considerarse una forma de sistema de programación funcional puro, de orden cero y de evaluación estricta. [113] Sin embargo, las hojas de cálculo generalmente carecen de funciones de orden superior, así como de reutilización de código y, en algunas implementaciones, también carecen de recursión. Se han desarrollado varias extensiones para programas de hojas de cálculo para habilitar funciones de orden superior y reutilizables, pero hasta ahora siguen siendo principalmente de naturaleza académica. [114]
La programación funcional es un área activa de investigación en el campo de la teoría de los lenguajes de programación . Existen varios lugares de publicación revisados por pares que se centran en la programación funcional, entre ellos la Conferencia Internacional sobre Programación Funcional , el Journal of Functional Programming y el Simposio sobre Tendencias en Programación Funcional .
La programación funcional se ha empleado en una amplia gama de aplicaciones industriales. Por ejemplo, Erlang , que fue desarrollado por la empresa sueca Ericsson a fines de la década de 1980, se utilizó originalmente para implementar sistemas de telecomunicaciones tolerantes a fallas , [11] pero desde entonces se ha vuelto popular para construir una gama de aplicaciones en empresas como Nortel , Facebook , Électricité de France y WhatsApp . [10] [12] [115] [116] [117] Scheme , un dialecto de Lisp , se utilizó como base para varias aplicaciones en las primeras computadoras Apple Macintosh [3] [4] y se ha aplicado a problemas como software de simulación de entrenamiento [5] y control de telescopios . [6] OCaml , que se introdujo a mediados de la década de 1990, ha tenido un uso comercial en áreas como análisis financiero, [14] verificación de controladores , programación de robots industriales y análisis estático de software integrado . [15] Haskell , aunque inicialmente fue pensado como un lenguaje de investigación, [17] también se ha aplicado en áreas como sistemas aeroespaciales, diseño de hardware y programación web. [16] [17]
Otros lenguajes de programación funcional que se han utilizado en la industria incluyen Scala , [118] F# , [18] [19] Wolfram Language , [7] Lisp , [119] Standard ML [120] [121] y Clojure. [122] Scala se ha utilizado ampliamente en la ciencia de datos , [123] mientras que ClojureScript , [124] Elm [125] o PureScript [126] son algunos de los lenguajes de programación frontend funcionales utilizados en producción. El marco Phoenix de Elixir también se utiliza en algunos proyectos comerciales relativamente populares, como Font Awesome o la plataforma de anuncios clasificados de Allegro (una de las plataformas de comercio electrónico más grandes de Polonia) [127] Allegro Lokalnie. [128]
Las "plataformas" funcionales han sido populares en finanzas para el análisis de riesgos (particularmente en los grandes bancos de inversión). Los factores de riesgo se codifican como funciones que forman gráficos interdependientes (categorías) para medir las correlaciones en los cambios del mercado, de manera similar a las optimizaciones de base de Gröbner , pero también para marcos regulatorios como el Análisis y Revisión Integral de Capital . Dado el uso de variaciones de OCaml y Caml en finanzas, estos sistemas a veces se consideran relacionados con una máquina abstracta categórica . La programación funcional está fuertemente influenciada por la teoría de categorías . [ cita requerida ]
Muchas universidades enseñan programación funcional. [129] [130] [131] [132] Algunas lo tratan como un concepto introductorio de programación [132] mientras que otras enseñan primero métodos de programación imperativa. [131] [133]
Fuera de la informática, la programación funcional se utiliza para enseñar resolución de problemas, conceptos algebraicos y geométricos. [134] También se ha utilizado para enseñar mecánica clásica, como en el libro Estructura e interpretación de la mecánica clásica .
En particular, Scheme ha sido una opción relativamente popular para enseñar programación durante años. [135] [136]
{{cite journal}}
: CS1 maint: DOI inactivo a partir de agosto de 2024 ( enlace )Haskell tiene una amplia gama de usos comerciales, desde la industria aeroespacial y de defensa hasta las finanzas, pasando por empresas emergentes de Internet, firmas de diseño de hardware y fabricantes de cortadoras de césped.
eficaz.
El método Object.freeze() congela un objeto. Un objeto congelado ya no se puede modificar; congelar un objeto impide que se le agreguen nuevas propiedades, que se eliminen las propiedades existentes, impide cambiar la enumerabilidad, la configurabilidad o la capacidad de escritura de las propiedades existentes y evita que se cambien los valores de las propiedades existentes. Además, congelar un objeto también impide que se cambie su prototipo. freeze() devuelve el mismo objeto que se pasó.