stringtranslate.com

Función de primera clase

En informática , se dice que un lenguaje de programación tiene funciones de primera clase si trata a las funciones 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 requieren 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 una necesidad para el estilo de programación funcional , en el que el uso de funciones de orden superior es una práctica estándar. Un ejemplo simple de una función de orden superior es la función map , que toma como argumentos una función y una lista, y devuelve la lista formada al aplicar la función a cada miembro de la lista. Para que un lenguaje admita map , debe admitir el paso de 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 denominaban problemas funarg , el nombre proviene de "argumento de función". [5] En los primeros lenguajes imperativos, estos problemas se evitaban ya sea no admitiendo funciones como tipos de resultado (por ejemplo, ALGOL 60 , Pascal ) u omitiendo funciones anidadas y, por lo tanto, variables no locales (por ejemplo, C ). El primer lenguaje funcional Lisp adoptó el enfoque del alcance 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 alcance léxico se introdujo en Scheme y requiere el manejo de referencias a funciones como cierres en lugar de punteros de función 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 determinados 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 ciudadanos 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 pueden pasarse como argumentos a otras funciones de la misma manera que otros valores (una función que toma otra función como argumento se denomina función de orden superior). En el lenguaje Haskell :

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

Los lenguajes en los que las funciones no son de primera clase suelen permitir escribir funciones de orden superior mediante el uso de características como punteros de función o delegados . En el lenguaje C :

void map ( int ( * f )( int ), int x [], tamaño_t n ) { para ( 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 en listas , mientras que el ejemplo de C opera en matrices . Ambas son las estructuras de datos compuestas más naturales en los respectivos lenguajes y hacer que el ejemplo de C opere en 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 proporciona el tamaño de la matriz). La función de C actualiza la matriz en el lugar , sin devolver ningún valor, mientras que en Haskell las estructuras de datos son persistentes (se devuelve una nueva lista mientras que la anterior se deja 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 pliegue y el ejemplo de C en términos de recursión. Finalmente, la función de Haskell tiene un tipo polimórfico , ya que C no lo admite, hemos fijado todas las variables de tipo en el tipo constante int.

Funciones anónimas y anidadas

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

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

En un lenguaje que no admite funciones anónimas, debemos vincularlo a un nombre:

int f ( int x ) { devolver 3 * x + 1 ; }         int main () { int lista [] = { 1 , 2 , 3 , 4 , 5 }; mapa ( f , lista , 5 ); }             

Variables no locales y cierres

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

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

Si las funciones se representan con simples punteros de función, ya no podemos saber cómo se le debe pasar el valor que está fuera del cuerpo de la función y, por eso, es necesario crear un cierre manualmente. Por lo tanto, no podemos hablar aquí de funciones de "primera clase".

typedef struct { int ( * f )( int , int , int ); int a ; int b ; } cierre_t ;           void map ( cierre_t * cierre , int x [], tamaño_t n ) { para ( int i = 0 ; i < n ; ++ i ) x [ i ] = ( cierre -> f )( cierre -> a , cierre -> b , x [ i ]); }                     int f ( int a , int b , int x ) { devolver a * x + b ; }             void main () { int l [] = { 1 , 2 , 3 , 4 , 5 }; int a = 3 ; int b = 1 ; cierre_t cierre = { f , a , b }; mapa ( & cierre , l , 5 ); }                           

Tenga en cuenta también que mapahora está especializado en funciones que hacen referencia a dos ints 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 , igualmente nos habríamos encontrado 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

Cuando devolvemos una función, en realidad estamos devolviendo su cierre. En el ejemplo de C, cualquier variable local capturada por el cierre quedará fuera del alcance una vez que regresemos de la función que crea el cierre. Forzar el cierre en un punto posterior dará como resultado un comportamiento indefinido, posiblemente corrompiendo la pila. Esto se conoce como el problema de funarg ascendente .

Asignar funciones a variables

Asignar funciones a variables y almacenarlas dentro de estructuras de datos (globales) potencialmente sufre 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 soportar la comprobación de la igualdad de funciones. Si se analiza más a fondo, esta cuestión parece más difícil y hay que distinguir entre varios tipos de igualdad de funciones: [7]

Igualdad extensional
Dos funciones f y g se consideran extensionalmente iguales si coinciden en sus salidas para todas las entradas (∀ x . f ( x ) = g ( x )). Bajo esta definición de igualdad, por ejemplo, dos implementaciones cualesquiera de un algoritmo de ordenamiento estable , como el ordenamiento por inserción y el ordenamiento por fusión , se considerarían iguales. Decidir sobre la igualdad extensional es indecidible en general e incluso para funciones con dominios finitos a menudo intratable. Por esta razón, ningún lenguaje de programación implementa la igualdad de funciones como igualdad extensional.
Igualdad intencional
En el caso de la igualdad intensional, dos funciones f y g se consideran iguales si tienen la misma "estructura interna". Este tipo de igualdad se podría 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 igualdad extensional (asumiendo que las funciones son deterministas y no tienen entradas ocultas, como el contador del 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 para igualdad utilizan la igualdad de referencia. A todas las funciones o cierres se les asigna un identificador único (normalmente la dirección del cuerpo de la función o del cierre) y la igualdad se decide en función de la igualdad del identificador. Dos definiciones de funciones definidas por separado, pero por lo demás idénticas, se considerarán desiguales. La igualdad referencial implica igualdad extensional e intensional. La igualdad referencial rompe la transparencia referencial y, por tanto, no se admite en 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 suposiciones hipotéticas y la aplicación de la función corresponde a la regla de inferencia del modus ponens . Además del caso habitual de funciones de programación, la teoría de tipos también utiliza funciones de primera clase para modelar matrices asociativas y estructuras de datos similares .

En las teorías de categorías de programación, la disponibilidad de funciones de primera clase corresponde al supuesto de categoría cerrada . Por ejemplo, el cálculo lambda de tipos simples 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 , tienen funciones de primera clase. Cuando se diseñó Lisp , uno de los primeros lenguajes funcionales, no se comprendían adecuadamente todos los aspectos de las funciones de primera clase, lo que dio lugar a que las funciones tuvieran un alcance dinámico. Los dialectos posteriores de Scheme y Common Lisp sí tienen funciones de primera clase con alcance léxico.

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

En el caso de los lenguajes imperativos, es necesario distinguir entre Algol y sus descendientes, como Pascal, la familia C tradicional y las variantes modernas que utilizan la recolección de basura. La familia Algol ha permitido funciones anidadas y funciones de orden superior que toman funciones como argumentos, pero no funciones de orden superior que devuelven funciones como resultados (excepto Algol 68, que lo permite). La razón de esto era que no se sabía cómo tratar con variables no locales si se devolvía una función anidada como resultado (y Algol 68 produce errores de tiempo de ejecución en tales casos).

La familia C permitía pasar funciones como argumentos y devolverlas como resultados, pero evitaba los problemas al no admitir funciones anidadas (el compilador gcc las permite como una extensión). Como la utilidad de devolver funciones radica principalmente en la capacidad de devolver funciones anidadas que han capturado variables no locales, en lugar de funciones de nivel superior, generalmente no se considera que estos lenguajes tengan 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. Las funciones de primera clase a menudo solo se han admitido en revisiones posteriores del lenguaje, incluidas C# 2.0 y la extensión Blocks de Apple para C, C++ y Objective-C. C++11 ha agregado compatibilidad con funciones anónimas y cierres al lenguaje, pero debido a la naturaleza del lenguaje que no recolecta basura, se debe tener especial cuidado con las variables no locales en las funciones que se devuelven como resultados (ver a continuación).

C++
Los cierres de C++11 pueden capturar variables no locales mediante construcción de copia, por referencia (sin extender su vida útil) o por construcción de movimiento (la variable vive tanto como el cierre). La primera opción es segura si se devuelve el cierre, pero requiere una copia y no se puede usar para modificar la variable original (que podría no existir más en el momento en que se llama al cierre). La segunda opción evita potencialmente una copia costosa y permite modificar la variable original, pero no es segura en caso de que se devuelva el cierre (consulte referencias colgantes ). La tercera opción es segura si se devuelve el cierre y no requiere una copia, pero tampoco se puede usar para modificar la variable original.
Java
Los cierres 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 toman el tipo inferido del contexto. Las referencias a métodos son limitadas. Para obtener 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 crear cierres. [22]
En Common Lisp , el identificador de una función en el espacio de nombres de funciones no se puede utilizar como referencia a un valor de primera clase. functionSe debe utilizar el operador especial para recuperar la función como valor: (function foo)evalúa a un objeto de función. #'fooexiste como una notación abreviada. Para aplicar un objeto de función de este tipo, se debe utilizar 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 utilizar como valor ni pasar. Primero se debe recuperar en un objeto Methodor Procpara usarlo como dato de primera clase. La sintaxis para llamar a un objeto de función de este tipo difiere de la de llamar a métodos regulares.
Las definiciones de métodos anidados en realidad no anidan el alcance.
Curry explícito con [1].

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 desde el original el 21 de septiembre de 2021. Consultado el 27 de septiembre de 2021 .
  2. ^ Pragmática del lenguaje 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. ^ ab 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 desde el original el 16 de febrero de 2010.{{cite journal}}: CS1 maint: bot: estado de URL original desconocido ( enlace )(también el 16 de febrero de 2010)
  5. ^ Joel Moses . "La función de FUNCTION en LISP, o por qué el problema de 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 que la contiene haya salido, se desatará el infierno". (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 que utilizan lambdas/cierres
  11. ^ ab Doc No. 1968: V Samko; J Willcock, J Järvi, D Gregor, A Lumsdaine (26 de febrero de 2006) Expresiones y cierres lambda para C++
  12. ^ "Mac Dev Center: Temas de programación en bloques: Introducción". Archivado desde el original el 31 de agosto de 2009.
  13. ^ "2 ejemplos en Go que pueden tener aplicación parcial".
  14. ^ "partial_application". Docs.rs . Consultado el 3 de noviembre de 2020 .
  15. ^ "SRFI 26: Notación para especializar parámetros sin currificar".
  16. ^ "John Resig - Aplicación parcial en JavaScript".
  17. ^ Katz, Ian (23 de julio de 2010). "Código Lua para Curry (funciones de curry)". Archivado desde el original el 6 de noviembre de 2018.
  18. ^ "Blog | Perlgeek.de :: Curry".
  19. ^ "Novedades en Python 2.5 — Documentación de Python 3.10.0".
  20. ^ "Funciones anónimas - MATLAB y Simulink - MathWorks España".
  21. ^ Evaluación de funciones parciales en MATLAB
  22. ^ Cierres en ZetaLisp Archivado el 19 de marzo de 2012 en Wayback Machine.

Referencias

Enlaces externos