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 tratan como ciudadanos de primera clase , lo que significa que pueden vincularse a nombres (incluidos identificadores locales ), pasarse como argumentos y devolverse desde otras funciones, tal como puede hacerlo cualquier otro tipo de datos . Esto permite escribir programas en un estilo declarativo y componible , donde pequeñas funciones se combinan de forma modular .
La programación funcional a veces se trata como 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 recibir información 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 mundo académico , evolucionando a partir del cálculo lambda , un sistema formal de computación basado únicamente en funciones. Históricamente, la programación funcional ha sido menos popular que la programación imperativa, pero muchos lenguajes funcionales se están utilizando hoy en día en la industria y la educación, incluidos Common Lisp , Scheme , [3] [4] [5] [6] Clojure , Wolfram Language , [7] [8] Raqueta , [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 comúnmente utilizado 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 de dominio específico como SQL y Lex / Yacc utilizan algunos elementos de 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 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 cálculo 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 de cálculo equivalentes, [37] demostrando que el cálculo lambda es Turing completo . El cálculo lambda constituye la base de todos los lenguajes de programación funcionales. Moses Schönfinkel y Haskell Curry desarrollaron una formulación teórica equivalente, la lógica combinatoria , en las décadas de 1920 y 1930. [38]
Posteriormente, Church desarrolló un sistema más débil, el cálculo lambda de tipo simple , que amplió el cálculo lambda asignando un tipo de datos a todos los términos. [39] Esto forma la base para la programación funcional tipada estáticamente.
El primer lenguaje de programación funcional de alto nivel , Lisp , fue desarrollado a finales de la década de 1950 para la serie de computadoras científicas IBM 700/7000 por John McCarthy mientras estaba en el Instituto Tecnológico de Massachusetts (MIT). [40] Las funciones Lisp se definieron utilizando la notación lambda de Church, ampliada con una construcción de etiqueta 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. Dialectos posteriores, como Scheme y Clojure , y derivados como Dylan y Julia , buscaron simplificar y racionalizar Lisp en torno a un núcleo limpiamente funcional, 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), 1956, a veces se cita 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, depende en gran medida de la estructura de la lista mutante y de características imperativas similares.
Kenneth E. Iverson desarrolló APL a principios de la década de 1960, como se describe en su libro de 1962 Un lenguaje de programación ( ISBN 9780471430148 ). APL fue la principal influencia en el 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 la programación ISWIM. idioma. [48]
John Backus presentó FP en su conferencia del Premio Turing de 1977 "¿Se puede liberar la programación del estilo von Neumann ? Un estilo funcional y su álgebra de programas". [49] Define los programas funcionales como aquellos que se construyen de forma jerárquica mediante "combinación de formas" que permiten un "álgebra de programas"; En lenguaje moderno, esto significa que los programas funcionales siguen el principio de composicionalidad . [ cita necesaria ] 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 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 finalmente se desarrolló en varios dialectos, los más comunes de los cuales ahora son OCaml y Standard ML .
En la década de 1970, Guy L. Steele y Gerald Jay Sussman desarrollaron Scheme , como se describe en los Lambda Papers y en el libro de texto de 1985 Structure and Interpretation of Computer Programs . Scheme fue el primer dialecto de lisp que utilizó alcance léxico y requirió optimización de llamadas de cola , 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 constructivos ), 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 funcionales posteriores. [ cita necesaria ]
El lenguaje funcional perezoso Miranda , desarrollado por David Turner, apareció inicialmente en 1985 y tuvo una fuerte influencia en Haskell . Con Miranda como propietario, Haskell comenzó con un consenso en 1987 para formar un estándar abierto para la investigación de programación funcional; Los lanzamientos de implementación han estado en curso desde 1990.
Más recientemente, 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 un concepto. [53]
La programación funcional sigue utilizándose en entornos comerciales. [54] [55] [56]
Varios conceptos [57] y paradigmas son específicos de la programación funcional y, en general, ajenos a la programación imperativa (incluida la programación orientada a objetos ). Sin embargo, los lenguajes de programación a menudo atienden a varios paradigmas de programación, por lo que los programadores que utilizan lenguajes "principalmente 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 ambas permiten funciones como argumentos y resultados de otras funciones. La distinción entre los dos es sutil: "orden superior" describe un concepto matemático de funciones que operan sobre otras funciones, mientras que "primera clase" es un término informático para entidades de lenguaje de programación que no tienen restricciones en su uso (por lo tanto, primera Las funciones de clase pueden aparecer en cualquier parte del programa que otras entidades de primera clase, como los números, incluso como argumentos de otras funciones y como sus valores de retorno).
Las funciones de orden superior permiten la aplicación parcial o currying , una técnica que aplica una función a sus argumentos uno por uno, y cada aplicación devuelve una nueva función que acepta el siguiente argumento. Esto permite a un programador expresar de manera sucinta, por ejemplo, la función sucesora como el operador de suma aplicado parcialmente al número uno natural .
Las funciones puras (o expresiones) no tienen efectos secundarios (memoria o E/S). Esto significa que las funciones puras tienen varias propiedades útiles, muchas de las cuales pueden usarse para optimizar el código:
Si bien la mayoría de los compiladores de 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, lo que evita optimizaciones que involucran esas funciones externas. Algunos compiladores, como gcc , agregan palabras clave adicionales para que un programador marque explícitamente funciones externas como puras, para permitir tales optimizaciones. Fortran 95 también permite designar funciones puras . [59] C++11 agregó constexpr
una palabra clave con semántica similar.
La iteración (bucle) en lenguajes funcionales generalmente se logra mediante recursividad . Las funciones recursivas se invocan a sí mismas, permitiendo que una operación se repita hasta llegar al caso base . En general, la recursividad requiere mantener una pila , que consume espacio en una cantidad lineal hasta la profundidad de la recursividad. Esto podría hacer que el uso de la recursividad sea prohibitivamente costoso en lugar de los bucles imperativos. Sin embargo, un compilador puede reconocer y optimizar una forma especial de recursividad conocida como recursividad 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 implementaciones que admitan la recursividad 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 recursividad para expresar un bucle y hacerlo sería seguro para el espacio. [62] Además, contrariamente a su nombre, representa todas las llamadas de cola, no solo la recursividad de cola. Si bien la recursión de cola adecuada generalmente se implementa convirtiendo el código en bucles imperativos, las implementaciones pueden implementarlo de otras maneras. Por ejemplo, Chicken mantiene intencionalmente una pila y deja que se desborde . Sin embargo, cuando esto sucede, su recolector de basura reclamará espacio, [63] permitiendo un número ilimitado de llamadas de cola activas aunque no convierta la recursividad de cola en un bucle.
Los patrones comunes de recursividad se pueden abstraer utilizando funciones de orden superior, siendo los catamorfismos y anamorfismos (o "pliegues" y "desplegados") los ejemplos más obvios. Estos esquemas de recursividad desempeñan un papel análogo a 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 recursividad sin restricciones y son Turing completos , 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 sólo permiten recursividad bien fundada y son fuertemente normalizadores (los cálculos no terminantes sólo pueden expresarse con flujos infinitos de valores llamados codata ). Como consecuencia, estos lenguajes no logran ser completos según Turing y expresar ciertas funciones en ellos es imposible, pero aun así pueden expresar una amplia clase de cálculos interesantes evitando al mismo tiempo los problemas introducidos por la recursividad sin restricciones. La programación funcional limitada a una recursividad bien fundada con algunas otras restricciones se denomina 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 se refieren 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. Bajo evaluación estricta, la evaluación de cualquier término que contenga un subtérmino reprobado falla. Por ejemplo, la expresión:
longitud de impresión ([2+1, 3*2, 1/0, 5-4])
falla bajo evaluación estricta debido a la división por cero en el tercer elemento de la lista. En evaluación diferida, la función de longitud devuelve el valor 4 (es decir, el número de elementos de la lista), ya que al evaluarla no se intentan 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 diferida 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 diferida en lenguajes funcionales es la reducción de gráficos . [65] La evaluación diferida se utiliza de forma predeterminada en varios lenguajes funcionales puros, incluidos Miranda , Clean y Haskell .
Hughes 1984 aboga por la evaluación diferida como mecanismo para mejorar la modularidad del programa mediante la separación de preocupaciones , facilitando 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 operativa para ayudar en dicho análisis. [66] Harper 2009 propone incluir evaluación estricta y perezosa en el mismo idioma, utilizando el sistema de tipos del idioma 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 funcionales han tendido a utilizar el cálculo lambda tipificado , rechazando todos los programas no válidos en el momento de la compilación y arriesgándose a errores de falso positivo , a diferencia del cálculo lambda no tipificado , que acepta todos los programas válidos. programas en tiempo de compilación y corre el riesgo de errores de falso negativo , utilizados en Lisp y sus variantes (como Scheme ), ya que rechazan todos los programas no vá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 basado en pruebas , mientras que la inferencia de tipos libera al programador de la necesidad de declarar tipos manualmente 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. Estos tipos se denominan tipos dependientes . Estos sistemas de tipos no tienen inferencia de tipos decidibles 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 tipificados 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 de interés principalmente en la investigación académica (incluidas las matemáticas formalizadas ), también han comenzado a usarse en ingeniería. Compcert es un compilador de un subconjunto del lenguaje de programación C escrito en Coq y verificado formalmente. [72]
Se puede implementar una forma limitada de tipos dependientes llamados tipos de datos algebraicos generalizados (GADT) de una 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 adiciones a otros lenguajes, incluidos Java y C#. [76]
Los programas funcionales no tienen declaraciones 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 ejecución. Entonces, los programas funcionales son referencialmente transparentes. [77]
Considere la declaración de asignación de Cx=x * 10
, esto cambia el valor asignado a la variable x
. Digamos que el valor inicial de x
fue 1
, luego dos evaluaciones consecutivas de la variable x
arrojan 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, considere otra función como la que es transparente, ya que no cambia implícitamente la entrada x y, por lo tanto, no tiene tales efectos secundarios . Los programas funcionales utilizan exclusivamente este tipo de funciones y, por tanto, son referencialmente transparentes.int plusone(int x) {return x+1;}
Las estructuras de datos puramente funcionales a menudo se representan de manera diferente a sus contrapartes imperativas . [78] Por ejemplo, la matriz con tiempos constantes de acceso y actualización 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. Los arrays pueden ser sustituidos por mapas o listas de acceso aleatorio, que admiten 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 sin modificar las versiones anteriores de la estructura de datos. En Clojure, las estructuras de datos persistentes se utilizan como alternativas funcionales a sus contrapartes imperativas. Los vectores persistentes, por ejemplo, utilizan árboles para una actualización parcial. Llamar al método de inserción 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 las E/S. La programación funcional pura evita por completo los efectos secundarios y proporciona transparencia referencial.
Las funciones de orden superior rara vez se utilizan en la programación imperativa anterior. Un programa imperativo tradicional podría utilizar un bucle para recorrer y modificar una lista. Un programa funcional, por otro lado, probablemente usaría una función "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 siguientes dos ejemplos (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 ListaNúm = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]; dejar resultado = 0 ; for ( let i = 0 ; i < numList . length ; i ++ ) { if ( numList [ i ] % 2 === 0 ) { resultado += numList [ i ] * 10 ; } }
Programación funcional con funciones de orden superior:
resultado constante = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] . filtro ( n => n % 2 === 0 ) . mapa ( a => a * 10 ) . reducir (( 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 uno por uno (consulte la décima regla de Greenspun ).
Hay tareas (por ejemplo, mantener el saldo de una cuenta bancaria) que a menudo parecen implementarse de forma más natural con el Estado. La programación funcional pura realiza estas tareas, y las tareas de E/S, como aceptar entradas del usuario e imprimir en la pantalla, de una manera diferente.
El lenguaje de programación funcional puro Haskell los implementa usando mónadas , derivadas de la teoría de categorías . [80] Las mónadas ofrecen una manera de abstraer ciertos tipos de patrones computacionales, incluido (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, con plantillas y ejemplos adecuados, a muchos estudiantes les resulta difícil entenderlas 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 pasando por 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 administradas 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 cálculos. [ cita necesaria ]
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 necesaria ]
Los lenguajes de programación funcional suelen ser menos eficientes en el 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 los arreglos planos de manera muy eficiente con CPU profundamente canalizadas, captarlos previamente de manera eficiente a través de cachés (sin una búsqueda compleja de punteros) o manejarlos con instrucciones SIMD. Tampoco es fácil crear sus contrapartes inmutables de propósito general igualmente eficientes. Para los lenguajes puramente funcionales, la desaceleración en el peor de los casos 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 un tiempo de acceso logarítmico (como un árbol equilibrado). [84] Sin embargo, estas desaceleraciones no son universales. Para los programas que realizan cálculos numéricos intensivos, los lenguajes funcionales como OCaml y Clean son sólo un poco más lentos que C según The Computer Language Benchmarks Game . [85] Para 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.
En muchos casos, la inmutabilidad de los datos puede conducir a la eficiencia de la ejecución al permitir que el compilador haga suposiciones que no son seguras en un lenguaje imperativo, lo que aumenta las oportunidades de expansión en línea . [86] Incluso si la copia involucrada que puede parecer implícita cuando se trata de estructuras de datos persistentes e inmutables puede parecer computacionalmente costosa, algunos lenguajes de programación funcionales, como Clojure, resuelven este problema implementando mecanismos para compartir memoria de forma segura entre datos formalmente inmutables . [87] Rust se distingue por su enfoque de la inmutabilidad de los datos, que implica referencias inmutables [88] y un concepto llamado vida útil. [89]
Los datos inmutables con separación de identidad y estado y 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 cerraduras. 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 compartir el estado y la 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 diferida también puede acelerar el programa, incluso de forma asintótica, mientras que puede ralentizarlo como máximo en un factor constante (sin embargo, puede introducir pérdidas de memoria si se usa incorrectamente). Launchbury 1993 [66] analiza cuestiones teóricas relacionadas con las pérdidas de memoria debidas a una evaluación perezosa, y O'Sullivan et al. 2008 [93] brindan algunos consejos prácticos para analizarlos y solucionarlos. Sin embargo, las implementaciones más generales de evaluación diferida que hacen un uso extensivo de código y datos desreferenciados funcionan mal en procesadores modernos con canalizaciones profundas y cachés multinivel (donde una falla de caché puede costar cientos de ciclos) [ cita necesaria ] .
Es posible que algunos lenguajes de programación funcionales no optimicen abstracciones como funciones de orden superior como " mapa " o " filtro " con tanta eficiencia como las operaciones imperativas subyacentes. Considere, como ejemplo, las dos formas siguientes de comprobar si 5 es un número par en Clojure :
( ¿par? 5 ) ( .equals ( mod 5 2 ) 0 )
Cuando se compara 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 incluso? "Devuelve verdadero si n es par, genera una excepción si n no es un número entero" { :added "1.0" :static true } [ n ] ( if ( integer? n ) ( zero? ( bit-and ( clojure.lang.RT/uncheckedLongCast n ) 1 )) ( throw ( IllegalArgumentException. ( str "El argumento debe ser un número entero: " n )))))
tiene un tiempo medio de ejecución de 4,76 ms, mientras que el segundo, en el que .equals
se invoca directamente el método Java subyacente , tiene un tiempo medio de ejecución de 2,8 μs, aproximadamente 1700 veces más rápido. Parte de eso se puede atribuir a la verificación de tipos y al manejo de excepciones involucrados en la implementación de even?
, así que tomemos, por ejemplo, la biblioteca lo para Go , que implementa varias funciones de orden superior comunes en lenguajes de programación funcionales que usan 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 puede atribuirse 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 una sobrecarga de tiempo de ejecución adicional. Esto se logra gracias al compilador que utiliza loop unrolling , donde cada iteración de un bucle, ya sea imperativa o mediante iteradores, se convierte en una instrucción ensambladora 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 de CPU específicos , lo que permitirá el acceso en tiempo constante en tiempo de ejecución. [96]
Es posible utilizar un estilo de programación funcional 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] tenían funciones de primera clase desde sus inicios. [101] Python tenía 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 convencionales como PHP 5.3, Visual Basic 9 , C# 3.0, C++11 y Kotlin . [28] [ cita necesaria ]
En PHP, las clases anónimas , cierres y lambdas son totalmente compatibles. Se están desarrollando bibliotecas y extensiones de lenguaje para estructuras de datos inmutables para ayudar a la programación en el estilo funcional.
En Java , a veces se pueden utilizar clases anónimas para simular cierres; [104] sin embargo, las clases anónimas no siempre son reemplazos adecuados de los cierres porque tienen capacidades más limitadas. [105] Java 8 admite expresiones lambda como reemplazo de algunas clases anónimas. [106]
En C# , las clases anónimas no son necesarias porque los cierres y lambdas son totalmente compatibles. Se están desarrollando bibliotecas y extensiones de lenguaje para estructuras de datos inmutables para ayudar a la programación en el estilo funcional en C#.
Muchos patrones de diseño orientado 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 los 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 verse 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 argumentos de entrada y salida, las relaciones se pueden consultar con cualquier patrón de entradas y salidas. Considere el siguiente programa lógico:
madre ( charles , elizabeth ). madre ( harry , diana ).
El programa se puede consultar, como un programa funcional, para generar madres a partir de niños:
?- madre ( harry , X ). X = diana . ?- madre ( charles , X ). X = Isabel .
Pero también se puede consultar al revés , para generar hijos:
?- madre ( X , elizabeth ). X = Carlos . ?- 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 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 funciones en programación funcional: [110]
abuelo ( X ) := padre ( padre ( X )). padre ( X ) : = madre ( X ). padre ( X ) : = padre ( 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 similar a una función en forma relacional y ejecuta el programa lógico resultante utilizando la estrategia de ejecución estándar Prolog.
Emacs , una familia de editores de texto altamente extensible, utiliza su propio dialecto Lisp para escribir complementos. Richard Stallman, autor original de la implementación más popular de Emacs, GNU Emacs y Emacs Lisp, 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 expresiones S , 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 recursividad. Se han desarrollado varias extensiones para programas de hojas de cálculo para permitir funciones reutilizables y de orden superior, 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 del lenguaje de programación . Hay varios lugares de publicaciones revisadas por pares que se centran en la programación funcional, incluida 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 , desarrollado por la empresa sueca Ericsson a finales de los años 1980, se utilizó originalmente para implementar sistemas de telecomunicaciones tolerantes a fallos , [11] pero desde entonces se ha hecho popular para crear 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 el entrenamiento - software de simulación [5] y control del telescopio . [6] OCaml , que se introdujo a mediados de la década de 1990, ha tenido 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 concebido 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 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 es utilizado por algunos proyectos comerciales relativamente populares, como Font Awesome o Allegro (una de las plataformas de comercio electrónico más grandes de Polonia) [127], la plataforma de anuncios clasificados Allegro Lokalnie. [128]
Las "plataformas" funcionales han sido populares en las finanzas para el análisis de riesgos (particularmente entre los grandes bancos de inversión). Los factores de riesgo se codifican como funciones que forman gráficos (categorías) interdependientes para medir las correlaciones en los cambios del mercado, de manera similar a las optimizaciones de la 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 las 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 necesaria ]
Muchas universidades enseñan programación funcional. [129] [130] [131] [132] Algunos lo tratan como un concepto introductorio de programación [132] mientras que otros enseñan primero métodos de programación imperativos. [131] [133]
Fuera de la informática, la programación funcional se utiliza para enseñar conceptos geométricos y algebraicos y de resolución de problemas. [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]
Haskell tiene una amplia gama de usos comerciales, desde el sector aeroespacial y de defensa hasta las finanzas, pasando por nuevas empresas web, empresas de diseño de hardware y fabricantes de cortadoras de césped.
Escala efectiva.
El método Object.freeze() congela un objeto. Un objeto congelado ya no se puede cambiar; congelar un objeto evita que se le agreguen nuevas propiedades, que se eliminen propiedades existentes, evita cambiar la enumerabilidad, configurabilidad o escritura de las propiedades existentes y evita que se cambien los valores de las propiedades existentes. Además, congelar un objeto también evita que se cambie su prototipo. congelar() devuelve el mismo objeto que se pasó.