Articulo de referencia

Función (programación informática)

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 ...

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 ser invocada varias veces.

Las unidades invocables constituyen una potente herramienta de programación. [ 2 ] Su propósito principal es permitir la descomposición de un problema grande o complejo en fragmentos con una carga cognitiva relativamente baja y asignarles nombres significativos (a menos que sean anónimos). Su aplicación juiciosa puede reducir el coste de desarrollo y mantenimiento del software, al tiempo que aumenta su calidad y fiabilidad. [ 3 ]

Las unidades invocables están presentes en múltiples niveles de abstracción en el entorno de programación. [ 4 ] Por ejemplo, un programador puede escribir una función en el código fuente que se compila a código máquina e 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 tipos diferentes de unidades invocables , con distintas implicaciones y características.

Terminología

Algunos lenguajes de programación , como COBOL y BASIC , distinguen entre funciones que devuelven un valor (generalmente llamadas "funciones") y aquellas que no lo hacen (generalmente llamadas "subprograma", "subrutina" o "procedimiento"); otros, como C , C++ y Rust , solo usan el término "función" independientemente de si devuelven un valor o no; otros, como ALGOL 60 y PL/I , solo usan la palabra " procedimiento" . Algunos lenguajes orientados a objetos , como Java y C# , se refieren a las funciones dentro de las clases como " métodos ".

Historia

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 tipo EDVAC ". [ 5 ] Generalmente se atribuye a Maurice Wilkes , David Wheeler y Stanley Gill 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 pila de direcciones de retorno . [ 9 ]

La idea de una subrutina surgió cuando las máquinas de computación ya llevaban tiempo existiendo. Las instrucciones de salto aritmético y condicional se planificaron con antelació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, como la Manchester Baby , y algunos microprocesadores primitivos, como 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 utilizaran la secuencia de llamadas —una serie de instrucciones— en cada punto de llamada .

En 1945, se implementó una única subrutina en el Z4 de Konrad Zuse en forma de cinta. [ 10 ]

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

En enero de 1947, John Mauchly presentó notas generales en un simposio sobre maquinaria de cálculo digital a gran escala, patrocinado conjuntamente por la Universidad de Harvard y la Oficina de Artillería de la Armada de los Estados Unidos. En ellas, analiza el funcionamiento en serie y en paralelo, sugiriendo lo siguiente:

...la estructura de la máquina no tiene por qué ser complicada en absoluto. Dado que se dispone de todas las características lógicas esenciales para este procedimiento, es posible desarrollar una instrucción de codificación para ubicar las subrutinas en la memoria en lugares conocidos por la máquina, de forma que puedan ser fácilmente llamadas a su uso.

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

Kay McNulty había trabajado estrechamente 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. [ 13 ] Ella y los demás programadores de ENIAC utilizaron las subrutinas para ayudar a calcular las trayectorias de los misiles. [ 13 ]

Goldstine y von Neumann escribieron un artículo fechado el 16 de agosto de 1948 en el que analizaban el uso de subrutinas. [ 14 ]

Algunos de los primeros ordenadores y microprocesadores, como el IBM 1620 , el Intel 4004 y el Intel 8008 , y los microcontroladores PIC , contaban con una llamada a subrutina de una sola instrucción que utilizaba una pila de hardware dedicada para almacenar las direcciones de retorno. Este hardware solo admitía unos pocos niveles de anidamiento de subrutinas, pero sí podía 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 , solían utilizar una convención de llamada que guardaba el contador de instrucciones en la primera ubicación de memoria de la subrutina llamada. Esto permitía niveles de anidamiento de subrutinas arbitrariamente profundos, pero no admitía subrutinas recursivas. El IBM System/360 tenía una instrucción de llamada a subrutina que colocaba el valor del contador de instrucciones guardado en un registro de propósito general; con código adicional, esto podía utilizarse para admitir anidamiento de subrutinas arbitrariamente profundo y subrutinas recursivas. El Burroughs B5000 [ 15 ] (1961) es uno de los primeros ordenadores en almacenar datos de retorno de subrutinas en una pila.

El DEC PDP-6 [ 16 ] (1964) es una de las primeras máquinas basadas en acumulador que tenía una instrucción de llamada a subrutina que guardaba 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 el ejemplo; esta característica también admite tanto anidamiento de subrutinas de profundidad arbitraria como subrutinas recursivas. [ 17 ]

Soporte de idiomas

En los primeros ensambladores, la compatibilidad con subrutinas era limitada. Las subrutinas no estaban separadas explícitamente entre sí ni del programa principal, e incluso 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 ofrecer una compatibilidad mucho más sofisticada tanto para subrutinas en línea como para subrutinas ensambladas por separado que podían enlazarse entre sí.

Uno de los primeros lenguajes de programación en admitir 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 primitivos también admitían la programación procedimental.

Bibliotecas

Incluso con este método engorroso, las subrutinas demostraron ser muy útiles. Permitieron usar 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 proporcionarse mediante una cinta independiente, que se cargaba o insertaba antes o después del programa principal (o "línea principal" [ 18 ] ); y la misma cinta de subrutinas podía ser utilizada por muchos programas diferentes. Un enfoque similar se empleaba en los ordenadores que cargaban las instrucciones del programa desde tarjetas perforadas . El término " biblioteca de subrutinas" originalmente significaba, en sentido literal, una biblioteca que almacenaba colecciones indexadas de cintas o conjuntos de tarjetas para uso colectivo.

Regreso mediante salto indirecto

Para eliminar la necesidad de código automodificable , 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 esos ordenadores, en lugar de modificar el salto de retorno de la función, el programa que la llamaba almacenaba la dirección de retorno en una variable, de modo que cuando la función finalizaba, ejecutaba un salto indirecto que dirigía la ejecución a la ubicación indicada por la variable predefinida.

Saltar a la subrutina

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 la década de 1950 existían al menos cinco variantes de esta instrucción.

  • Salto de retorno: almacene la dirección de retorno en la primera palabra de la subrutina. El salto de retorno (RJ) en el UNIVAC 1103A [ 19 ] es un ejemplo temprano.
  • Salto de índice: almacene la dirección de retorno en un registro de índice. Transfer and Set indeX (TSX) en el IBM 704 [ 20 ] es un ejemplo temprano.
  • Almacenamiento automático en una ubicación de almacenamiento dedicada: la ubicación Store P (STP) en el RCA 501 [ 21 ] es un ejemplo temprano.
  • Almacenamiento automático en un registro designado: Los registros de historial de secuencia y cosecuencia (SH y CSH) [ 22 ] en el Honeywell 800 son un ejemplo temprano.
  • Corrección de la dirección de retorno: La instrucción "return address" suma uno al contenido del registro del contador de programa y corrige la parte de la dirección de una instrucción de salto en la última palabra de la subrutina con la dirección de retorno de la función que la llama. La instrucción "return address" va seguida inmediatamente de una instrucción de salto al inicio de la subrutina. LGP-30 [ 23 ] es un ejemplo.

La arquitectura del IBM System/360 es un ejemplo de cómo se guarda la dirección de retorno en un registro específico. Las instrucciones de salto 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 salto indirecto (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 su contenido en una ubicación de memoria privada o en una pila de registros .

La arquitectura HP 2100 es un ejemplo de cómo se guarda la dirección de retorno en la primera palabra de la subrutina. La instrucción JSB guarda la dirección de retorno en la ubicación de memoria que es el destino de la bifurcación. La ejecución del procedimiento comienza en la siguiente ubicación de memoria. En el lenguaje ensamblador de HP 2100, se escribiría, por ejemplo:

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

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 retorno de MYSUB). AA ... (Comienza el cuerpo de MYSUB.) ... JMP MYSUB,I (Regresa al programa que realizó la llamada.) 

La instrucción JSB colocaba la dirección de la instrucción NEXT (BB) en la ubicación especificada como su operando (MYSUB), y luego saltaba a la siguiente ubicación (AA = MYSUB + 1). La subrutina podía entonces regresar al programa principal ejecutando el salto indirecto JMP MYSUB, I, que saltaba a la ubicación almacenada en MYSUB. Los compiladores de Fortran y otros lenguajes podían utilizar fácilmente estas instrucciones cuando estuvieran 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 se asignaban a 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. Concretamente, se reservaba un espacio en cada celda para almacenar la dirección de retorno . Dado que las referencias circulares no están permitidas para el orden de recálculo natural, esto permite recorrer el árbol sin reservar espacio para una pila en la memoria, que era muy limitada en ordenadores pequeños como el IBM PC .

pila de llamadas

La mayoría de las implementaciones modernas de llamadas a funciones utilizan una pila de llamadas , un caso especial de la estructura de datos de pila , para gestionar las llamadas y los retornos de las funciones. Cada llamada a un procedimiento crea una nueva entrada, denominada marco de pila , en la parte superior de la pila; cuando el procedimiento finaliza, su marco de pila se elimina de la pila y su espacio puede utilizarse 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, así como la dirección de retorno.

La secuencia de llamadas puede implementarse mediante una secuencia de instrucciones ordinarias (un enfoque que todavía se utiliza en las arquitecturas de computación con conjunto de instrucciones reducido (RISC) y de palabra de instrucción muy larga (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 se suele implementar como un área contigua de memoria. Es una decisión de diseño arbitraria si la parte inferior de la pila es la dirección más baja o la más alta dentro de esta área, de modo que la pila puede crecer hacia adelante o hacia atrás en la memoria; sin embargo, muchas arquitecturas optan por esta última opción. [ 24 ]

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 otra para datos. La primera funcionaba como una pila de llamadas y el programador solo podía acceder a ella indirectamente mediante otras construcciones del lenguaje, mientras que la segunda era más directamente accesible.

Cuando se introdujeron por primera vez las llamadas a procedimientos basadas en pila, una motivación importante fue ahorrar memoria valiosa. [ 25 ] 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, que se han llamado pero aún no han regresado). Debido a la forma 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. Para tales programas, el mecanismo de pila de llamadas podía ahorrar cantidades significativas de memoria. De hecho, el mecanismo de pila de llamadas puede considerarse como el método más antiguo y simple para la administración automática de 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 multihilo , generalmente hay más de una pila. [ 26 ] Un entorno que admite completamente corrutinas o evaluación perezosa puede usar estructuras de datos distintas a las pilas para almacenar sus registros de activación.

Apilamiento retardado

Una desventaja del mecanismo de pila de llamadas es el mayor costo de una llamada a procedimiento y su correspondiente retorno. Este costo adicional incluye el incremento y decremento del puntero de pila (y, en algunas arquitecturas, la comprobación de desbordamiento de pila ), así como el acceso a las variables y parámetros locales mediante direcciones relativas al marco, en lugar de direcciones absolutas. Este costo puede traducirse en un mayor tiempo de ejecución, una mayor complejidad del procesador o ambas.

Esta sobrecarga es más obvia y objetable en los procedimientos hoja o funciones hoja , que regresan sin hacer ninguna llamada a procedimiento por sí mismas. [ 27 ] [ 28 ] Para reducir esa sobrecarga, muchos compiladores modernos intentan retrasar el uso de una pila de llamadas hasta que sea realmente necesaria. Por ejemplo, la llamada a 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 será necesario después de que Q regrese.

Características

En general, una unidad invocable es una lista de instrucciones que, a partir de la primera, se ejecuta secuencialmente, salvo que su lógica interna indique lo contrario. Puede ser invocada (llamada) varias veces durante la ejecución de un programa. La ejecución continúa con la siguiente instrucción tras la llamada, cuando esta devuelve el control.

Implementaciones

Las características de las implementaciones de unidades invocables han evolucionado 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 características para definir y llamar a funciones, incluyendo sintaxis para acceder a dichas características, tales como:

  • Delimitar la implementación de una función del resto del programa.
  • Asignar un identificador , nombre, a una función
  • Defina parámetros formales con un nombre y un tipo de datos para cada uno.
  • Asigne un tipo de dato al valor devuelto, si corresponde.
  • Especifique un valor de retorno en el cuerpo de la función.
  • Llamar a una función
  • Proporcione parámetros reales que correspondan a los parámetros formales de la función llamada.
  • Devuelva el control a la persona que realizó la llamada en el momento de la misma.
  • Consume el valor de retorno en la función que realiza la llamada.
  • Desechar los valores devueltos por una llamada
  • Proporcione un ámbito de nombres privado para las variables.
  • Identificar las variables externas a la función que sean accesibles dentro de ella.
  • Propagar una condición excepcional fuera de una función y manejarla en el contexto de llamada.
  • Empaquetar funciones en un contenedor como un módulo , una biblioteca , un objeto o una clase.

Nomenclatura

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 ) en comparación con una que no lo hace ( subrutina o procedimiento ). Otros lenguajes, como C , C++ , C# y Lisp , utilizan un único nombre para una unidad invocable: función . Los lenguajes de la familia C utilizan 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 invocable de raíz cuadrada podría llamarse como y = sqrt(x).

Una unidad invocable que no devuelve un valor se llama como una instrucción independiente, como print("hello"). Esta sintaxis también se puede usar para una unidad invocable que sí devuelve un valor, pero este valor de retorno se ignorará.

Algunos lenguajes más antiguos requieren una palabra clave para las 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 la función declara como parámetros formales . Quien realiza la llamada pasa parámetros reales , también llamados argumentos , para que coincidan. Los distintos lenguajes de programación ofrecen diferentes convenciones para pasar argumentos.

Valor de retorno

En algunos lenguajes, como BASIC, una función invocable tiene una sintaxis (palabra clave) diferente para cuando devuelve un valor y cuando no. En otros lenguajes, la sintaxis es la misma en ambos casos. En algunos de estos lenguajes, se utiliza una palabra clave adicional para indicar que no hay valor de retorno, como voiden C, C++ y C#. En otros lenguajes, como Python, la diferencia radica en si el cuerpo contiene una instrucción `return` con un valor, y una función invocable en particular puede devolver un valor o no, según el flujo de control.

Efectos secundarios

En muchos contextos, una función invocable puede tener 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 principios de diseño, considera que los efectos secundarios son indeseables . Martin argumenta que los efectos secundarios pueden generar acoplamiento temporal o dependencias de orden. [ 29 ]

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. Estos lenguajes generalmente solo admiten funciones que devuelven un valor, ya que no existe ningún valor en una función que no tenga ni valor de retorno ni efectos secundarios.

Variables locales

La mayoría de los contextos admiten variables locales : memoria propiedad de una función invocable para almacenar valores intermedios. Estas variables suelen almacenarse 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 .

Llamada anidada recursión

Si el lenguaje lo permite, una función puede llamarse a sí misma, lo que provoca que su ejecución se suspenda mientras se ejecuta otra función anidada de la misma. 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 la función recursiva utilice las mismas variables en lugar de variables locales, normalmente las declara en un contexto compartido, como `static` o `global`.

Los lenguajes de programación, desde ALGOL , PL/I y C hasta los lenguajes modernos, utilizan casi invariablemente una pila de llamadas, generalmente integrada en los conjuntos de instrucciones, para registrar la activación de cada llamada. De esta forma, una llamada anidada puede modificar sus variables locales sin afectar a las variables de la llamada suspendida.

La recursión 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 para encontrar los números de Fibonacci :

int fibonacci ( unsigned int n ) { if ( n <= 1 ) { return n ; } return fibonacci ( n - 1 ) + fibonacci ( n - 2 ); }

Los lenguajes primitivos como Fortran no admitían inicialmente la recursión porque solo se asignaba un conjunto de variables y una dirección de retorno para cada función invocable. [ 30 ] Los primeros conjuntos de instrucciones de las computadoras 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 .

Alcance anidado

Algunos lenguajes, como Ada , Pascal , PL/I y Python , permiten declarar y definir una función dentro del cuerpo de otra función, de modo que el nombre de la función interna solo sea visible dentro del cuerpo de la función externa.

Un ejemplo sencillo en Pascal:

función E ( x : real ) : real ; función F ( y : real ) : real ; inicio F := x + y fin ; inicio E := F ( 3 ) + F ( 4 ) fin ;

La función Festá anidada dentro de E. Tenga en cuenta que Eel parámetro de xtambién es visible en F(ya que Fes parte de E) mientras que tanto xcomo yson invisibles fuera de Ey Frespectivamente.

Reentrada

Si una función invocable puede ejecutarse correctamente incluso cuando otra ejecución de la misma función ya está en curso, se dice que dicha función es reentrante . Una función invocable reentrante también resulta útil en entornos multihilo, ya que varios hilos pueden invocarla sin temor a interferir entre sí. En el sistema de procesamiento de transacciones IBM CICS , la cuasi-reentrante era un requisito ligeramente menos restrictivo, pero similar, para los programas de aplicación compartidos por varios hilos.

Sobrecarga

Algunos lenguajes admiten sobrecarga : permiten múltiples funciones invocables con el mismo nombre en el mismo ámbito, pero que operan con diferentes tipos de entrada. Consideremos la función de raíz cuadrada aplicada a 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 distinto. Al escribir tres funciones invocables separadas con el mismo nombre, por ejemplo, ` sqrt` , el código resultante puede ser más fácil de escribir y mantener, ya que cada una tiene un nombre relativamente fácil de entender y recordar, en lugar de usar nombres más largos y complicados como `sqrt_real` , `sqrt_complex` , `qrt_matrix` .

La sobrecarga es compatible con muchos lenguajes que admiten tipado fuerte . A menudo, el compilador selecciona la sobrecarga que debe llamar según el tipo de los argumentos de entrada, o falla si los argumentos de entrada no seleccionan ninguna sobrecarga. Los lenguajes más antiguos y de tipado débil generalmente no admiten la sobrecarga.

Aquí hay un ejemplo de sobrecarga en C++ , dos funciones areaque aceptan diferentes tipos:

// Devuelve el área de un rectángulo definido por altura y anchura. double area ( double height , double width ) { return h * w ; }// Devuelve el área de un círculo definido por radio double area ( double radius ) { return radius * radius * std :: numbers :: pi ; }int main () { double rectangleArea = area ( 3 , 4 ); double circleArea = area ( 5 ); }

PL/I tiene el GENERICatributo para 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(FLOTAR), ruta_de_ruta_de_otro_caso);

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 FIXED BINARY, a "flame" cuando sea FLOAT, etc. Si el argumento no coincide con ninguna de las opciones, se llamará a "pathname".

Cierre

Un cierre es una función invocable que contiene los valores de algunas de sus variables, obtenidos del entorno en el que se creó. Los cierres fueron una característica destacada del lenguaje de programación Lisp, introducido por John McCarthy . Dependiendo de la implementación, los cierres pueden funcionar como un mecanismo para efectos secundarios.

Notificación de excepciones

Además de su comportamiento habitual en caso de fallo , una función invocable puede necesitar informar al emisor sobre una condición excepcional que se haya producido durante su ejecución.

La mayoría de los lenguajes modernos admiten excepciones, lo que permite un flujo de control excepcional que va eliminando elementos de la pila de llamadas hasta que se encuentra un manejador de excepciones para gestionar la condición.

Los lenguajes que no admiten excepciones pueden usar el valor de retorno para indicar si una llamada se realizó correctamente o no. Otra opción es usar una ubicación conocida, como una variable global, para indicar el éxito de la llamada. Una función escribe el valor y quien la llama lo lee después de la llamada.

En el 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 directo de la tabla de ramificación, que generalmente 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 del 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) utiliza el valor devuelto en el registro 15 para indexar la tabla de ramas, * ramificación a la instrucción de rama apropiada. TABLA B OK código de retorno =00 BUENO } B Código de retorno BAD = 04 Entrada no válida } Tabla de ramificación B ERROR código de retorno =08 Condición inesperada } 

Llamada por encima

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

  • Asignación y recuperación de almacenamiento de pila de llamadas
  • Guardar y restaurar los registros del procesador
  • Copia de variables de entrada
  • Copiar valores después de la llamada en el contexto del llamador
  • Prueba automática del código de retorno
  • Manejo de excepciones
  • Despacho, por ejemplo, para un método virtual en un lenguaje orientado a objetos.

Se emplean diversas técnicas para minimizar el coste 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 usar si la función 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 usar su valor 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 xdebe obtenerse de nuevo antes de la segunda llamada, ya que la primera llamada puede haberlo modificado. Determinar si una función invocable tiene un efecto secundario es difícil , de hecho, indecidible en virtud del teorema de Rice . Por lo tanto, 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 funcional normalmente asume el peor caso, que toda función invocable puede tener efectos secundarios.

Inserción en línea

La inserción de código en línea elimina las llamadas a funciones específicas. El compilador reemplaza cada llamada con el código compilado de la función. Esto no solo evita la sobrecarga de la llamada, sino que también permite al compilador optimizar el código de la función que realiza la llamada de manera más eficaz , teniendo en cuenta el contexto y los argumentos. Sin embargo, la inserción de código en línea suele aumentar el tamaño del código compilado, excepto cuando se llama solo una vez o el cuerpo de la función es muy corto, como una sola línea.

Intercambio

Las funciones invocables pueden definirse dentro de un programa o por separado en una biblioteca que puede ser utilizada por varios programas.

Interoperabilidad

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

Funciones integradas

Una función integrada , o función intrínseca , es una función para la cual el compilador genera código en tiempo de compilación o la proporciona de una manera diferente a otras funciones. [ 31 ] Una función integrada no necesita ser definida como otras funciones, ya que está integrada en el lenguaje de programación. [ 32 ]

funciones evaluadas en tiempo de compilación

En algunos lenguajes de programación, como C++ , Rust y Zig , las funciones se pueden evaluar en tiempo de compilación , lo que reduce la sobrecarga en tiempo de ejecución al no utilizar marcos de pila adicionales , usar menos memoria e incorporar los valores de retorno al propio código binario . [ 33 ]

Programación

Compensaciones

Ventajas

Entre las ventajas de dividir un programa en funciones se incluyen:

  • Descomponer una tarea de programación compleja en pasos más sencillos: esta es una de las dos herramientas principales de la programación estructurada , junto con las estructuras de datos.
  • Reducción de código duplicado dentro de un programa
  • Permitir la reutilización de código en múltiples programas.
  • Dividir una tarea de programación grande entre varios programadores o varias etapas de un proyecto.
  • Ocultar los detalles de implementación a los usuarios de la función.
  • Se mejora la legibilidad del código reemplazando un bloque de código con una llamada a una función, donde el nombre descriptivo de la función sirve para describir dicho bloque. Esto hace que el código que realiza la llamada sea conciso y legible, incluso si la función no está destinada a ser reutilizada.
  • Mejorar la trazabilidad (es decir, la mayoría de los lenguajes ofrecen formas de obtener el rastro de llamadas que incluye los nombres de las funciones involucradas y quizás incluso más información, como nombres de archivos y números de línea); al no descomponer el código en funciones, la depuración se vería gravemente perjudicada.

Desventajas

En comparación con el uso de código en línea, la invocación de una función impone cierta sobrecarga computacional en el mecanismo de llamada.

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

Convenciones

Se han desarrollado numerosas convenciones de programación en relación con las funciones invocables.

En lo que respecta a la nomenclatura, muchos desarrolladores nombran una función invocable con una frase que comienza con un verbo cuando realiza una tarea determinada, con un adjetivo cuando realiza una consulta y con un sustantivo cuando se utiliza para sustituir variables.

Algunos programadores sugieren que una función invocable debe realizar exactamente una tarea, y si realiza más de una, debe dividirse en varias funciones invocables. Argumentan que las funciones invocables son componentes clave en el mantenimiento del software y que sus funciones dentro del programa deben permanecer diferenciadas.

Los defensores de la programación modular abogan por que cada función invocable tenga una dependencia mínima del resto del código . Por ejemplo, el uso de variables globales se considera generalmente poco recomendable, ya que añade acoplamiento entre todas las funciones invocables que las utilizan. Si dicho acoplamiento no es necesario, aconsejan refactorizar las funciones invocables para que acepten parámetros pasados ​​como argumento.

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 proporcionan separación del código que se puede llamar, no tienen mecanismo para pasar argumentos ni para 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 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 y muestra la raíz cuadrada de dicho valor. Las líneas 100 a 130 son la función invocable.

Pequeño básico

En Microsoft Small Basic , dirigido a estudiantes que se inician en la programación en un lenguaje basado en texto, una unidad invocable se denomina subrutina . La Subpalabra clave indica el inicio de una subrutina y va seguida de un identificador de nombre. Las líneas subsiguientes constituyen el cuerpo, que finaliza con la EndSubpalabra clave. [ 34 ]

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

Esto puede denominarse como SayHello(). [ 35 ]

Visual Basic

En versiones posteriores de Visual Basic (VB), incluyendo 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 utiliza en el contexto de una clase, un procedimiento es un método. [ 36 ]

Cada parámetro tiene un tipo de datos que se puede especificar, pero si no se especifica, se utiliza por defecto Objectpara versiones posteriores basadas en .NET y variantes para VB6 . [ 37 ]

VB admite convenciones de paso de parámetros por valor y por referencia mediante las palabras clave ByValy ByRef, respectivamente. A menos ByRefque se 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 HacerAlgo () ' Algún código aquí Fin Sub

No devuelve un valor y debe llamarse de forma independiente, comoDoSomething

Función GiveMeFive () como entero GiveMeFive = 5 Fin de la función

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

Sub AddTwo ( ByRef intValue as Integer ) intValue = intValue + 2 End Sub

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

C y C++

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, voidsi no devuelve ningún valor, con el nombre de la función, los argumentos formales entre paréntesis y el cuerpo del código 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 puede denominarse función libre para distinguirla de una función miembro. [ 38 ]

void hacerAlgo () { // algún código }

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

int returnFive () { return 5 ; }

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

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. Se puede llamar para una variable vcomo addTwo(&v)donde el signo de 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.

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

Esta función requiere C++ ; no compilaría en 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 esta addTwo(v)no incluye un signo de ampersand, ya que el compilador gestiona 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 proporciona información sobre el argumento, como la longitud de las cadenas y los límites del array. 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 los argumentos por referencia. Una función (trivial) para cambiar el signo de cada elemento de un array bidimensional podría ser la siguiente:

change_sign : procedimiento ( array ) ; declarar array (*,*) float ; array = - array ; fin change_sign ;

Esto podría llamarse con varios arreglos de la siguiente manera:

/* primer rango de la matriz: de -5 a +10 y de 3 a 9 */ declare array1 (- 5 : 10 , 3 : 9 ) float ; /* segundo rango de la matriz: de 1 a 16 y de 1 a 16 */ declare array2 ( 16 , 16 ) float ; call change_sign ( array1 ) ; call change_sign ( array2 ) ;

Pitón

En Python , la palabra clave defindica el inicio de la definición de una función. Las instrucciones del cuerpo de la función siguen con sangría 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. [ 39 ]

def format_greeting ( name : str ) -> str : return f "¡Bienvenido, { name } !"def greet_martin () -> None : print ( format_greeting ( "Martin" ))

La primera función devuelve un texto de saludo que incluye el nombre proporcionado por quien la llama. La segunda función llama a la primera y se invoca de forma similar greet_martin()a como se escribe "Bienvenido Martin" en la consola.

Prólogo

En la interpretación procedimental de los 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

lo cual tiene la lectura lógica:

A if B

se comporta como un procedimiento que reduce los objetivos que se unifican con Aa 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 ).

Nótese que la función de maternidad se representa mediante una relación, como en una base de datos relacional . Sin embargo, en Prolog , las relaciones funcionan como unidades invocables.X=mother(Y)

Por ejemplo, la llamada al 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 ). .

Véase 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; CAR Hoare (1972). Programación estructurada . Academic Press. ISBN 0-12-200550-3.
  4. "¿Qué es la abstracción en programación? Explicado para principiantes" . freeCodeCamp.org . 21 de diciembre de 2022. Consultado el 25 de noviembre de 2025 .
  5. 1 2 Mauchly, JW (1982). "Preparación de problemas para máquinas tipo EDVAC" . En Randell, Brian (ed.). Los orígenes de las computadoras digitales . Springer. pp. 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 programas" (PDF) . Actas de la reunión nacional de la ACM de 1952 (Pittsburgh) sobre - ACM '52 . pág. 235. doi : 10.1145/609784.609816 . 
  7. Wilkes, MV; Wheeler, DJ; Gill, S. (1951). Preparación de programas para una computadora digital electrónica . Addison-Wesley.
  8. Dainith, John (2004). ""subrutina abierta". Diccionario de informática . Encyclopedia.com . Consultado el 14 de enero de 2013 .
  9. Turing, Alan M. (1945), Informe del Dr. AM Turing sobre propuestas para el desarrollo de una Máquina de Computación Automática (ACE): Presentado al Comité Ejecutivo del NPL en febrero de 1946Reimpreso en Copeland, BJ , ed. (2005). Alan Turing's Automatic Computing Engine . Oxford: Oxford University Press. p. 383. ISBN  0-19-856593-3.
  10. Speiser, Ambros Paul (2002). «La Z4 de Konrad Zuse: Arquitectura, programación y modificaciones en la ETH Zúrich». En Rojas, Rául ; Hashagen, Ulf [en alemán] (eds.). Los primeros ordenadores: Historia y arquitecturas . MIT. pp. 263–276 . ISBN  978-0-262-18197-6.
  11. Turing, Alan Mathison (19 de marzo de 1946) [1945], Propuestas para el desarrollo en la división de matemáticas de una máquina de cálculo automática (ACE)(Nota: Presentado el 19 de marzo de 1946 ante el Comité Ejecutivo del Laboratorio Nacional de Física (Gran Bretaña).)
  12. Carpenter, Brian Edward ; Doran, Robert William (1 de enero de 1977) [octubre de 1975]. "La otra máquina de Turing" . The Computer Journal . 20 (3): 269– 279. doi : 10.1093/comjnl/20.3.269 .(11 páginas)
  13. 1 2 Isaacson, Walter (18 de septiembre de 2014). "Walter Isaacson sobre las mujeres de ENIAC" . Fortune . Archivado del original el 12 de diciembre de 2018. Recuperado el 14 de diciembre de 2018 .
  14. Herman H. Goldstine; John von Neumann (1947). "Volumen 3, 12.0 COMBINACIÓN DE RUTINAS" (PDF) . Informe sobre los aspectos matemáticos y lógicos de un instrumento de computación electrónica - Parte II, Volumen I-3, Planificación y codificación de problemas para un instrumento de computación electrónica (PDF) (Informe técnico). Instituto de Estudios Avanzados .
  15. Características operativas de los procesadores para el Burroughs B5000 (PDF) . Revisión A. Burroughs Corporation . 1963. 5000-21005 . Consultado el 8 de febrero de 2024 .
  16. "Instrucciones de empuje hacia abajo" (PDF) . Procesador de datos programado 6 - Manual (PDF) . pág. 37. Consultado el 8 de febrero de 2024 . 
  17. Guy Lewis Steele Jr. AI Memo 443. 'Desmintiendo el mito de la "llamada a procedimiento costosa"; o, Implementaciones de llamadas a procedimiento consideradas perjudiciales' . Sección "C. Por qué las llamadas a procedimiento tienen mala reputación".
  18. Frank, Thomas S. (1983). Introducción al PDP-11 y su lenguaje ensamblador . Serie de software de Prentice-Hall. Prentice-Hall. pág. 195. ISBN  9780134917047Recuperado el 6 de julio de 2016. Podríamos proporcionar a nuestro empleado de ensamblaje copias del código fuente de todas nuestras subrutinas útiles y luego, al presentarle un programa principal para ensamblaje, indicarle qué subrutinas se llamarán en el programa principal [...]
  19. El sistema informático de propósito general UNIVAC Scientific (modelo 1103 A) - Información preliminar (PDF) . Remington Rand Engineering Research Associates Division. 1 de diciembre de 1955. págs. 1-10 . Consultado el 8 de octubre de 2025 . 
  20. "Control Operations" (PDF) . 704 Electronic Data Processing Machine - Manual of Operations (PDF) . IBM . 1955. p. 25. 24-6661-2 . Consultado el 8 de octubre de 2025 . 
  21. "Almacenamiento automático del contenido del registro P (STP)" (PDF) . Sistema de procesamiento electrónico de datos RCA 501 - Manual de referencia para programadores (PDF) . División de procesamiento electrónico de datos de RCA . 1958. pág. 18. P501-2 . Consultado el 8 de octubre de 2025 . 
  22. "Registros de historial" (PDF) . Sistema de procesamiento de datos transistorizado Honeywell 800 - Manual de referencia del programador (PDF) . Honeywell . 1960. pág. 55. DSI-31 . Consultado el 8 de octubre de 2025 . 
  23. Manual de programación de la computadora electrónica Royal Precision LGP-30 . Port Chester, Nueva York: Royal Precision. Abril de 1957. Consultado el 19 de noviembre de 2023 .
  24. Tanenbaum, Andrew S.; Austin, Todd (2013). Organización estructurada de computadoras (6.ª ed.). Boston: Pearson. ISBN  978-0-13-291652-3.
  25. Tanenbaum, Andrew S.; Austin, Todd (2013). Organización estructurada de computadoras . Siempre aprendiendo (6.ª ed.). Boston, Mass. Múnich: Pearson. ISBN  978-0-13-291652-3.
  26. Buttlar, Dick; Farrell, Jacqueline; Nichols, Bradford (1996). PThreads Programming: A POSIX Standard for Better Multiprocessing . O'Reilly Media, Inc. pp. 2–5 . ISBN  978-1-4493-6475-5OCLC 1036778036 .​ 
  27. "Centro de información de ARM" . Infocenter.arm.com . Consultado el 29 de septiembre de 2013 .
  28. "Uso de la pila x64" . Microsoft Learn . Microsoft. Tipos de funciones . Consultado el 29 de octubre de 2025 .
  29. Martin, Robert C. (1 de agosto de 2008). Clean Code: A Handbook of Agile Software Craftsmanship (1.ª ed.). Pearson . ISBN  9780132350884. Consultado el 19 de mayo de 2024 .
  30. Verhoeff, Tom (2018). «Una clase magistral sobre recursión» . 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 . Springer. pág. 616. ISBN  978-3-319-98355-4OCLC 1050567095 
  31. "Funciones integradas" . ibm.com . 9 de marzo de 2017. Consultado el 25 de diciembre de 2023 .
  32. Material de estudio Python . Abril de 2023. pág. 87. Consultado el 25 de diciembre de 2023 . 
  33. Microsoft Learn (22 de febrero de 2023). "constexpr (C++)" . learn.microsoft.com . Microsoft Learn.
  34. "Small Basic" . Small Basic . Consultado el 8 de febrero de 2024 .
  35. "Guía básica de introducción: Capítulo 9: Subrutinas" . Microsoft. 17 de enero de 2024.
  36. "Procedimientos en Visual Basic" . Microsoft Learn . 15 de septiembre de 2021. Consultado el 8 de febrero de 2024 .
  37. "Instrucción Dim (Visual Basic)" . Microsoft Learn . 15 de septiembre de 2021. Consultado el 8 de febrero de 2024 .
  38. "qué se entiende por función libre" .
  39. "4. Más herramientas de control de flujo — Documentación de Python 3.9.7" .