En programación informática , una función (también procedimiento , método , subrutina , rutina o subprograma ) es una unidad invocable [1] de lógica de software que tiene una interfaz y un comportamiento bien definidos y puede invocarse varias veces.
Las unidades invocables proporcionan una herramienta de programación poderosa. [2] El propósito principal es permitir la descomposición de un problema grande y/o complicado en fragmentos que tengan una carga cognitiva relativamente baja y asignarles nombres significativos (a menos que sean anónimos). Una aplicación juiciosa puede reducir el costo de desarrollo y mantenimiento del 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 de máquina, pero son diferentes tipos de unidades invocables, con diferentes implicaciones y características.
El significado de cada término invocable (función, procedimiento, método, etc.) es, de hecho, diferente. No son sinónimos . Sin embargo, cada uno de ellos añade una capacidad a la programación que tiene puntos en común.
El término utilizado suele reflejar el contexto en el que se utiliza, generalmente en función del lenguaje que se utilice. Por ejemplo:
Sub
, abreviatura de subrutina o subprocedimiento , es el nombre de un objeto invocable que no devuelve un valor, mientras que a Function
sí devuelve un valor.La idea de una unidad invocable fue concebida inicialmente por John Mauchly y Kathleen Antonelli durante su trabajo en ENIAC y registrada en un simposio de Harvard de enero de 1947 sobre "Preparación de problemas para máquinas de tipo EDVAC ". [4] A Maurice Wilkes , David Wheeler y Stanley Gill se les atribuye generalmente la invención formal de este concepto, al que denominaron subrutina cerrada , [5] [6] en contraste con una subrutina abierta o macro . [7] 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 tan lejos como para inventar el concepto de una pila de direcciones de retorno . [8]
La idea de una subrutina se elaboró después de que las máquinas de computación ya existían durante 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 con el paso de los años. Las primeras computadoras y microprocesadores, como el Manchester Baby y el RCA 1802 , no tenían una sola instrucción de llamada de 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 un medio para llamar y regresar de subrutinas. [9] [10]
En enero de 1947, John Mauchly presentó notas generales en un "Simposio sobre máquinas de cálculo digital a gran escala" bajo el patrocinio conjunto de la Universidad de Harvard y la Oficina de Artillería de la Armada de los Estados Unidos. En este artículo, analiza el funcionamiento 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 se dispone de 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 se puedan utilizar 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 la 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 se almacenarán entonces en la máquina, y todo lo que uno necesita hacer es hacer una breve referencia a ellas por número, tal como se indican en la codificación. [4]
Kay McNulty había trabajado en estrecha colaboración con John Mauchly en el equipo ENIAC y desarrolló una idea para subrutinas para la computadora ENIAC que estaba programando durante la Segunda Guerra Mundial. [11] Ella y los otros programadores de ENIAC usaron las subrutinas para ayudar a calcular las trayectorias de los misiles. [11]
Goldstine y von Neumann escribieron un artículo fechado el 16 de agosto de 1948 en el que analizaban el uso de subrutinas. [12]
Algunas computadoras y microprocesadores muy antiguos, como el IBM 1620 , el 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 admite solo unos pocos niveles de anidación de subrutinas, pero puede admitir subrutinas recursivas. Las máquinas anteriores a mediados de la década de 1960, como el UNIVAC I , el PDP-1 y el IBM 1130 , generalmente usan 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 anidación 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 usar para admitir anidación de subrutinas arbitrariamente profundas y subrutinas recursivas. La Burroughs B5000 [13] (1961) es una de las primeras computadoras en almacenar datos de retorno de subrutinas en una pila.
La DEC PDP-6 [14] (1964) es una de las primeras máquinas basadas en acumuladores que tiene una instrucción de llamada a 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 tanto la anidación de subrutinas de profundidad arbitraria como las subrutinas recursivas. [15]
En los primeros ensambladores, el soporte de subrutinas era limitado. Las subrutinas no estaban explícitamente separadas entre sí o del programa principal y, de hecho, el código fuente de una subrutina podía intercalarse con el de otros subprogramas. Algunos ensambladores ofrecían macros predefinidas para generar las secuencias de llamada y retorno. En la década de 1960, los ensambladores solían tener un soporte mucho más sofisticado para subrutinas en línea y ensambladas por separado que podían vincularse entre sí.
Uno de los primeros lenguajes de programación que admitía 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 tempranos también admitían programación procedimental.
Incluso con este enfoque engorroso, las subrutinas resultaron muy útiles, ya que permitieron utilizar el mismo código en muchos programas diferentes. La memoria era un recurso muy escaso en las primeras computadoras, y las subrutinas permitieron un ahorro significativo en el tamaño de los programas.
Muchos de los primeros ordenadores cargaban las instrucciones del programa en la memoria desde una cinta de papel perforada . Cada subrutina podía entonces ser proporcionada por un trozo de cinta independiente, cargada o empalmada antes o después del programa principal (o "línea principal" [16] ); y la misma cinta de subrutina podía entonces ser utilizada por muchos programas diferentes. Un enfoque similar se utilizó en los ordenadores que cargaban las instrucciones del programa desde tarjetas perforadas . El nombre biblioteca de subrutinas originalmente significaba una biblioteca, en el sentido literal, que mantenía colecciones indexadas de cintas o mazos de tarjetas para uso colectivo.
Para eliminar la necesidad de un código automodificable , los diseñadores de computadoras eventualmente 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 de 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 la llama almacenaría la dirección de retorno en una variable de modo que cuando la función se completara, ejecutaría un salto indirecto que dirigiría la ejecución a la ubicación dada por la variable predefinida.
Otro avance fue la instrucción de salto a subrutina , que combinaba el guardado de la dirección de retorno con el salto de llamada, minimizando así significativamente la sobrecarga .
En el IBM System/360 , por ejemplo, las instrucciones de bifurcación BAL o BALR, diseñadas para la llamada a procedimientos, guardaban la dirección de retorno en un registro del procesador especificado en la instrucción, por convención el registro 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 necesitaba ese registro para algún otro propósito (como llamar a otra subrutina), guardaba 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 de retorno se almacenaría en la ubicación de memoria que era el destino de la bifurcación. La ejecución del procedimiento comenzaría en realidad en la siguiente ubicación de memoria. En el lenguaje ensamblador del HP 2100, se escribiría, por ejemplo:
...JSB MYSUB (Llama a la subrutina MYSUB).BB... (Volveré aquí después de que MYSUB esté terminado).
para llamar a una subrutina llamada MYSUB desde el programa principal. La subrutina se codificaría como
MYSUB NOP (Almacenamiento para la dirección de devolución de MYSUB).AA... (Inicio del cuerpo de MYSUB.)...JMP MYSUB,I (Regresa al programa de llamada.)
La instrucción JSB colocó la dirección de la instrucción NEXT (es decir, BB) en la ubicación especificada como su operando (es decir, MYSUB), y luego se ramificó a la ubicación NEXT posterior a esa (es decir, AA = MYSUB + 1). La subrutina podría entonces regresar al programa principal ejecutando el salto indirecto JMP MYSUB, I que se ramificó a la ubicación almacenada en la ubicación MYSUB.
Los compiladores de Fortran y otros lenguajes podían utilizar fácilmente estas instrucciones cuando estaban disponibles. Este enfoque admitía múltiples niveles de llamadas; sin embargo, dado que la dirección de retorno, los parámetros y los valores de retorno de una subrutina tenían asignadas ubicaciones de memoria fijas, no permitía llamadas recursivas.
Por cierto, Lotus 1-2-3 utilizó un método similar a principios de los años 1980 para descubrir las dependencias de recálculo en una hoja de cálculo. Es decir, se reservaba una ubicación en cada celda para almacenar la dirección de retorno . Dado que no se permiten referencias circulares para el orden de recálculo natural, esto permite un recorrido por el árbol sin reservar espacio para una pila en la memoria, que era muy limitado en computadoras pequeñas como la IBM PC .
La mayoría de las implementaciones modernas de una llamada a una función utilizan una pila de llamadas , un caso especial de la estructura de datos de pila , para implementar llamadas a funciones y retornos. Cada llamada a un procedimiento crea una nueva entrada, llamada marco de pila , en la parte superior de la pila; cuando el procedimiento retorna, su marco de pila se elimina de la pila y su espacio se puede utilizar para otras llamadas a procedimientos. Cada marco de pila contiene los datos privados de la llamada correspondiente, que normalmente incluyen los parámetros y las 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 que todavía se utiliza en arquitecturas de computación de conjunto de instrucciones reducido (RISC) y de palabras de instrucción muy largas (VLIW)), pero muchas máquinas tradicionales diseñadas desde fines de la década de 1960 han incluido instrucciones especiales para ese propósito.
La pila de llamadas se suele implementar 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 esto último. [ cita requerida ]
Algunos diseños, en particular algunas implementaciones de Forth , utilizaban dos pilas independientes, una principalmente para la información de control (como direcciones de retorno y contadores de bucles) y la otra para los datos. La primera era, o funcionaba como, una pila de llamadas y solo era accesible indirectamente para el programador a través de otras construcciones del lenguaje, mientras que la segunda era accesible de forma más directa.
Cuando se introdujeron por primera vez las llamadas a procedimientos basadas en pila, una motivación importante fue ahorrar memoria valiosa. [ cita requerida ] 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 activas actualmente (es decir, que se han llamado pero aún no han regresado). Debido a las formas en que los programas generalmente se ensamblaban a partir de bibliotecas, no era (y todavía es) raro encontrar programas que incluyen miles de funciones, de las cuales solo un puñado están activas en un momento dado. [ cita requerida ] 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 simple 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 multiproceso , generalmente hay más de una pila. [17] Un entorno que admite totalmente corrutinas o evaluación diferida puede usar estructuras de datos distintas de las pilas para almacenar sus registros de activación.
Una desventaja del mecanismo de pila de llamadas es el aumento del costo de una llamada a un procedimiento y su correspondiente retorno. [ Aclaración necesaria ] El costo adicional incluye el incremento y decremento del puntero de pila (y, en algunas arquitecturas, la verificación de desbordamiento de pila ) y el acceso a las variables y parámetros locales mediante direcciones relativas al marco, en lugar de direcciones absolutas. El costo puede traducirse 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 de hoja o funciones de hoja , que regresan sin hacer ninguna llamada de procedimiento por sí mismos. [18] [19] [20] Para reducir esa sobrecarga, muchos compiladores modernos intentan retrasar el uso de una pila de llamadas hasta que realmente sea necesaria. [ cita requerida ] 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 hacer ninguna otra llamada, la pila de llamadas no se usa en absoluto. Si P necesita llamar a otro procedimiento Q , entonces usará la pila de llamadas para guardar el contenido de cualquier registro (como la dirección de retorno) que se necesitará después de que Q regrese.
En general, una unidad invocable es una lista de instrucciones que, a partir de la primera instrucción, se ejecutan de forma secuencial, 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.
Las características de las implementaciones de unidades invocables han evolucionado con el tiempo y varían según el contexto. En esta sección se describen las características de las distintas implementaciones comunes.
La mayoría de los lenguajes de programación modernos proporcionan características para definir y llamar funciones, incluida la sintaxis para acceder a dichas funciones, entre ellas:
Algunos lenguajes, como Pascal , Fortran , Ada y muchos dialectos de BASIC , utilizan un nombre diferente para una unidad invocable que devuelve un valor ( función o subprograma ) y para una que no lo hace ( subrutina o procedimiento ). Otros lenguajes, como C , C++ , C# y Lisp , utilizan un solo nombre para una unidad invocable, función . Los lenguajes de la familia C utilizan la palabra clave void
para indicar que no hay valor de retorno.
Si se declara que devuelve un valor, se puede incorporar una llamada en una expresión para consumir el valor devuelto. Por ejemplo, una unidad invocable de raíz cuadrada podría llamarse como y = sqrt(x)
.
Una unidad invocable que no devuelve un valor se denomina 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")
.
La mayoría de las implementaciones, especialmente en lenguajes modernos, admiten parámetros que el invocable declara como parámetros formales . El invocador pasa parámetros reales , también conocidos como argumentos , para que coincidan. Los distintos lenguajes de programación proporcionan diferentes convenciones para pasar argumentos.
En algunos lenguajes, como BASIC, un objeto invocable tiene una sintaxis diferente (es decir, una palabra clave) para un objeto invocable que devuelve un valor y para uno que no lo devuelve. En otros lenguajes, la sintaxis es la misma independientemente de ello. En algunos de estos lenguajes, se utiliza una palabra clave adicional para declarar que no hay valor de retorno; por ejemplo, void
en 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 objeto invocable en particular puede devolver con o sin un valor según el flujo de control.
En muchos contextos, un objeto 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.
Robert C. Martin , conocido por promover los principios de diseño, considera que los efectos secundarios son indeseables . Martin sostiene que los efectos secundarios pueden dar lugar a acoplamientos temporales o dependencias de orden. [21]
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. Dichos lenguajes normalmente 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 efecto secundario.
La mayoría de los contextos admiten variables locales ( memoria propiedad de un objeto invocable para almacenar valores intermedios). Estas variables se almacenan normalmente en el registro de activación de la llamada en la pila de llamadas junto con otra información, como la dirección de retorno .
Si el lenguaje lo permite, un objeto invocable puede llamarse a sí mismo, lo que hace que su ejecución se suspenda mientras se ejecuta otra ejecución anidada del mismo objeto invocable. La recursión es un medio útil para simplificar algunos algoritmos complejos y descomponer problemas complejos. Los lenguajes recursivos proporcionan una nueva copia de las variables locales en cada llamada. Si el programador desea que el objeto invocable recursivo utilice las mismas variables en lugar de utilizar variables locales, normalmente 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 los 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 llamadas suspendidas.
La recursión permite la implementación directa de funciones definidas por inducción matemática y algoritmos recursivos de división y conquista . A continuación, se muestra un ejemplo de una función recursiva en C/C++ para encontrar números de Fibonacci :
int Fib ( int n ) { si ( n <= 1 ) { devolver n ; } devolver Fib ( n - 1 ) + Fib ( n - 2 ); }
Los primeros lenguajes como Fortran no admitían inicialmente la recursión porque solo se asignaba un conjunto de variables y direcciones de retorno para cada invocable. [22] Los primeros conjuntos de instrucciones de computadora dificultaban el almacenamiento de direcciones de retorno y variables en una pila. Las máquinas con registros de índice o registros de propósito general , por ejemplo, la serie CDC 6000 , PDP-6 , GE 635 , System/360 , la serie UNIVAC 1100 , podían usar uno de esos registros como puntero de pila .
Algunos lenguajes, por ejemplo, Ada , Pascal , PL/I , Python , admiten la declaración y definición de una función dentro de, por ejemplo, un cuerpo de función, de modo que el nombre de la función interna solo sea visible dentro del cuerpo de la función externa.
Si un objeto invocable se puede ejecutar correctamente incluso cuando ya está en curso otra ejecución del mismo objeto invocable, se dice que ese objeto invocable es reentrante . Un objeto invocable reentrante también es útil en situaciones de subprocesos múltiples , ya que varios subprocesos pueden llamar al mismo objeto invocable sin temor a interferir entre sí. En el sistema de procesamiento de transacciones IBM CICS , el requisito cuasi-reentrante era un poco menos restrictivo, pero similar, para los programas de aplicación que compartían muchos subprocesos.
Algunos lenguajes admiten la sobrecarga : permiten múltiples funciones 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 la entrada de números reales, números complejos y matrices. El algoritmo para cada tipo de entrada es diferente y el valor de retorno puede tener un tipo diferente. Al escribir tres funciones invocables separadas con el mismo nombre, es decir, sqrt , el código resultante puede ser más fácil de escribir y mantener ya que cada una 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 lenguajes que admiten tipado fuerte . A menudo, el compilador selecciona la sobrecarga que se va a llamar en función del tipo de los argumentos de entrada o falla si los argumentos de entrada no seleccionan una sobrecarga. Los lenguajes más antiguos y de tipado débil generalmente no admiten la sobrecarga.
A continuación se muestra un ejemplo de sobrecarga en C++ , dos funciones Area
que aceptan tipos diferentes:
// devuelve el área de un rectángulo definido por la altura y el 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 () { doble área_rectángulo = Área ( 3 , 4 ); doble área_círculo = Área ( 5 ); }
PL/I tiene el GENERIC
atributo de definir un nombre genérico para un conjunto de referencias de entrada llamadas con diferentes tipos de argumentos. Ejemplo:
DECLARAR gen_name GENÉRICO( nombre CUANDO(BINARIO FIJO), llama CUANDO(FLOTANTE), ruta DE LO CONTRARIO);
Se pueden especificar varias definiciones de argumentos para cada entrada. Una llamada a "gen_name" dará como resultado una llamada a "name" cuando el argumento sea BINARIO FIJO, "flame" cuando sea FLOAT, etc. Si el argumento no coincide con ninguna de las opciones, se llamará a "pathname".
Un cierre es un objeto invocable más los valores 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 un mecanismo para los efectos secundarios.
Además de su comportamiento de ruta feliz , un objeto invocable puede necesitar informar al llamador 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 hace estallar la pila de llamadas hasta que se encuentra un controlador de excepciones para manejar la condición.
Los lenguajes que no admiten excepciones pueden usar el valor de retorno para indicar el éxito o el fracaso de una llamada. Otro enfoque es usar una ubicación conocida como una variable global para indicar el éxito. Un objeto invocable escribe el valor y el invocador lo lee después de una llamada.
En IBM System/360 , donde se esperaba el código de retorno de una subrutina, el valor de retorno se diseñaba a menudo para que fuera un múltiplo de 4, de modo que pudiera usarse como un índice de tabla de ramificación directa en una tabla de ramificación que a menudo se ubicaba inmediatamente después de la instrucción de llamada 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, almacenando la dirección de retorno en R14 B TABLE(15) usa el valor devuelto en el registro 15 para indexar la tabla de rama,*ramificación a la rama apropiada instr.TABLA B OK código de retorno =00 BUENO } B Código de retorno incorrecto = 04 Entrada no válida } Tabla de ramas B ERROR código de retorno = 08 Condición inesperada }
Una llamada tiene una sobrecarga de tiempo de ejecución , que puede incluir, entre otras cosas:
Se emplean varias técnicas para minimizar el coste de ejecución de las llamadas.
Algunas optimizaciones para minimizar la sobrecarga de llamadas pueden parecer sencillas, pero no se pueden usar si el invocable tiene efectos secundarios. Por ejemplo, en la expresión (f(x)-1)/(f(x)+1)
, la función f
no se puede llamar solo una vez con su valor usado dos veces ya que las dos llamadas pueden devolver resultados diferentes. Además, en los pocos lenguajes que definen el orden de evaluación de los operandos del operador de división, el valor de x
debe recuperarse nuevamente 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.
La inserción en línea elimina las llamadas a elementos invocables específicos. El compilador reemplaza cada llamada con el código compilado del elemento invocable. Esto no solo evita la sobrecarga de llamadas, sino que también permite al compilador optimizar el código del elemento que realiza la llamada de manera más efectiva al tener en cuenta el contexto y los argumentos en esa llamada. Sin embargo, la inserción en línea 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.
Los objetos invocables se pueden definir dentro de un programa o por separado en una biblioteca que puede ser utilizada por múltiples programas.
Un compilador traduce las instrucciones de llamada y retorno en instrucciones de máquina de acuerdo con una convención de llamada bien definida . En el caso del 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 instrucciones de llamada y retorno se denominan prólogo y epílogo del procedimiento .
Una función incorporada , o función intrínseca , es una función para la cual el compilador genera código en tiempo de compilación o lo proporciona de una manera diferente a la de otras funciones. [23] Una función incorporada no necesita definirse como otras funciones ya que está incorporada al lenguaje de programación. [24]
Las ventajas de dividir un programa en funciones incluyen:
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 requerida ]
Una función normalmente requiere un código de mantenimiento estándar , tanto a la entrada como a la salida de la función ( prólogo y epílogo de la función , que normalmente guardan registros de propósito general y la dirección de retorno como mínimo).
Se han desarrollado muchas convenciones de programación con respecto a los objetos invocables.
Con respecto a la denominación, muchos desarrolladores nombran un objeto 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 objeto invocable debe realizar exactamente una tarea y, si realiza más de una, debe dividirse en varios objetos invocables. Argumentan que los objetos invocables son componentes clave en el mantenimiento de software y que sus funciones en el programa deben seguir siendo distintas.
Los defensores de la programación modular sostienen que cada objeto invocable debe tener una dependencia mínima del resto del código base . Por ejemplo, el uso de variables globales generalmente se considera poco sensato, porque agrega acoplamiento entre todos los objetos invocables que usan las variables globales. Si dicho acoplamiento no es necesario, recomiendan refactorizar los objetos invocables para que acepten los parámetros pasados .
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 que se puede llamar, no hay ningún mecanismo para pasar argumentos o devolver un valor y todas las variables son globales. Proporciona el comando GOSUB
donde sub es la abreviatura de sub procedimiento , 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 UN PROGRAMA BÁSICO 20 GOSUB 100 30 GOTO 20 100 INPUT “ DAME UN NÚMERO ” ; N 110 PRINT “ LA RAÍZ CUADRADA DE ” ; N ; 120 PRINT “ ES ” ; SQRT ( N ) 130 RETURN
Este código solicita repetidamente al usuario que ingrese un número e informa la raíz cuadrada del valor. Las líneas 100 a 130 son las que se pueden llamar.
En Microsoft Small Basic , destinado a los estudiantes que están aprendiendo a programar en un lenguaje basado en texto, una unidad invocable se denomina subrutina . La Sub
palabra clave indica el inicio de una subrutina y va seguida de un identificador de nombre. Las líneas subsiguientes son el cuerpo que termina con la EndSub
palabra clave. [25]
Sub SayHello TextWindow . WriteLine ( "¡Hola!" ) EndSub
Esto se puede llamar como SayHello()
. [26]
En versiones posteriores de Visual Basic (VB), incluida la última línea de productos y VB6 , se utiliza el término procedimiento para el concepto de unidad invocable. La palabra clave Sub
se utiliza para no devolver ningún valor y Function
para devolver un valor. Cuando se utiliza 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 Object
para versiones posteriores basadas en .NET y variantes para VB6 . [28]
VB admite convenciones de paso de parámetros por valor y por referencia a través de las palabras clave ByVal
y ByRef
, respectivamente. A menos que ByRef
se especifique , se pasa un argumento ByVal
. Por lo tanto, ByVal
rara vez se especifica explícitamente.
Para un tipo simple como un número, estas convenciones son relativamente claras. El paso ByRef
permite que el procedimiento modifique la variable pasada, mientras que el paso ByVal
no lo hace. Para un objeto, la semántica puede confundir a los programadores, ya que un objeto siempre se trata como una referencia. Al pasar un objeto ByVal
se copia 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í Fin Sub
El no devuelve un valor y debe llamarse de forma independiente, comoDoSomething
Función GiveMeFive () como entero GiveMeFive = 5 Fin de función
Esto devuelve el valor 5 y una llamada puede ser parte de una expresión comoy = x + GiveMeFive()
Sub AddTwo ( PorRef intValue como Entero ) intValue = intValue + 2 Fin Sub
Esto tiene un efecto secundario: modifica la variable pasada por referencia y podría llamarse para una variable v
como AddTwo(v)
. Si v es 5 antes de la llamada, será 7 después.
En C y C++ , una unidad invocable se denomina función . La definición de una función comienza con el nombre del tipo de valor que devuelve o void
para indicar que no devuelve un valor. A continuación, se incluyen 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 miembro o método . Una función fuera de una clase se puede denominar función libre para distinguirla de una función miembro. [29]
void doSomething () { /* algún código */ }
Esta función no devuelve un valor y siempre se llama de forma independiente, comodoSomething()
int damecinco () { devolver 5 ; }
Esta función devuelve el valor entero 5. La llamada puede ser independiente o en una expresión comoy = x + giveMeFive()
vacío addTwo ( int * pi ) { * pi += 2 ; }
Esta función tiene un efecto secundario: modifica el valor pasado por la dirección al valor de entrada más 2. Se puede llamar para una variable v
como addTwo(&v)
donde el ampersand (&) le indica al compilador que pase la dirección de una variable. Si v es 5 antes de la llamada, será 7 después.
vacío 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 ampersand, ya que el compilador maneja el paso por referencia sin sintaxis en la llamada.
En PL/I, a un procedimiento llamado se le puede pasar un descriptor que proporcione información sobre el argumento, como longitudes de cadena y límites de matriz. Esto permite que el procedimiento sea más general y elimina la necesidad de que el programador pase dicha información. De manera predeterminada, 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í:
cambio_signo: procedimiento(matriz); declarar array(*,*) float; matriz = -matriz;fin cambio_signo;
Esto se podría llamar con varias matrices de la siguiente manera:
/* primeros límites de la matriz de -5 a +10 y de 3 a 9 */declarar array1 (-5:10, 3:9)float;/* límites de la segunda matriz de 1 a 16 y de 1 a 16 */declarar array2 (16,16) float;llamar a change_sign(array1);llamar a change_sign(array2);
En Python , la palabra clave def
denota el comienzo de la definición de una función. Las instrucciones del cuerpo de la función siguen sangradas en las líneas subsiguientes 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 ): return "Bienvenido" + nombre def greeting_martin (): print ( format_greeting ( "Martin" ))
La primera función devuelve un texto de saludo que incluye el nombre que pasó el autor de la llamada. La segunda función llama a la primera y se la llama como si fuera greet_martin()
para escribir "Bienvenido Martin" en la consola.
En la interpretación procedimental de programas lógicos , las implicaciones lógicas se comportan como procedimientos de reducción de objetivos. Una regla (o cláusula ) de la forma:
A :- B
cuya lectura lógica es:
A if B
se comporta como un procedimiento que reduce los objetivos que se unifican con A
a subobjetivos que son instancias de B
.
Consideremos, por ejemplo, el programa Prolog:
madre_hijo ( elizabeth , charles ). padre_hijo ( charles , william ). padre_hijo ( charles , harry ). padre_hijo ( X , Y ) :- madre_hijo ( X , Y ). padre_hijo ( X , Y ) :- padre_hijo ( X , Y ).
Tenga en cuenta que la función de maternidad se representa mediante una relación, como en una base de datos relacional . Sin embargo, las relaciones en Prolog funcionan como unidades invocables.X = mother(Y)
Por ejemplo, la llamada a un procedimiento produce la salida . Pero el mismo procedimiento puede llamarse con otros patrones de entrada-salida. Por ejemplo:?- parent_child(X, charles)
X = elizabeth
?- padre_hijo ( elizabeth , Y ). Y = charles .?- padre_hijo ( X , Y ). X = elizabeth , Y = charles .X = charles , Y = harry .X = charles , Y = william .?- padre_hijo ( william , harry ). no .?- padre_hijo ( elizabeth , charles ). sí .
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.
Podríamos proporcionarle a nuestro empleado de ensamblaje copias del código fuente de todas nuestras subrutinas útiles y luego, cuando le presentemos un programa principal para ensamblaje, decirle qué subrutinas se llamarán en el programa principal [...]