Articulo de referencia

Función de primera clase

En informática , se dice que un lenguaje de programación tiene funciones de primera clase si las trata como ciudadanos de primera clase . Esto significa que el lenguaje admite p...

En informática , se dice que un lenguaje de programación tiene funciones de primera clase si las trata como ciudadanos de primera clase . Esto significa que el lenguaje admite pasar funciones como argumentos a otras funciones, devolverlas como valores de otras funciones y asignarlas a variables o almacenarlas en estructuras de datos. [ 1 ] Algunos teóricos de lenguajes de programación también exigen soporte para funciones anónimas (literales de función). [ 2 ] En lenguajes con funciones de primera clase, los nombres de las funciones no tienen ningún estatus especial; se tratan como variables ordinarias con un tipo de función . [ 3 ] El término fue acuñado por Christopher Strachey en el contexto de "funciones como ciudadanos de primera clase" a mediados de la década de 1960. [ 4 ]

Las funciones de primera clase son esenciales para la programación funcional , donde el uso de funciones de orden superior es una práctica habitual. Un ejemplo sencillo de función de orden superior es la función `map` , que recibe como argumentos una función y una lista, y devuelve la lista resultante de aplicar la función a cada elemento de la lista. Para que un lenguaje admita `map` , debe permitir pasar una función como argumento.

Existen ciertas dificultades de implementación al pasar funciones como argumentos o devolverlas como resultados, especialmente en presencia de variables no locales introducidas en funciones anidadas y anónimas . Históricamente, estos problemas se denominaron problemas de funarg , nombre que proviene de argumento de función . [ 5 ] En los primeros lenguajes imperativos, estos problemas se evitaron al no admitir funciones como tipos de resultado (por ejemplo, ALGOL 60 , Pascal ) u omitir funciones anidadas y, por lo tanto, variables no locales (por ejemplo, C ). El lenguaje funcional Lisp adoptó el enfoque de ámbito dinámico , donde las variables no locales se refieren a la definición más cercana de esa variable en el punto donde se ejecuta la función, en lugar de donde se definió. El soporte adecuado para funciones de primera clase con ámbito léxico se introdujo en Scheme y requiere manejar referencias a funciones como cierres en lugar de punteros a funciones simples , [ 4 ] lo que a su vez hace que la recolección de basura sea una necesidad.

Conceptos

En esta sección, comparamos cómo se manejan ciertos modismos de programación en un lenguaje funcional con funciones de primera clase ( Haskell ) en comparación con un lenguaje imperativo donde las funciones son elementos de segunda clase ( C ).

Funciones de orden superior: pasar funciones como argumentos

En los lenguajes donde las funciones son ciudadanos de primera clase, las funciones se pueden pasar como argumentos a otras funciones de la misma manera que otros valores (una función que toma otra función como argumento se llama función de orden superior). En el lenguaje Haskell :

map :: ( a -> b ) -> [ a ] ​​-> [ b ] map f [] = [] map f ( x : xs ) = f x : map f xs

Los lenguajes donde las funciones no son de primera clase a menudo permiten escribir funciones de orden superior mediante el uso de características como punteros a funciones o delegados . En el lenguaje C :

void map ( int ( * f )( int ), int x [], size_t n ) { for ( int i = 0 ; i < n ; ++ i ) { x [ i ] = f ( x [ i ]); } }

Hay varias diferencias entre los dos enfoques que no están directamente relacionadas con el soporte de funciones de primera clase. El ejemplo de Haskell opera sobre listas , mientras que el ejemplo de C opera sobre arreglos . Ambas son las estructuras de datos compuestas más naturales en los respectivos lenguajes y hacer que el ejemplo de C operara sobre listas enlazadas lo habría hecho innecesariamente complejo. Esto también explica el hecho de que la función de C necesita un parámetro adicional (que da el tamaño del arreglo). La función de C actualiza el arreglo in situ , sin devolver ningún valor, mientras que en Haskell las estructuras de datos son persistentes (se devuelve una nueva lista mientras que la anterior permanece intacta). El ejemplo de Haskell usa recursión para recorrer la lista, mientras que el ejemplo de C usa iteración . Nuevamente, esta es la forma más natural de expresar esta función en ambos lenguajes, pero el ejemplo de Haskell podría haberse expresado fácilmente en términos de un fold y el ejemplo de C en términos de recursión. Finalmente, la función de Haskell tiene un tipo polimórfico , como esto no es compatible con C, hemos fijado todas las variables de tipo a la constante de tipo int.

Funciones anónimas y anidadas

En los lenguajes que admiten funciones anónimas, podemos pasar una función de este tipo como argumento a una función de orden superior:

principal = map ( \ x -> 3 * x + 1 ) [ 1 , 2 , 3 , 4 , 5 ]

En un lenguaje que no admite funciones anónimas, tenemos que vincularlas a un nombre:

int f ( int x ) { return 3 * x + 1 ; }int main () { int list [] = { 1 , 2 , 3 , 4 , 5 }; map ( f , list , 5 ); }

Variables no locales y cierres

Una vez que tenemos funciones anónimas o anidadas, resulta natural que hagan referencia a variables fuera de su cuerpo (llamadas variables no locales ):

principal = sea a = 3 b = 1 en map ( \ x -> a * x + b ) [ 1 , 2 , 3 , 4 , 5 ]

Si las funciones se representan con punteros a funciones simples, ya no podemos saber cómo se debe pasar a la función el valor que se encuentra fuera de su cuerpo, por lo que es necesario construir manualmente un cierre. Por lo tanto, no podemos hablar de funciones de "primera clase" en este caso.

typedef struct { int ( * f )( int , int , int ); int a ; int b ; } Closure ;void map ( Closure * closure , int x [], size_t n ) { for ( int i = 0 ; i < n ; ++ i ) { x [ i ] = ( closure -> f )( closure -> a , closure -> b , x [ i ]); } }int f ( int a , int b , int x ) { return a * x + b ; }void main () { int l [] = { 1 , 2 , 3 , 4 , 5 }; int a = 3 ; int b = 1 ; Closure closure = { f , a , b }; map ( & closure , l , 5 ); }

También tenga en cuenta que mapahora está especializado para funciones que hacen referencia a dos intelementos fuera de su entorno. Esto se puede configurar de forma más general, pero requiere más código repetitivo . Si fhubiera sido una función anidada, seguiríamos encontrándonos con el mismo problema, y ​​esta es la razón por la que no se admiten en C. [ 6 ]

Funciones de orden superior: devuelven funciones como resultados.

Al devolver una función, en realidad estamos devolviendo su clausura. En el ejemplo de C, cualquier variable local capturada por la clausura quedará fuera de ámbito una vez que regresemos de la función que la construye. Forzar la clausura en un punto posterior resultará en un comportamiento indefinido, posiblemente corrompiendo la pila. Esto se conoce como el problema de los argumentos de función ascendentes .

Asignación de funciones a variables

Asignar funciones a variables y almacenarlas dentro de estructuras de datos (globales) puede presentar las mismas dificultades que devolver funciones.

f :: [[ Entero ] -> [ Entero ]] f = sea a = 3 b = 1 en [ map ( \ x -> a * x + b ), map ( \ x -> b * x + a )]

Igualdad de funciones

Como se puede comprobar la igualdad de la mayoría de los literales y valores, es natural preguntarse si un lenguaje de programación puede admitir la comprobación de igualdad de funciones. Tras un análisis más detallado, esta cuestión parece más compleja y es necesario distinguir entre varios tipos de igualdad de funciones: [ 7 ]

Igualdad extensional
Dos funciones f y g se consideran extensionalmente iguales si coinciden en sus resultados para todas las entradas (∀ x . f ( x ) = g ( x )). Según esta definición de igualdad, por ejemplo, cualquier par de implementaciones de un algoritmo de ordenación estable , como la ordenación por inserción y la ordenación por fusión , se considerarían iguales. Decidir sobre la igualdad extensional es, en general, indecidible , e incluso para funciones con dominios finitos suele ser intratable. Por esta razón, ningún lenguaje de programación implementa la igualdad de funciones como igualdad extensional.
Igualdad intensional
En igualdad intensional, dos funciones f y g se consideran iguales si tienen la misma "estructura interna". Este tipo de igualdad se puede implementar en lenguajes interpretados comparando el código fuente de los cuerpos de las funciones (como en Interpreted Lisp 1.5) o el código objeto en lenguajes compilados . La igualdad intensional implica la igualdad extensional (siempre que las funciones sean deterministas y no tengan entradas ocultas, como el contador de programa o una variable global mutable ).
Igualdad de referencia
Dada la impracticabilidad de implementar la igualdad extensional e intensional, la mayoría de los lenguajes que admiten funciones de prueba de igualdad utilizan la igualdad por referencia. A todas las funciones o cierres se les asigna un identificador único (generalmente la dirección del cuerpo de la función o del cierre) y la igualdad se decide en función de la igualdad de dicho identificador. Dos definiciones de función definidas por separado, pero idénticas en lo demás, se considerarán desiguales. La igualdad por referencia implica la igualdad intensional y extensional. La igualdad por referencia rompe la transparencia referencial y, por lo tanto, no es compatible con lenguajes puros como Haskell.

teoría de tipos

En la teoría de tipos , el tipo de funciones que aceptan valores de tipo A y devuelven valores de tipo B se puede escribir como AB o B A. En la correspondencia de Curry-Howard , los tipos de funciones están relacionados con la implicación lógica ; la abstracción lambda corresponde a la descarga de supuestos hipotéticos y la aplicación de funciones corresponde a la regla de inferencia modus ponens . Además del caso habitual de las funciones de programación, la teoría de tipos también utiliza funciones de primera clase para modelar arreglos asociativos y estructuras de datos similares .

En las explicaciones de la programación basadas en la teoría de categorías , la disponibilidad de funciones de primera clase corresponde al supuesto de categoría cerrada . Por ejemplo, el cálculo lambda simplemente tipado corresponde al lenguaje interno de las categorías cartesianas cerradas .

Soporte de idiomas

Los lenguajes de programación funcional, como Erlang , Scheme , ML , Haskell , F# y Scala , poseen funciones de primera clase. Cuando se diseñó Lisp , uno de los primeros lenguajes funcionales, no se comprendían del todo todos los aspectos de las funciones de primera clase, lo que dio como resultado funciones con ámbito dinámico. Los dialectos posteriores de Scheme y Common Lisp sí cuentan con funciones de primera clase con ámbito léxico.

Muchos lenguajes de scripting, incluidos Perl , Python , PHP , Lua , Tcl /Tk, JavaScript e Io , tienen funciones de primera clase.

En los lenguajes imperativos, es necesario distinguir entre Algol y sus descendientes, como Pascal, la familia C tradicional y las variantes modernas con recolección de basura. La familia Algol permitía funciones anidadas y funciones de orden superior que aceptaban funciones como argumentos, pero no funciones de orden superior que devolvieran funciones como resultado (excepto Algol 68, que sí lo permitía). Esto se debía a que se desconocía cómo manejar variables no locales si se devolvía una función anidada como resultado (y Algol 68 producía errores en tiempo de ejecución en tales casos).

La familia C permitía tanto pasar funciones como argumentos como devolverlas como resultados, pero evitaba problemas al no admitir funciones anidadas. (El compilador gcc las admite como extensión). Dado que la utilidad de devolver funciones reside principalmente en la capacidad de devolver funciones anidadas que han capturado variables no locales, en lugar de funciones de nivel superior, estos lenguajes generalmente no se consideran lenguajes con funciones de primera clase.

Los lenguajes imperativos modernos suelen admitir la recolección de basura, lo que hace posible la implementación de funciones de primera clase. Estas funciones a menudo solo han sido compatibles con revisiones posteriores del lenguaje, como C# 2.0 y la extensión Blocks de Apple para C, C++ y Objective-C. C++11 ha añadido compatibilidad con funciones anónimas y cierres, pero debido a la falta de recolección de basura en el lenguaje, se debe tener especial cuidado con las variables no locales en las funciones que se devuelven como resultados (véase más abajo).

C++
Las clausuras de C++11 pueden capturar variables no locales mediante construcción por copia, por referencia (sin extender su tiempo de vida) o mediante construcción por movimiento (la variable existe mientras exista la clausura). La primera opción es segura si se devuelve la clausura, pero requiere una copia y no se puede usar para modificar la variable original (que podría no existir en el momento en que se llama a la clausura). La segunda opción potencialmente evita una copia costosa y permite modificar la variable original, pero no es segura si se devuelve la clausura (véase referencias colgantes ). La tercera opción es segura si se devuelve la clausura y no requiere una copia, pero tampoco se puede usar para modificar la variable original.
Java
Las clausuras de Java 8 solo pueden capturar variables no locales finales o "efectivamente finales". Los tipos de funciones de Java se representan como clases. Las funciones anónimas adoptan el tipo inferido del contexto. Las referencias a métodos son limitadas. Para más detalles, consulte Función anónima §  Limitaciones de Java .
Ceceo
Las variantes de Lisp con ámbito léxico admiten cierres. Las variantes con ámbito dinámico no admiten cierres o necesitan una construcción especial para crearlos. [ 22 ]
En Common Lisp , el identificador de una función en el espacio de nombres de funciones no se puede usar como referencia a un valor de primera clase. functionSe debe usar el operador especial para recuperar la función como un valor: (function foo)se evalúa como un objeto de función. #'fooexiste como notación abreviada. Para aplicar dicho objeto de función, se debe usar la funcallfunción: (funcall #'foo bar baz).
Pitón
Aplicación parcial explícita functools.partialdesde la versión 2.5 y operator.methodcallerdesde la versión 2.6.
Rubí
El identificador de una "función" regular en Ruby (que en realidad es un método) no se puede usar como valor ni pasar como argumento. Primero debe recuperarse en un Methodobjeto Procpara poder usarse como dato de primera clase. La sintaxis para llamar a dicho objeto función difiere de la de llamar a métodos regulares.
Las definiciones de métodos anidados en realidad no anidan el ámbito.
Curry explícito con .

Véase también

Notas

  1. Abelson, Harold ; Sussman, Gerald Jay (1984). Estructura e interpretación de programas informáticos . MIT Press. Formulación de abstracciones con procedimientos de orden superior . ISBN 0-262-01077-1Archivado del original el 21/09/2021 . Consultado el 27/09/2021 .
  2. Pragmática de los lenguajes de programación , por Michael Lee Scott, sección 11.2 "Programación funcional".
  3. Roberto Ierusalimschy ; Luis Enrique de Figueiredo; Waldemar Celes (2005). "La implementación de Lua 5.0" . Revista de Informática Universal . 11 (7): 1159– 1176. doi : 10.3217/jucs-011-07-1159 .
  4. 1 2 Burstall, Rod; Strachey, Christopher (2000). "Understanding Programming Languages" (PDF) . Higher-Order and Symbolic Computation . 13 (52): 11– 49. doi : 10.1023/A:1010052305354 . S2CID 1989590. Archivado del original el 16 de febrero de 2010. {{cite journal}}: CS1 maint: bot: estado de la URL original desconocido ( enlace ) (también el 16/02/2010)
  5. Joel Moses . "La función de FUNCTION en LISP, o por qué el problema FUNARG debería llamarse el problema del entorno" . MIT AI Memo 199, 1970.
  6. "Si intentas llamar a la función anidada a través de su dirección después de que la función contenedora haya finalizado, se desatará el caos." ( Colección de compiladores GNU: Funciones anidadas )
  7. Andrew W. Appel (1995). "Igualdad intensional  ;=) para continuaciones" .
  8. Tanenbaum, AS (1977). "Una comparación de PASCAL y Algol 68" . The Computer Journal . 21 (4): 319. doi : 10.1093/comjnl/21.4.316 .
  9. "La historia de Python: Orígenes de las características "funcionales" de Python" . 21 de abril de 2009.
  10. Funciones anidadas mediante lambdas/cierres
  11. 1 2 Doc No. 1968 : V Samko; J Willcock, J Järvi, D Gregor, A Lumsdaine (26 de febrero de 2006) Expresiones lambda y cierres para C++
  12. "Centro de desarrollo para Mac: Temas de programación por bloques: Introducción" . Archivado del original el 31 de agosto de 2009.
  13. "2 ejemplos en Go que puedes aplicar parcialmente" .
  14. "partial_application" . Docs.rs. Consultado el 3 de noviembre de 2020 .
  15. "SRFI 26: Notación para la especialización de parámetros sin currificación" .
  16. "John Resig - Aplicación parcial en JavaScript" .
  17. Katz, Ian (23 de julio de 2010). "Código Lua para curry (funciones de currying)" . Archivado del original el 6 de noviembre de 2018.
  18. "Blog | Perlgeek.de :: Currying" . 
  19. "Novedades de Python 2.5 — Documentación de Python 3.10.0" .
  20. "Funciones anónimas - MATLAB y Simulink - MathWorks Reino Unido" .
  21. Evaluación parcial de funciones en MATLAB
  22. Cierres en ZetaLisp Archivado el 19/03/2012 en Wayback Machine

Referencias

  • Leonidas Fegaras . "Lenguajes funcionales y funciones de orden superior" . CSE5317/CSE4305: Diseño y construcción de compiladores. Universidad de Texas en Arlington.