En informática , un parámetro de procedimiento es un parámetro de un procedimiento que es en sí mismo un procedimiento.
Este concepto es una herramienta de programación extremadamente poderosa y versátil , porque permite a los programadores modificar ciertos pasos de un procedimiento de biblioteca de formas arbitrariamente complicadas, sin tener que entender o modificar el código de ese procedimiento.
Esta herramienta es particularmente efectiva y conveniente en lenguajes que admiten definiciones de funciones locales , como Pascal y el dialecto GNU moderno de C. Es aún más así cuando se dispone de cierres de funciones . Los objetos en lenguajes de programación orientados a objetos proporcionan la misma funcionalidad (y más) , pero a un costo significativamente mayor.
Los parámetros procedimentales están relacionados de alguna manera con los conceptos de función de primera clase y función anónima , pero son distintos de ellos. Estos dos conceptos tienen más que ver con cómo se definen las funciones que con cómo se utilizan.
En la mayoría de los lenguajes que proporcionan esta característica, un parámetro de procedimiento f de una subrutina P se puede llamar dentro del cuerpo de P como si fuera un procedimiento ordinario:
procedimiento P ( f ): devuelve f (6,3) * f (2,1)
Al llamar a la subrutina P , se le debe dar un argumento, que debe ser alguna función previamente definida y compatible con la forma en que P utiliza su parámetro f . Por ejemplo, si definimos
procedimiento más ( x , y ): devuelve x + y
entonces podemos llamar P ( más ), y el resultado será más (6,3) * más (2,1) = (6 + 3)*(2 + 1) = 27. Por otro lado, si definimos
procedimiento quot ( u , v ): devuelve u / v
entonces la llamada P ( quot ) devolverá quot (6,3)* quot (2,1) = (6/3)*(2/1) = 4. Finalmente, si definimos
procedimiento malvado ( z ) devuelve z + 100
entonces la llamada P ( malvada ) no tendrá mucho sentido y puede marcarse como un error.
Algunos lenguajes de programación que tienen esta característica pueden permitir o requerir una declaración de tipo completa para cada parámetro de procedimiento f , incluyendo el número y tipo de sus argumentos, y el tipo de su resultado, si lo hay. Por ejemplo, en el lenguaje de programación C el ejemplo anterior podría escribirse como
int P ( int ( * f )( int a , int b )) { devolver f ( 6 , 3 ) * f ( 2 , 1 ); }
En principio, la función actual actf que se pasa como argumento cuando se llama a P debe ser compatible con el tipo declarado del parámetro de procedimiento f . Esto generalmente significa que actf y f deben devolver el mismo tipo de resultado, deben tener la misma cantidad de argumentos y los argumentos correspondientes deben tener el mismo tipo. Sin embargo, los nombres de los argumentos no necesitan ser los mismos, como se muestra en los ejemplos de más y quot anteriores. Sin embargo, algunos lenguajes de programación pueden ser más restrictivos o más liberales en este sentido.
En los lenguajes que permiten parámetros procedimentales, las reglas de alcance suelen definirse de tal forma que los parámetros procedimentales se ejecutan en su ámbito nativo. Más precisamente, supongamos que la función actf se pasa como argumento a P , como su parámetro procedimental f ; y f se llama entonces desde dentro del cuerpo de P . Mientras actf se ejecuta, ve el entorno de su definición. [ ejemplo necesario ]
La implementación de estas reglas de alcance no es trivial. Para cuando finalmente se ejecuta actf , los registros de activación donde se encuentran sus variables de entorno pueden estar en una profundidad arbitraria en la pila. Este es el llamado problema funarg descendente .
El concepto de parámetro procedimental se explica mejor con ejemplos. Una aplicación típica es la siguiente implementación genérica del algoritmo de ordenamiento por inserción , que toma dos parámetros enteros a , b y dos parámetros procedimentales prec , swap :
procedimiento isort ( a , b , prec , swap ): entero i , j ; i ← a ; mientras i ≤ b hacer j ← i ; mientras j > a y prec ( j , j −1) hacer swap ( j , j −1); j ← j −1; i ← i +1;
Este procedimiento se puede utilizar para ordenar los elementos x [ a ] a x [ b ] de alguna matriz x , de tipo arbitrario , en un orden especificado por el usuario. Los parámetros prec y swap deben ser dos funciones , definidas por el cliente, ambas tomando dos enteros r , s entre a y b . La función prec debe devolver verdadero si y solo si los datos almacenados en x [ r ] deben preceder a los datos almacenados en x [ s ], en el orden definido por el cliente. La función swap debe intercambiar los contenidos de x [ r ] y x [ s ], y no devolver ningún resultado.
Mediante la elección adecuada de las funciones prec y swap , se puede utilizar el mismo procedimiento isort para reordenar matrices de cualquier tipo de datos, almacenadas en cualquier medio y organizadas en cualquier estructura de datos que proporcione acceso indexado a elementos individuales de la matriz. (Sin embargo, tenga en cuenta que existen algoritmos de ordenamiento que son mucho más eficientes que el ordenamiento por inserción para matrices grandes).
Por ejemplo, podemos ordenar una matriz z de 20 números de punto flotante, z [1] a z [20] en orden creciente llamando a isort (1, 20, zprec , zswap ), donde las funciones zprec y zswap se definen como
procedimiento zprec ( r , s ): return ( z [ r ] < z [ s ]);procedimiento zswap ( r , s ): float t ; t ← z [ r ]; z [ r ] ← z [ s ]; z [ s ] ← t
Para otro ejemplo, supongamos que M es una matriz de números enteros con 10 filas y 20 columnas, con índices que comienzan en 1. El siguiente código reorganizará los elementos en cada fila de modo que todos los valores pares estén antes que todos los valores impares:
entero i procedimiento eoprec ( r , s ): return ( M [ i , r ] mod 2) < ( M [ i , s ] mod 2);procedimiento eoswap ( r , s ): entero t ; t ← M [ i , r ]; M [ i , r ] ← M [ i , s ]; M [ i , s ] ← t ;para i del 1 al 10 hacer isort (1, 20, eoprec, eoswap);
Tenga en cuenta que los efectos de eoprec y eoswap dependen del número de fila i , pero el procedimiento isort no necesita saberlo.
El siguiente ejemplo utiliza isort para definir un procedimiento vecsort que toma un entero n y un vector entero v con elementos v [0] a v [ n −1] y los ordena en orden creciente o decreciente, dependiendo de si un tercer parámetro incr es verdadero o falso , respectivamente:
procedimiento vecsort ( n , v , incr ): procedimiento vprec ( r , s ): si incr entonces devuelve v [ r ] < v [ s ]; de lo contrario devuelve v [ r ] > v [ s ]; procedimiento vswap ( r , s ): entero t ; t ← v [ r ]; v [ r ] ← v [ s ]; v [ s ] ← t isort (0, n −1, vprec , vswap );
Tenga en cuenta el uso de definiciones de funciones anidadas para obtener una función vprec cuyo efecto depende del parámetro incr pasado a vecsort . En lenguajes que no permiten definiciones de funciones anidadas, como C estándar, obtener este efecto requeriría un código bastante complicado y/o no seguro para subprocesos .
El siguiente ejemplo ilustra el uso de parámetros procedimentales para procesar estructuras de datos abstractas independientemente de su implementación concreta. El problema consiste en fusionar dos secuencias ordenadas de registros en una única secuencia ordenada, donde la naturaleza de los registros y el criterio de ordenación pueden ser elegidos por el cliente. La siguiente implementación supone únicamente que cada registro puede ser referenciado por una dirección de memoria y que existe una "dirección nula" Λ que no es la dirección de ningún registro válido. El cliente debe proporcionar las direcciones A , B de los primeros registros de cada secuencia y las funciones prec , next y append , que se describirán más adelante.
procedimiento merge ( A , B , prec , nextA , appendA , nextB , appendB ): dirección ini , fin , t ini ← Λ; fin ← Λ mientras A ≠ Λ o B ≠ Λ hacer si B = Λ o ( A ≠ Λ y B ≠ Λ y prec ( A , B )) entonces t ← nextA ( A ) fin ← appendA ( A , fin ); si ini = Λ entonces ini ← fin A ← t de lo contrario t ← nextB ( B ) fin ← appendB ( B , fin ); si ini = Λ entonces ini ← fin B ← t devolver ini
La función prec debe tomar las direcciones r , s de dos registros, uno de cada secuencia, y devolver verdadero si el primer registro debe venir antes que el otro en la secuencia de salida. La función nextA debe tomar la dirección de un registro de la primera secuencia y devolver la dirección del siguiente registro en la misma secuencia, o Λ si no hay ninguno. La función appendA debe agregar el primer registro de la secuencia A a la secuencia de salida; sus argumentos son la dirección A del registro que se agregará y la dirección fin del último registro de la lista de salida (o Λ si esa lista aún está vacía). El procedimiento appendA debe devolver la dirección actualizada del elemento final de la lista de salida. Los procedimientos nextB y appendB son análogos para la otra secuencia de entrada.
Para ilustrar el uso del procedimiento de fusión genérico, aquí está el código para fusionar dos listas enlazadas simples , comenzando con nodos en las direcciones R , S. Aquí asumimos que cada registro x contiene un campo entero x.INFO y un campo de dirección x.NEXT que apunta al siguiente nodo; donde los campos de información están en orden creciente en cada lista. Las listas de entrada se desmantelan mediante la fusión y sus nodos se utilizan para construir la lista de salida.
procedimiento listmerge ( R , S ): procedimiento prec ( r , s ): devuelve r . INFO < s . INFO procedimiento siguiente ( x ): devuelve x . SIGUIENTE procedimiento append ( x , fin ) si fin ≠ Λ entonces fin . NEXT ← x x . NEXT ← Λ devuelve x devolver fusión ( R , S , anterior , siguiente , anexar , siguiente , anexar )
El código siguiente ilustra la independencia del procedimiento de fusión genérico de la representación real de las secuencias. Fusiona los elementos de dos matrices ordinarias U [0] a U [ m −1] y V [0] a V [ n −1] de números de punto flotante, en orden decreciente. Las matrices de entrada no se modifican y la secuencia fusionada de valores se almacena en un tercer vector W [0] a W [ m + n −1]. Como en el lenguaje de programación C, suponemos que la expresión "& V " produce la dirección de la variable V , "* p " produce la variable cuya dirección es el valor de p , y que "&( X [ i ])" es equivalente a "&( X [0]) + i " para cualquier matriz X y cualquier entero i .
procedimiento arraymerge ( U , m , V , n , W ): procedimiento prec ( r , s ): return (* r ) > (* s ) procedimiento nextU ( x ): si x = &( U [ m −1]) entonces devuelve Λ de lo contrario devuelve x + 1 procedimiento nextV ( x ): si x = &( V [ n −1]) entonces devuelve Λ de lo contrario devuelve x + 1 procedimiento append ( x , fin ) si fin = Λ entonces fin ← &( W [0]) (* fin ) ← (* x ) devuelve fin + 1 si m = 0 entonces U ← Λ si n = 0 entonces V ← Λ devolver fusión ( U , V , prec , nextU , append , nextV , append )
El siguiente procedimiento calcula la integral aproximada f ( x ) d x de una función real f dada en un intervalo dado [ a , b ] de la recta real . El método numérico utilizado es la regla del trapecio con un número dado de pasos n ; los números reales se aproximan mediante números de punto flotante.
procedimiento Intg ( f , a , b , n ): float t , x , s ; entero i si b = a entonces devuelve 0 x ← a ; s ← f ( a )/2; para i de 1 a n −1 hace t ← i /( n +1); x ← (1− t )* a + t * b ; s ← s + f ( x ) s ← f ( b )/2 devuelve ( b − a )* s / n
Consideremos ahora el problema de integrar una función dada , con dos argumentos, sobre un disco con centro dado ( ) y radio dado . Este problema se puede reducir a dos integrales anidadas de una sola variable mediante el cambio de variables
El siguiente código implementa la fórmula del lado derecho :
procedimiento DiskIntg ( g , xc , yc , R , n ) procedimiento gring ( z ): procedimiento gpolar ( t ): float x , y x ← xc + z * cos ( t ) y ← yc + z * sin ( t ) devuelve g ( x , y ) entero m ← ronda ( n * z / R ) retorno z * Intg ( gpolar , 0, 2*π, m ) devolver Intg ( gring , 0, R , n )
Este código utiliza el procedimiento de integración Intg en dos niveles. El nivel externo (última línea) utiliza Intg para calcular la integral de para los que varían de 0 a . El nivel interno (penúltima línea) define como la integral de línea de sobre el círculo con centro y radio .
Los parámetros de procedimiento fueron inventados antes de la era de las computadoras electrónicas, por el matemático Alonzo Church , como parte de su modelo de cálculo lambda .
Los parámetros procedimentales como característica del lenguaje de programación fueron introducidos por ALGOL 60. De hecho, ALGOL 60 tenía un poderoso mecanismo de paso de parámetros de " llamada por nombre " que podía simplificar algunos usos de los parámetros procedimentales; véase Dispositivo de Jensen .
Los parámetros procedimentales fueron una característica esencial del lenguaje de programación LISP , que también introdujo el concepto de cierre de función o funarg . El lenguaje de programación C permite que los punteros de función se pasen como parámetros, que logran el mismo fin, y se utilizan a menudo como devoluciones de llamadas en la programación basada en eventos y como controladores de errores. Sin embargo, solo unos pocos compiladores de C modernos permiten definiciones de funciones anidadas, por lo que sus otros usos son relativamente poco comunes. Los parámetros procedimentales también se proporcionaron en Pascal, junto con las definiciones de procedimientos anidados; sin embargo, dado que Pascal estándar no permitía la compilación por separado, la característica también se usó poco en ese lenguaje.