stringtranslate.com

Función (programación informática)

En programación de computadoras , una función , subprograma , procedimiento , método , rutina , subrutina o unidad invocable [1] es una secuencia de instrucciones que tiene un comportamiento bien definido y puede ser invocada por otro software para exhibir ese comportamiento.

Las unidades invocables proporcionan una poderosa herramienta de programación. [2] El propósito principal es permitir la descomposición de un problema grande y/o complicado en partes que tienen una carga cognitiva relativamente baja y asignar a las partes nombres significativos (a menos que sean anónimos). Una aplicación juiciosa puede reducir el costo de desarrollo y mantenimiento de software, al tiempo que aumenta su calidad y confiabilidad. [3]

Las unidades invocables están presentes en múltiples niveles de abstracción en el entorno de programación. Por ejemplo, un programador puede escribir una función en código fuente que se compila en código de máquina que implementa una semántica similar . Hay una unidad invocable en el código fuente y una asociada en el código máquina, pero son diferentes tipos de unidades invocables, con diferentes implicaciones y características.

Historia

La idea de una unidad invocable fue concebida inicialmente por John Mauchly y Kathleen Antonelli durante su trabajo en ENIAC , [4] y registrada en un simposio de Harvard en enero de 1947 sobre "Preparación de problemas para máquinas tipo EDVAC ". [5] A Maurice Wilkes , David Wheeler y Stanley Gill generalmente se les atribuye la invención formal de este concepto, al que denominaron subrutina cerrada , [6] [7] en contraste con una subrutina abierta o macro . [8] Sin embargo, Alan Turing había discutido las subrutinas en un artículo de 1945 sobre propuestas de diseño para el NPL ACE , llegando incluso a inventar el concepto de una pila de remitentes . [9]

La idea de una subrutina surgió cuando las máquinas informáticas ya existían desde hacía algún tiempo. Las instrucciones aritméticas y de salto condicional se planificaron con anticipación y han cambiado relativamente poco, pero las instrucciones especiales utilizadas para las llamadas a procedimientos han cambiado mucho a lo largo de los años. Las primeras computadoras y microprocesadores, como el Manchester Baby y el RCA 1802 , no tenían ni una sola instrucción de llamada a subrutina. Se podían implementar subrutinas, pero requerían que los programadores usaran la secuencia de llamada (una serie de instrucciones) en cada sitio de llamada .

Las subrutinas se implementaron en el Z4 de Konrad Zuse en 1945.

En 1945, Alan M. Turing utilizó los términos "enterrar" y "desenterrar" como medio para llamar y regresar de subrutinas. [10] [11]

En enero de 1947, John Mauchly presentó notas generales en el "Simposio sobre maquinaria de cálculo digital a gran escala" bajo el patrocinio conjunto de la Universidad de Harvard y la Oficina de Artillería de la Marina de los Estados Unidos. Aquí analiza la operación en serie y en paralelo, sugiriendo

...la estructura de la máquina no tiene por qué ser complicada en lo más mínimo. Es posible, puesto que están disponibles todas las características lógicas esenciales para este procedimiento, desarrollar una instrucción de codificación para colocar las subrutinas en la memoria en lugares conocidos por la máquina, y de tal manera que puedan ser utilizadas fácilmente.

En otras palabras, se puede designar la subrutina A como división, la subrutina B como multiplicación compleja y la subrutina C como evaluación de un error estándar de una secuencia de números, y así sucesivamente a través de la lista de subrutinas necesarias para un problema particular. ... Todas estas subrutinas quedarán entonces almacenadas en la máquina, y bastará con hacer una breve referencia a ellas mediante el número, tal como se indica en la codificación. [5]

Kay McNulty había trabajado estrechamente con John Mauchly en el equipo ENIAC y desarrolló una idea de subrutinas para la computadora ENIAC que estaba programando durante la Segunda Guerra Mundial. [12] Ella y los otros programadores de ENIAC utilizaron las subrutinas para ayudar a calcular las trayectorias de los misiles. [12]

Goldstine y von Neumann escribieron un artículo fechado el 16 de agosto de 1948 sobre el uso de subrutinas. [13]

Algunas de las primeras computadoras y microprocesadores, como IBM 1620 , Intel 4004 e Intel 8008 , y los microcontroladores PIC , tienen una llamada de subrutina de una sola instrucción que utiliza una pila de hardware dedicada para almacenar direcciones de retorno; dicho hardware solo admite unos pocos niveles. de anidamiento de subrutinas, pero puede admitir subrutinas recursivas. Las máquinas anteriores a mediados de la década de 1960, como la UNIVAC I , la PDP-1 y la IBM 1130 , suelen utilizar una convención de llamada que guardaba el contador de instrucciones en la primera ubicación de memoria de la subrutina llamada. Esto permite niveles arbitrariamente profundos de anidamiento de subrutinas, pero no admite subrutinas recursivas. El IBM System/360 tenía una instrucción de llamada de subrutina que colocaba el valor del contador de instrucciones guardado en un registro de propósito general; esto se puede utilizar para admitir anidamiento de subrutinas arbitrariamente profundo y subrutinas recursivas. El Burroughs B5000 [14] (1961) es una de las primeras computadoras en almacenar datos de retorno de subrutinas en una pila.

El DEC PDP-6 [15] (1964) es una de las primeras máquinas basadas en acumuladores que tiene una instrucción de llamada de subrutina que guarda la dirección de retorno en una pila direccionada por un acumulador o registro de índice. Las líneas posteriores PDP-10 (1966), PDP-11 (1970) y VAX-11 (1976) siguieron su ejemplo; Esta característica también admite anidamiento de subrutinas arbitrariamente profundo y subrutinas recursivas. [dieciséis]

Ayuda de idioma

En los primeros ensambladores, el soporte de subrutinas era limitado. Las subrutinas no estaban separadas explícitamente entre sí ni del programa principal y, de hecho, el código fuente de una subrutina podía intercalarse con el de otros subprogramas. Algunos ensambladores ofrecerían macros predefinidas para generar las secuencias de llamada y retorno. En la década de 1960, los ensambladores generalmente tenían un soporte mucho más sofisticado para subrutinas ensambladas en línea y por separado que podían vincularse entre sí.

Uno de los primeros lenguajes de programación que admitió subrutinas y funciones escritas por el usuario fue FORTRAN II . El compilador IBM FORTRAN II se lanzó en 1958. ALGOL 58 y otros lenguajes de programación antiguos también admitían la programación procedimental.

Bibliotecas

Incluso con este enfoque engorroso, las subrutinas resultaron muy útiles. Permitieron el uso del mismo código en muchos programas diferentes. La memoria era un recurso muy escaso en las primeras computadoras y las subrutinas permitían ahorros significativos en el tamaño de los programas.

Muchas de las primeras computadoras cargaban las instrucciones del programa en la memoria desde una cinta de papel perforada . Cada subrutina podría entonces ser proporcionada por un trozo de cinta separado, cargado o empalmado antes o después del programa principal (o "línea principal" [17] ); y la misma cinta de subrutina podría ser utilizada por muchos programas diferentes. Se utilizó un enfoque similar en computadoras que cargaban instrucciones de programas a partir de tarjetas perforadas . El nombre biblioteca de subrutinas originalmente significaba una biblioteca, en sentido literal, que mantenía colecciones indexadas de cintas o barajas de cartas para uso colectivo.

Regreso por salto indirecto

Para eliminar la necesidad de un código automodificado , los diseñadores de computadoras finalmente proporcionaron una instrucción de salto indirecto , cuyo operando, en lugar de ser la dirección de retorno en sí, era la ubicación de una variable o registro del procesador que contenía la dirección de retorno.

En esas computadoras, en lugar de modificar el salto de retorno de la función, el programa que llama almacenaría la dirección de retorno en una variable para que cuando la función se completara, ejecutara un salto indirecto que dirigiría la ejecución a la ubicación dada por la variable predefinida.

Saltar a subrutina

Otro avance fue el salto a la instrucción de subrutina, que combinaba el guardado de la dirección del remitente con el salto de llamada, minimizando así significativamente los gastos generales .

En IBM System/360 , por ejemplo, las instrucciones de bifurcación BAL o BALR, diseñadas para llamar a procedimientos, guardarían la dirección de retorno en un registro del procesador especificado en la instrucción, mediante el registro de convención 14. Para regresar, la subrutina solo tenía que ejecutar una instrucción de bifurcación indirecta (BR) a través de ese registro. Si la subrutina necesitara ese registro para algún otro propósito (como llamar a otra subrutina), guardaría el contenido del registro en una ubicación de memoria privada o en una pila de registros .

En sistemas como el HP 2100 , la instrucción JSB realizaría una tarea similar, excepto que la dirección del remitente se almacenaba en la ubicación de la memoria que era el destino de la rama. En realidad, la ejecución del procedimiento comenzaría en la siguiente ubicación de la memoria. En el lenguaje ensamblador HP 2100, se escribiría, por ejemplo

...JSB MYSUB (Llama a la subrutina MYSUB.)BB... (Volveré aquí cuando termine MYSUB).

para llamar una subrutina llamada MYSUB desde el programa principal. La subrutina se codificaría como

MYSUB NOP (Almacenamiento de la dirección del remitente de MYSUB).AA... (Inicio del cuerpo de MYSUB.)...JMP MYSUB,I (Vuelve al programa de llamada).

La instrucción JSB colocó la dirección de la instrucción SIGUIENTE (es decir, BB) en la ubicación especificada como su operando (es decir, MYSUB), y luego se bifurcó a la ubicación SIGUIENTE después de eso (es decir, AA = MYSUB + 1). La subrutina podría luego regresar al programa principal ejecutando el salto indirecto JMP MYSUB, I que se bifurcó a la ubicación almacenada en la ubicación MYSUB.

Los compiladores de Fortran y otros lenguajes podrían utilizar fácilmente estas instrucciones cuando estén disponibles. Este enfoque admitía múltiples niveles de llamadas; sin embargo, dado que a la dirección de retorno, los parámetros y los valores de retorno de una subrutina se les asignaron ubicaciones de memoria fijas, no permitía llamadas recursivas.

Por cierto, Lotus 1-2-3 utilizó un método similar a principios de la década de 1980 para descubrir las dependencias de recálculo en una hoja de cálculo. Es decir, se reservó una ubicación en cada celda para almacenar la dirección del remitente . Dado que no se permiten referencias circulares para el orden de recálculo natural, esto permite recorrer un árbol sin reservar espacio para una pila en la memoria, lo cual era muy limitado en computadoras pequeñas como la IBM PC .

Pila de llamadas

La mayoría de las implementaciones modernas de una llamada a función utilizan una pila de llamadas , un caso especial de la estructura de datos de la pila , para implementar llamadas y retornos a funciones. Cada llamada a procedimiento crea una nueva entrada, llamada marco de pila , en la parte superior de la pila; cuando el procedimiento regresa, su marco de pila se elimina de la pila y su espacio puede usarse para otras llamadas a procedimientos. Cada marco de pila contiene los datos privados de la llamada correspondiente, que normalmente incluyen los parámetros y variables internas del procedimiento, y la dirección de retorno.

La secuencia de llamada se puede implementar mediante una secuencia de instrucciones ordinarias (un enfoque todavía utilizado en arquitecturas de computación con conjunto de instrucciones reducido (RISC) y palabras de instrucción muy largas (VLIW), pero muchas máquinas tradicionales diseñadas desde finales de la década de 1960 han incluido instrucciones especiales para ese propósito.

La pila de llamadas suele implementarse como un área contigua de memoria. Es una elección de diseño arbitraria si la parte inferior de la pila es la dirección más baja o más alta dentro de esta área, de modo que la pila pueda crecer hacia adelante o hacia atrás en la memoria; sin embargo, muchas arquitecturas eligieron lo último. [ cita necesaria ]

Algunos diseños, en particular algunas implementaciones de Forth , utilizaban dos pilas separadas, una principalmente para información de control (como direcciones de retorno y contadores de bucle) y la otra para datos. El primero era, o funcionaba como, una pila de llamadas y el programador sólo podía acceder indirectamente a través de otras construcciones del lenguaje, mientras que el segundo era accesible más directamente.

Cuando se introdujeron por primera vez las llamadas a procedimientos basados ​​en pila, una motivación importante fue ahorrar una memoria preciosa. [ cita necesaria ] Con este esquema, el compilador no tiene que reservar espacio separado en la memoria para los datos privados (parámetros, dirección de retorno y variables locales) de cada procedimiento. En cualquier momento, la pila contiene solo los datos privados de las llamadas que están actualmente activas (es decir, las que han sido llamadas pero aún no han regresado). Debido a la forma en que los programas generalmente se ensamblaban a partir de bibliotecas, no era (y sigue siendo) raro encontrar programas que incluyen miles de funciones, de las cuales sólo unas pocas están activas en un momento dado. [ cita necesaria ] Para tales programas, el mecanismo de pila de llamadas podría ahorrar cantidades significativas de memoria. De hecho, el mecanismo de pila de llamadas puede verse como el método más antiguo y sencillo para la gestión automática de la memoria .

Sin embargo, otra ventaja del método de pila de llamadas es que permite llamadas a funciones recursivas , ya que cada llamada anidada al mismo procedimiento obtiene una instancia separada de sus datos privados.

En un entorno de subprocesos múltiples , generalmente hay más de una pila. [18] Un entorno que admita totalmente corrutinas o evaluación diferida puede utilizar estructuras de datos distintas de las pilas para almacenar sus registros de activación.

Apilamiento retrasado

Una desventaja del mecanismo de pila de llamadas es el mayor costo de una llamada a procedimiento y su retorno coincidente. [ aclaración necesaria ] El costo adicional incluye incrementar y disminuir el puntero de la pila (y, en algunas arquitecturas, verificar el desbordamiento de la pila ) y acceder a las variables y parámetros locales mediante direcciones relativas al marco, en lugar de direcciones absolutas. El costo puede materializarse en un mayor tiempo de ejecución, una mayor complejidad del procesador, o ambas cosas.

Esta sobrecarga es más obvia y objetable en los procedimientos hoja o funciones hoja , que regresan sin realizar ninguna llamada a ningún procedimiento. [19] [20] [21] Para reducir esa sobrecarga, muchos compiladores modernos intentan retrasar el uso de una pila de llamadas hasta que sea realmente necesario. [ cita necesaria ] Por ejemplo, la llamada de un procedimiento P puede almacenar la dirección de retorno y los parámetros del procedimiento llamado en ciertos registros del procesador y transferir el control al cuerpo del procedimiento mediante un simple salto. Si el procedimiento P regresa sin realizar ninguna otra llamada, la pila de llamadas no se utiliza en absoluto. Si P necesita llamar a otro procedimiento Q , usará la pila de llamadas para guardar el contenido de cualquier registro (como la dirección de retorno) que será necesario después de que Q regrese.

Características

En general, una unidad invocable es una lista de instrucciones que, comenzando con la primera instrucción, se ejecuta secuencialmente excepto según lo indique su lógica interna. Se puede invocar (llamar) muchas veces durante la ejecución de un programa. La ejecución continúa en la siguiente instrucción después de la instrucción de llamada cuando devuelve el control.

Implementaciones

Las características de las implementaciones de unidades invocables evolucionaron con el tiempo y varían según el contexto. Esta sección describe las características de las diversas implementaciones comunes.

Características generales

La mayoría de los lenguajes de programación modernos proporcionan funciones para definir y llamar funciones, incluida la sintaxis para acceder a dichas funciones, que incluyen:

Nombrar

Algunos lenguajes, como Pascal , Fortran , Ada y muchos dialectos de BASIC , usan un nombre diferente para una unidad invocable que devuelve un valor ( función o subprograma ) frente a una que no lo hace ( subrutina o procedimiento ). Otros lenguajes, como C , C++ , C# y Lisp , usan solo un nombre para una unidad invocable, función . Los lenguajes de la familia C usan la palabra clave voidpara indicar que no hay valor de retorno.

Sintaxis de llamada

Si se declara que devuelve un valor, se puede incrustar una llamada en una expresión para consumir el valor de retorno. Por ejemplo, una unidad exigible de raíz cuadrada podría llamarse como y = sqrt(x).

Una unidad invocable que no devuelve un valor se llama como una declaración independiente como print("hello"). Esta sintaxis también se puede utilizar para una unidad invocable que devuelve un valor, pero el valor devuelto se ignorará.

Algunos lenguajes más antiguos requieren una palabra clave para llamadas que no consumen un valor de retorno, como CALL print("hello").

Parámetros

La mayoría de las implementaciones, especialmente en lenguajes modernos, admiten parámetros que el invocable declara como parámetros formales . Una persona que llama pasa parámetros reales , también conocidos como argumentos , para que coincidan. Los diferentes lenguajes de programación proporcionan diferentes convenciones para pasar argumentos.

Valor de retorno

En algunos lenguajes, como BASIC, un invocable tiene una sintaxis diferente (es decir, una palabra clave) para un invocable que devuelve un valor frente a uno que no lo hace. En otros idiomas, la sintaxis es la misma independientemente. En algunos de estos idiomas se utiliza una palabra clave adicional para declarar que no hay valor de retorno; por ejemplo voiden C, C++ y C#. En algunos lenguajes, como Python, la diferencia es si el cuerpo contiene una declaración de retorno con un valor, y un invocable particular puede regresar con o sin un valor según el flujo de control.

Efectos secundarios

En muchos contextos, un invocable puede tener un comportamiento de efectos secundarios , como modificar datos pasados ​​o globales, leer o escribir en un dispositivo periférico , acceder a un archivo , detener el programa o la máquina, o pausar temporalmente la ejecución del programa.

En lenguajes de programación estrictamente funcionales como Haskell , una función no puede tener efectos secundarios , lo que significa que no puede cambiar el estado del programa. Las funciones siempre devuelven el mismo resultado para la misma entrada. Por lo general, estos lenguajes solo admiten funciones que devuelven un valor, ya que no hay ningún valor en una función que no tenga valor de retorno ni efectos secundarios.

variables locales

La mayoría de los contextos admiten variables locales : memoria propiedad de un invocable para contener valores intermedios. Estas variables generalmente se almacenan en el registro de activación de la llamada en la pila de llamadas junto con otra información como la dirección del remitente .

Llamada anidada – recursividad

Si el lenguaje lo admite, un invocable puede llamarse a sí mismo, provocando que su ejecución se suspenda mientras se ejecuta otra ejecución anidada del mismo invocable. La recursividad es un medio útil para simplificar algunos algoritmos complejos y analizar problemas complejos. Los lenguajes recursivos proporcionan una nueva copia de las variables locales en cada llamada. Si el programador desea que el recursivo invocable use las mismas variables en lugar de locales, generalmente las declara en un contexto compartido, como estático o global.

Los lenguajes que se remontan a ALGOL , PL/I y C y los lenguajes modernos casi invariablemente utilizan una pila de llamadas, generalmente respaldada por conjuntos de instrucciones para proporcionar un registro de activación para cada llamada. De esa manera, una llamada anidada puede modificar sus variables locales sin afectar ninguna de las variables de las llamadas suspendidas.

La recursividad permite la implementación directa de funcionalidades definidas por inducción matemática y algoritmos recursivos de divide y vencerás . Aquí hay un ejemplo de una función recursiva en C/C++ para encontrar números de Fibonacci :

int Fib ( int n ) { si ( n <= 1 ) { return n ; } devolver Fib ( n - 1 ) + Fib ( n - 2 ); }                   

Los primeros lenguajes como Fortran inicialmente no admitían la recursividad porque solo se asignaba un conjunto de variables y una dirección de retorno para cada invocable. [22] Los primeros conjuntos de instrucciones informáticas dificultaban el almacenamiento de direcciones de retorno y variables en una pila. Las máquinas con registros de índice o registros de uso general , por ejemplo, series CDC 6000 , PDP-6 , GE 635 , System/360 , UNIVAC 1100 , podrían usar uno de esos registros como puntero de pila .

Alcance anidado

Algunos lenguajes como Pascal , PL/I y Ada admiten un alcance anidado para una función invocable, también conocida como función anidada , de modo que un invocable interno solo puede ser invocado por el invocable externo. El invocable interno puede tener acceso a las variables locales del externo.

Reentrada

Si un invocable se puede ejecutar correctamente incluso cuando ya hay otra ejecución del mismo invocable en progreso, se dice que ese invocable es reentrante . Un invocable reentrante también es útil en situaciones de subprocesos múltiples , ya que varios subprocesos pueden llamar al mismo invocable sin temor a interferir entre sí. En el sistema de procesamiento de transacciones IBM CICS , el cuasi-reentrante era un requisito ligeramente menos restrictivo, pero similar, para los programas de aplicación que eran compartidos por muchos subprocesos.

Sobrecarga

Algunos lenguajes admiten la sobrecarga : permiten múltiples invocables con el mismo nombre en el mismo ámbito, pero que operan en diferentes tipos de entrada. Considere la función de raíz cuadrada aplicada a números reales, números complejos y entradas matriciales. El algoritmo para cada tipo de entrada es diferente y el valor de retorno puede tener un tipo diferente. Escribiendo tres invocables separados con el mismo nombre. es decir , sqrt , el código resultante puede ser más fácil de escribir y mantener ya que cada uno tiene un nombre que es relativamente fácil de entender y recordar en lugar de dar nombres más largos y complicados como sqrt_real , sqrt_complex , qrt_matrix .

La sobrecarga se admite en muchos idiomas que admiten escritura segura . A menudo, el compilador selecciona la sobrecarga a llamar según el tipo de argumentos de entrada o falla si los argumentos de entrada no seleccionan una sobrecarga. Los lenguajes más antiguos y con tipos débiles generalmente no admiten la sobrecarga.

A continuación se muestra un ejemplo de sobrecarga en C++ , dos funciones Areaque aceptan diferentes tipos:

// devuelve el área de un rectángulo definido por alto y ancho double Area ( double h , double w ) { return h * w ; }          // devuelve el área de un círculo definido por el radio double Area ( double r ) { return r * r * 3.14 ; }          int main () { double rectángulo_area = Área ( 3 , 4 ); doble circulo_area = Área ( 5 ); }           

PL/I tiene el GENERICatributo de definir un nombre genérico para un conjunto de referencias de entrada llamadas con diferentes tipos de argumentos. Ejemplo:

DECLARAR gen_name GENERIC( nombre CUANDO (BINARIO FIJO), llama CUANDO (FLOTADOR), nombre de ruta DE LO CONTRARIO);

Se pueden especificar múltiples definiciones de argumentos para cada entrada. Una llamada a "gen_name" resultará en una llamada a "nombre" cuando el argumento sea BINARIO FIJO, "llama" cuando sea FLOAT", etc. Si el argumento coincide, no se llamará a ninguna de las opciones "nombre de ruta".

Cierre

Un cierre es un valor invocable más de algunas de sus variables capturadas del entorno en el que fue creado. Los cierres fueron una característica notable del lenguaje de programación Lisp, introducido por John McCarthy . Dependiendo de la implementación, los cierres pueden servir como mecanismo para efectos secundarios.

Informes de excepciones

Además de su comportamiento de ruta feliz , es posible que un invocable deba informar a la persona que llama sobre una condición excepcional que ocurrió durante su ejecución.

La mayoría de los lenguajes modernos admiten excepciones, lo que permite un flujo de control excepcional que abre la pila de llamadas hasta que se encuentra un controlador de excepciones para manejar la condición.

Los idiomas que no admiten excepciones pueden utilizar el valor de retorno para indicar el éxito o el fracaso de una llamada. Otro enfoque es utilizar una ubicación conocida como variable global para indicar el éxito. Un invocable escribe el valor y la persona que llama lo lee después de una llamada.

En IBM System/360 , donde se esperaba un código de retorno de una subrutina, el valor de retorno a menudo se diseñaba para que fuera un múltiplo de 4, de modo que pudiera usarse como un índice de tabla de rama directa en una tabla de rama a menudo ubicada inmediatamente después de la llame a la instrucción para evitar pruebas condicionales adicionales, mejorando aún más la eficiencia. En el lenguaje ensamblador System/360 , se escribiría, por ejemplo:

 BAL 14, SUBRTN01 va a una subrutina y almacena la dirección de retorno en R14 B TABLE(15) usa el valor devuelto en el registro 15 para indexar la tabla de sucursales,* bifurcación a la rama apropiada instr.TABLA B OK código de retorno =00 BUENO } B Código de retorno BAD = 04 Entrada no válida } Tabla de sucursales B Código de retorno de ERROR = 08 Condición inesperada}

Llamada general

Una llamada tiene una sobrecarga de tiempo de ejecución , que puede incluir, entre otros:

Se emplean varias técnicas para minimizar el costo de tiempo de ejecución de las llamadas.

Optimización del compilador

Algunas optimizaciones para minimizar la sobrecarga de llamadas pueden parecer sencillas, pero no se pueden utilizar si lo invocable tiene efectos secundarios. Por ejemplo, en la expresión (f(x)-1)/(f(x)+1), la función fno se puede llamar solo una vez y su valor se usa dos veces, ya que las dos llamadas pueden devolver resultados diferentes. Además, en los pocos idiomas que definen el orden de evaluación de los operandos del operador de división, el valor de xdebe recuperarse antes de la segunda llamada, ya que la primera llamada puede haberlo cambiado. Determinar si un invocable tiene un efecto secundario es difícil; de hecho, indecidible en virtud del teorema de Rice . Entonces, si bien esta optimización es segura en un lenguaje de programación puramente funcional, un compilador para un lenguaje que no se limita a lo funcional generalmente asume el peor de los casos, que cada invocable puede tener efectos secundarios.

en línea

La inserción elimina las llamadas a callables particulares. El compilador reemplaza cada llamada con el código compilado del invocable. Esto no solo evita la sobrecarga de la llamada, sino que también permite que el compilador optimice el código de la persona que llama de manera más efectiva al tener en cuenta el contexto y los argumentos de esa llamada. Sin embargo, la inserción generalmente aumenta el tamaño del código compilado, excepto cuando solo se llama una vez o el cuerpo es muy corto, como una línea.

Intercambio

Los callables se pueden definir dentro de un programa o por separado en una biblioteca que puede ser utilizada por múltiples programas.

Interoperabilidad

Un compilador traduce declaraciones de llamada y devolución en instrucciones de máquina de acuerdo con una convención de llamada bien definida . Para el código compilado por el mismo compilador o por uno compatible, las funciones se pueden compilar por separado de los programas que las llaman. Las secuencias de instrucciones correspondientes a las declaraciones de llamada y devolución se denominan prólogo y epílogo del procedimiento .

Funciones integradas

Una función incorporada , o función incorporada , o función intrínseca , es una función para la cual el compilador genera código en el momento de la compilación o lo proporciona de una manera distinta a otras funciones. [23] No es necesario definir una función incorporada como otras funciones, ya que está incorporada en el lenguaje de programación. [24]

Programación

Compensaciones

Ventajas

Las ventajas de dividir un programa en funciones incluyen:

Desventajas

En comparación con el uso de código en línea, invocar una función impone cierta sobrecarga computacional en el mecanismo de llamada. [ cita necesaria ]

Una función normalmente requiere un código de mantenimiento estándar , tanto en la entrada como en la salida de la función ( prólogo y epílogo de la función , que generalmente guarda registros de propósito general y la dirección del remitente como mínimo).

Convenciones

Se han desarrollado muchas convenciones de programación con respecto a los invocables.

Con respecto a la denominación, muchos desarrolladores nombran un invocable con una frase que comienza con un verbo cuando realiza una determinada tarea, con un adjetivo cuando realiza una consulta y con un sustantivo cuando se usa para sustituir variables.

Algunos programadores sugieren que un invocable debe realizar exactamente una tarea y, si realiza más de una tarea, debe dividirse en varios invocables. Argumentan que los callables son componentes clave en el mantenimiento del software y sus roles en el programa deben seguir siendo distintos.

Los defensores de la programación modular defienden que cada invocable debería tener una dependencia mínima del resto del código base . Por ejemplo, el uso de variables globales generalmente se considera imprudente, porque agrega acoplamiento entre todos los invocables que usan las variables globales. Si dicho acoplamiento no es necesario, recomiendan refactorizar los invocables para aceptar los parámetros pasados .

Ejemplos

BÁSICO temprano

Las primeras variantes de BASIC requieren que cada línea tenga un número único ( número de línea ) que ordena las líneas para su ejecución, no proporciona separación del código invocable, ningún mecanismo para pasar argumentos o devolver un valor y todas las variables son globales. Proporciona el comando GOSUBdonde sub es la abreviatura de subprocedimiento , subprocedimiento o subrutina . El control salta al número de línea especificado y luego continúa en la siguiente línea al regresar.

10 REM A PROGRAMA BÁSICO 20 GOSUB 100 30 GOTO 20 100 ENTRADA DAME UN NÚMERO ; N 110 IMPRIMIR LA RAÍZ CUADRADA DE ; norte ; 120 IMPRIMIR ES ; CUADRADO ( N ) 130 VOLVER                      

Este código solicita repetidamente al usuario que ingrese un número e informa la raíz cuadrada del valor. Las líneas 100-130 son las que se pueden llamar.

Pequeño Básico

En Microsoft Small Basic , dirigido al estudiante que aprende por primera vez a programar en un lenguaje basado en texto, una unidad invocable se llama subrutina . La Subpalabra clave indica el inicio de una subrutina y va seguida de un identificador de nombre. Las líneas siguientes son el cuerpo que termina con la EndSubpalabra clave. [25]

Sub SayHello TextWindow . WriteLine ( "¡Hola!" ) EndSub  

Esto se puede llamar como SayHello().[26]

Visual Básico

En versiones posteriores de Visual Basic (VB), incluida la última línea de productos y VB6 , el término procedimiento se utiliza para el concepto de unidad invocable. La palabra clave Subse utiliza para no devolver ningún valor y Functionpara devolver un valor. Cuando se usa en el contexto de una clase, un procedimiento es un método.[27]

Cada parámetro tiene un tipo de datos que se puede especificar, pero si no, el valor predeterminado es Objectpara versiones posteriores basadas en .NET y variante para VB6 . [28]

VB admite convenciones de paso de parámetros por valor y por referencia a través de las palabras clave ByValy ByRef, respectivamente. A menos que ByRefse especifique, se pasa un argumento ByVal. Por lo tanto, ByValrara vez se especifica explícitamente.

Para un tipo simple como un número, estas convenciones son relativamente claras. Pasar ByRefpermite que el procedimiento modifique la variable pasada, mientras que pasar ByValno. Para un objeto, la semántica puede confundir a los programadores ya que un objeto siempre se trata como una referencia. Pasar un objeto ByValcopia la referencia; no el estado del objeto. El procedimiento llamado puede modificar el estado del objeto a través de sus métodos pero no puede modificar la referencia del objeto del parámetro real.

Sub DoSomething () ' Algún código aquí finaliza Sub   

No devuelve un valor y debe llamarse independiente, comoDoSomething

Función GiveMeFive () como número entero GiveMeFive = 5 Función final      

Esto devuelve el valor 5 y una llamada puede ser parte de una expresión comoy = x + GiveMeFive()

Sub AddTwo ( ByRef intValue como entero ) intValue = intValue + 2 End Sub          

Esto tiene un efecto secundario: modifica la variable pasada por referencia y podría invocarse para variables vcomo AddTwo(v). Dando v es 5 antes de la llamada, será 7 después.

C y C++

En C y C++ , una unidad invocable se llama función . La definición de una función comienza con el nombre del tipo de valor que devuelve o voidpara indicar que no devuelve un valor. A esto le sigue el nombre de la función, los argumentos formales entre paréntesis y las líneas del cuerpo entre llaves.

En C++ , una función declarada en una clase (como no estática) se denomina función o método miembro . Una función fuera de una clase se puede llamar función libre para distinguirla de una función miembro.[29]

void hacer algo () { /* algún código */ }   

Esta función no devuelve un valor y siempre se llama independiente, comodoSomething()

int daMeCinco () { retorno 5 ; }    

Esta función devuelve el valor entero 5. La llamada puede ser independiente o en una expresión comoy = x + giveMeFive()

void addTwo ( int * pi ) { * pi += 2 ; }      

Esta función tiene un efecto secundario: modifica el valor pasado por dirección al valor de entrada más 2. Podría llamarse para variable vcuando addTwo(&v)el signo (&) le dice al compilador que pase la dirección de una variable. Dando v es 5 antes de la llamada, será 7 después.

void addTwo ( int & i ) { i += 2 ; }      

Esta función requiere C++; no se compilaría como C. Tiene el mismo comportamiento que el ejemplo anterior, pero pasa el parámetro real por referencia en lugar de pasar su dirección. Una llamada como addTwo(v)no incluye un signo comercial ya que el compilador maneja el paso por referencia sin sintaxis en la llamada.

PL/I

En PL/I, a un procedimiento llamado se le puede pasar un descriptor que proporcione información sobre el argumento, como longitudes de cadenas y límites de matrices. Esto permite que el procedimiento sea más general y elimina la necesidad de que el programador pase dicha información. Por defecto, PL/I pasa argumentos por referencia. Una función (trivial) para cambiar el signo de cada elemento de una matriz bidimensional podría verse así:

change_sign: procedimiento (matriz); declarar matriz(*,*) flotante; matriz = -matriz;finalizar signo_cambio;

Esto podría llamarse con varias matrices de la siguiente manera:

/* la primera matriz limita de -5 a +10 y de 3 a 9 */declarar matriz1 (-5:10, 3:9) flotante;/* la segunda matriz limita del 1 al 16 y del 1 al 16 */declarar array2 (16,16) flotante;llamar a change_sign(array1);llamar a change_sign(array2);

Pitón

En Python , la palabra clave defdenota el inicio de la definición de una función. Las declaraciones del cuerpo de la función siguen con sangría en las líneas siguientes y terminan en la línea que tiene la misma sangría que la primera línea o el final del archivo. [30]

def  format_greeting ( nombre ):  devolver  "Bienvenido "  +  nombre def  greet_martin ():  imprimir ( format_greeting ( "Martin" ))

La primera función devuelve un texto de saludo que incluye el nombre pasado por la persona que llama. La segunda función llama a la primera y se llama como greet_martin()para escribir "Bienvenido Martin" en la consola.

Ver también

Referencias

  1. ^ "Glosario de terminología". nist.gov . NIST . Consultado el 9 de febrero de 2024 . Unidad invocable: (De un programa de software o diseño lógico) Función, método, operación, subrutina, procedimiento o unidad estructural análoga que aparece dentro de un módulo.
  2. ^ Donald E. Knuth (1997). El arte de la programación informática, volumen I: algoritmos fundamentales . Addison-Wesley. ISBN 0-201-89683-4.
  3. ^ O.-J. Dahl; EW Dijkstra; COCHE Hoare (1972). Programación estructurada . Prensa académica. ISBN 0-12-200550-3.
  4. ^ Subrata Dasgupta (7 de enero de 2014). Comenzó con Babbage: la génesis de la informática. Prensa de la Universidad de Oxford. págs. 155–. ISBN 978-0-19-930943-6.
  5. ^ ab Mauchly, JW (1982). "Preparación de Problemas para Máquinas Tipo EDVAC". En Randell, Brian (ed.). Los orígenes de las computadoras digitales . Saltador. págs. 393–397. doi :10.1007/978-3-642-61812-3_31. ISBN 978-3-642-61814-7.
  6. ^ Wheeler, DJ (1952). «El uso de subrutinas en los programas» (PDF) . Actas de la reunión nacional de la ACM de 1952 (Pittsburgh) en - ACM '52 . pag. 235.doi : 10.1145 /609784.609816 .
  7. ^ Wilkes, MV; Wheeler, DJ; Gill, S. (1951). Elaboración de Programas para una Computadora Electrónica Digital . Addison-Wesley.
  8. ^ Dainith, John (2004). ""subrutina abierta. "Diccionario de informática". Enciclopedia.com . Consultado el 14 de enero de 2013 .
  9. ^ Turing, Alan M. (1945), Informe del Dr. AM Turing sobre propuestas para el desarrollo de un motor informático automático (ACE): presentado al Comité Ejecutivo de la NPL en febrero de 1946reimpreso en Copeland, BJ , ed. (2005). Motor de computación automática de Alan Turing . Oxford: Prensa de la Universidad de Oxford. pag. 383.ISBN _ 0-19-856593-3.
  10. ^ Turing, Alan Mathison (19 de marzo de 1946) [1945], Propuestas para el desarrollo en la División de Matemáticas de un motor de computación automática (ACE)(NB. Presentado el 19-03-1946 ante el Comité Ejecutivo del Laboratorio Nacional de Física (Gran Bretaña).)
  11. ^ Carpintero, Brian Edward ; Doran, Robert William (1 de enero de 1977) [octubre de 1975]. "La otra máquina de Turing". La revista informática . 20 (3): 269–279. doi : 10.1093/comjnl/20.3.269 .(11 páginas)
  12. ^ ab Isaacson, Walter (18 de septiembre de 2014). "Walter Isaacson sobre las mujeres de ENIAC". Fortuna . Archivado desde el original el 12 de diciembre de 2018 . Consultado el 14 de diciembre de 2018 .
  13. ^ Herman H. Goldstine; John von Neumann (1947). "Parte II, Volumen I-3, Planificación y codificación de problemas para un instrumento informático electrónico" (PDF) . Informe sobre los aspectos matemáticos y lógicos de un instrumento informático electrónico (Informe técnico).(consulte la página 163 del pdf para ver la página correspondiente)
  14. ^ Las características operativas de los procesadores de Burroughs B5000 (PDF) . Revisión A. Burroughs Corporation . 1963. 5000-21005 . Consultado el 8 de febrero de 2024 .
  15. ^ "Instrucciones para empujar hacia abajo" (PDF) . Procesador de datos programado 6 - Manual (PDF) . pag. 37 . Consultado el 8 de febrero de 2024 .
  16. ^ Guy Lewis Steele Jr. AI Memo 443. 'Desmentir el mito de la "llamada a procedimiento costoso"; o, Implementaciones de llamadas a procedimientos consideradas perjudiciales”. Apartado “C. Por qué las llamadas a procedimientos tienen mala reputación ".
  17. ^ Frank, Thomas S. (1983). Introducción al PDP-11 y su lenguaje ensamblador. Serie de software de Prentice-Hall. Prentice Hall. pag. 195.ISBN _ 9780134917047. Consultado el 6 de julio de 2016 . Podríamos proporcionarle a nuestro ensamblador copias del código fuente de todas nuestras subrutinas útiles y luego, cuando le presentemos un programa de línea principal para ensamblar, decirle qué subrutinas se llamarán en la línea principal.
  18. ^ Buttlar, Dick; Farrell, Jacqueline; Nichols, Bradford (1996). Programación PThreads: un estándar POSIX para un mejor multiprocesamiento. "O'Reilly Media, Inc.". págs. 2–5. ISBN 978-1-4493-6475-5. OCLC  1036778036.
  19. ^ "Centro de información ARM". Infocenter.arm.com . Consultado el 29 de septiembre de 2013 .
  20. ^ "Uso de pila x64". Documentos de Microsoft . Microsoft . Consultado el 5 de agosto de 2019 .
  21. ^ "Tipos de funciones". msdn.microsoft.com . Consultado el 29 de septiembre de 2013 .
  22. ^ Verhoeff, Tom (2018). "Una clase magistral sobre recursividad". En Böckenhauer, Hans-Joachim; Komm, Dennis; Unger, Walter (eds.). Aventuras entre límites inferiores y altitudes superiores: ensayos dedicados a Juraj Hromkovič con motivo de su 60 cumpleaños . Saltador. pag. 616.ISBN _ 978-3-319-98355-4. OCLC  1050567095.
  23. ^ "Funciones integradas". ibm.com . Consultado el 25 de diciembre de 2023 .
  24. ^ Material de estudio Python. Abril de 2023. p. 87 . Consultado el 25 de diciembre de 2023 .
  25. ^ "Pequeño básico". Pequeño Básico . Consultado el 8 de febrero de 2024 .
  26. ^ "Pequeña guía básica de introducción: Capítulo 9: Subrutinas". Microsoft.
  27. ^ "Procedimientos en Visual Basic". Microsoft aprende . Consultado el 8 de febrero de 2024 .
  28. ^ "Declaración tenue (Visual Basic)". Microsoft aprende . Consultado el 8 de febrero de 2024 .
  29. ^ "qué se entiende por función gratuita".
  30. ^ "4. Más herramientas de flujo de control: documentación de Python 3.9.7".