Subprogramas en ADA

En Ada existen dos tipos de subprogramas: funciones y procedimientos. Las funciones retornan un valor y son usadas en expresiones, mientras que los procedimientos no retornan valor y son llamados como instrucciones.

Las acciones a realizar en un subprograma se describen dentro de lo que se denomina “cuerpo del subprograma”, el que es declarado de la manera usual en la parte declarativa de, por ejemplo, un bloque u otro subprograma.

Funciones

 

Todas las funciones comienzan con la palabra reservada function seguida del nombre (identificador) de la función. Si existen parámetros, después del identificador se entrega una lista con los parámetros (separados por “;”) encerrada entre paréntesis. Luego de la lista de parámetros (si existe) viene la palabra return y el tipo (o subtipo) del valor de retorno de la función. Tanto el tipo de los parámetros como del valor de retorno debe ser indicado con un identificador de tipo (o subtipo) declarado previamente. Por ejemplo, no es posible indicar que el valor de retorno será un cierto rango de los enteros indicándolo explícitamente luego de la palabra return, sino que es necesario definir un subtipo para dicho rango con anterioridad a la declaración de la función y escribir el nombre del subtipo a continuación de la palabra return.

A la parte descrita hasta ahora  se le conoce como “especificación de la función” y es la que entrega

los datos para el entorno, en el sentido que en ella se entrega la información necesaria y suficiente para llamar a (hacer uso de) la función.

Después de la especificación viene la palabra is seguida del cuerpo de la función, que es semejante a un bloque: una parte declarativa, begin, una secuencia de instrucciones y end. Como en el caso de los bloques, la parte declarativa puede no existir, pero al menos debe existir una instrucción.

En algunos casos (como veremos más adelante) es necesario escribir sólo la especificación de la

función, sin su cuerpo. En este caso en lugar de la palabra is va un “;”.

Los parámetros formales son objetos locales de una función y actúan como constantes cuyos valores iniciales son calculados de acuerdo a los correspondientes parámetros reales. Cuando una función es llamada (utilizada dentro de un expresión) se elabora la parte declarativa de la manera usual y luego se ejecutan las instrucciones. Para entregar el valor de retorno se utiliza la instrucción return, la cual además entrega el control al lugar desde donde se hizo la llamada.

Consideremos nuestro ejemplo de la raíz cuadrada:

 

function SQRT(X:REAL) return REAL is

R:REAL;

 

begin


 

—  calcular valor de la raíz de X y guardarlo en R

return R;

 

end SQRT;

La función puede ser utilizada, por ejemplo, en T:REAL:= 0.3;

. . .

S:= SQRT(T + 0.5) + 3.6;

 

para ello se evalúa la expresión T + 0.5 (es decir, T + 0.5 es el parámetro real) y se asigna al parámetro formal X, el que dentro de la función se comporta como una constante, con valor inicial 0.8. Esto equivale a tener

 

X: constant REAL := T + 0.5;

 

Luego se elaboran las declaraciones, si las hubiera, y finalmente se ejecutan las instrucciones. La última de las cuales es la instrucción return, la que retorna el control a la expresión “SQRT(T + 0.5) + 3.6” junto con el valor contenido en R.

La expresión de una instrucción return puede ser de cualquier nivel de complejidad, lo importante es que el resultado de dicha expresión sea del tipo indicado como tipo de retorno en la especificación de la función.

El cuerpo de una función puede contener más de una instrucción return. La ejecución de cualquiera de ellas termina la función. Por ejemplo,

 

 

 

 

 

 

function SIGN(X:INTEGER) return INTEGER is

Entrega +1, 0 o -1 según sea el signo del entero X

begin

if X > 0 then

return +1;

elseif X < 0 then

return -1;

 

else

 

end if; end SIGN;


 

return 0;

 

 

Puede verse que la última instrucción no es necesariamente un return , lo importante es que la semántica del cuerpo de la función considere una instrucción return para todos los posibles casos. Si no fuese así y se llegase “end SIGN;” el sistema entregaría la excepción PROGRAM_ERROR (este tipo de excepción se usa para situaciones en las cuales se ha violado la secuencia de control en tiempo de ejecución).

Notemos que cada llamada a una función genera una nueva instancia de cada uno de los objetos

declarados en ella (incluyendo, lógicamente, los parámetros) y éstos desaparecen cuando la función termina. Por este motivo (es decir, la administración dinámica de memoria) es posible llamar recursivamente a una función. Por ejemplo:

 

function FACTORIAL(N:POSITIVE) return POSITIVE is begin

if N = 1 then return 1;

else       return N * FACTORIAL(N-1);

end if; end FACTORIAL;

 

Si escribimos

 

F:= FACTORIAL(4);

 

se produce la llamada recursiva a la función FACTORIAL con parámetros reales 4, 3, 2 y 1. A veces se dice que una función es recursiva puesto que “se llama a si misma”. Sin embargo, es necesario entender que cuando se llama a FACTORIAL(4) se genera una instancia de la función, es decir, se reserva espacio de memoria para todos los objetos locales (más otros datos) y se procede a ejecutar las instrucciones. Cuando se llega a “4*FACTORIAL(4 – 1)”, se genera otra instancia de la función, es decir, se toma más espacio de memoria para localizar los objetos locales, por lo tanto en este punto hay dos áreas de memoria para almacenar el estado de cada una de las dos llamadas (FACTORIAL(4) y FACTORIAL(3)). Esto se repetirá, en este caso, cuatro veces. Al ejecutar el cuerpo de la llamada FACTORIAL(1) no se genera una nueva instancia, sino que retorna el valor positivo 1. En este momento, la llamada FACTORIAL(1) termina, sus objetos locales dejan de existir, con lo que el espacio para ella reservado también desaparece. Lo mismo ocurrirá para las llamadas FACTORIAL(2), FACTORIAL(3) y finalmente FACTORIAL(4). Lógicamente, no es necesario repetir el código de la función, cada instancia de la función controla el número de instrucción que se está ejecutando.

No es necesario chequear que el parámetro sea positivo, pues el parámetro formal N es del subtipo POSITIVE. Por lo tanto, si se intenta FACTORILA(-2) se obtendrá CONSTRAIN_ERROR. Sin embargo, si intentamos FACTORIAL(1000) podría generarse una excepción STORAGE_ERROR puesto que las instancias de mil llamadas a la función estarían presentes en un cierto momento. Por otro lado, la llamada FACTORIAL(70) podría generar la excepción NUMERIC_ERROR..

Como ya se indicó un parámetro formal puede ser de cualquier tipo, pero dicho tipo (o subtipo) debe tener un nombre. Por lo tanto, está permitido que los parámetros puedan ser de tipo array no restringido. Por ejemplo, si definimos el tipo VECTOR como

 

type VECTOR is array (INTEGER range <>) of REAL; podemos escribir la función

function SUM(A:VECTOR) return REAL is

RESULT: REAL:= 0.0;

 

begin


 

for I in A´RANGE loop

RESULT := RESULT + A(I);

end loop;

return RESULT;

 

end SUM;

 

 

 

 

En este caso los límites del vector A serán tomados del arreglo de tipo VECTOR usado como parámetro real. Recordemos que todas las variables y constantes del tipo VECTOR deben tener límites definidos.

Entonces podemos escribir

 

V:VECTOR(1 .. 4):= (1.0,2.0,3.0,4.0);

W:VECTOR(-1 .. 3):= (1.5,2.5,3.5,4.5,5.5); S:REAL:=SUM(V);

T:REAL;

. . .

T:=SUM(W);

 

con lo que S tomará el valor 10 y T el valor 17.5.

Lógicamente, en Ada una función puede tener parámetros de tipo array restringido, pero debe ser por medio de un nombre de tipo o subtipo. Por ejemplo, sería incorrecto escribir

 

 

function SUM_6(A:VECTOR(1 .. 6)) return REAL

 

debería definirse un tipo o subtipo y luego usarlo para indicar la naturaleza del parámetro. Por ejemplo:

 

type VECTOR_6_A is array (1 ..  6) of REAL;

subtype VECTOR_6_B is VECTOR(1..6);

function SUM_6_A(A:VECTOR_6_A) return REAL;

function SUM_6_B(A:VECTOR_6_B) return REAL;

 

Lógicamente, el elegir entre definir un nuevo tipo o un subtipo dependerá de los requerimientos del problema a resolver. La diferencia entre SUM_6_A y SUM_6_B radica en que la segunda podrá aceptar arreglos de tipo VECTOR_6_B, VECTOR y otros subtipos de éste (en la media que contengan 6 elementos indexados del 1 al 6), en cambio SUM_6_A sólo aceptará arreglos del tipo VECTOR_6_A.

Consideremos otro ejemplo:

 

function INNER(A,B. VECTOR) return REAL is

RESULT: REAL := 0.0;

 

begin


 

for I in A´RANGE loop

RESULT.= RESULT + A(I) * B(I);

end loop;

return RESULT;

 

end INNER;

 

La función INNER calcula el producto interno entre dos vectores A y B.  Tenemos aquí un ejemplo de una función con más de un parámetro.

La función INNER no es un código robusto, puesto que sólo funciona correctamente cuando ambos parámetros A y B tiene los mismos límites y no se controla los casos en que éstos difieren. Por ejemplo:

 

T:VECTOR(1 .. 3):= (1.0, 2.0, 3.0);

U:VECTOR(1 .. 3):= (2.0, 3.0, 4.0);

V:VECTOR(0 .. 2):= (3.0, 4.0, 5.0);

W:VECTOR(1 .. 4):= (4.0, 5.0, 6.0, 7.0);

. . .

R:=INNER(T,U);   — R=1.0*2.0 + 2.0*3.0 + 3.0*4.0 = 20.0

R:=INNER(T,V);     — CONSTRAIN_ERROR al intentar accesar  B(3) R:=INNER(T,W);   — R=1.0*4.0 + 2.0*5.0 + 3.0*6.0 = 32.0

 

En el tercer caso se obtiene un valor (32.0), pero es erróneo calcular el producto interno de dos vectores de distinto largo. Sería deseable que el lenguaje proveyera un mecanismo para chequear en el momento de la llamada que los límites de A y B coincidan, pero lamentablemente no es así. La solución más adecuada es verificar los límites al comienzo de la función y, posiblemente, generar explícitamente una excepción CONSTRAIN_ERROR.

 

if A´FIRST /= B´FIRST or A´LAST /= B´LAST then raise CONSTRAIN_ERROR;

end if;

 

Hemos visto que un parámetro formal puede ser un arreglo no restringido, pero el valor de retorno de una función también puede ser un arreglo cuyos límites se definen de acuerdo a los del arreglo entregado por

 

 

 

 

la instrucción return. Por ejemplo, la siguiente función retorna un arreglo con los mismos límites del parámetro, pero con los elementos en orden inverso.

 

function REV(X:VECTOR) return VECTOR is

R:VECTOR(X´RANGE);

 

begin


 

for I in X´RANGE loop

R(I):= X(X´FIRST + X´LAST -I);

end loop; return R;

 

end REV; Ejercicio:

  1. Escriba una función de nombre PAR que indique  si un entero es par (TRUE) o impar (FALSE).
  2. Reescriba la función FACTORIAL de forma tal que el parámetro pueda ser positivo o cero (use el subtipo NATURAL). Recuerde que FACTORIAL(0) = 1.
  3. Escriba la función OUTER que entrega el producto externo de dos vectores (posiblemente con distintos rangos). El producto externo de los vectores A y B se define como la matriz Cij = Ai *Bj.
  4. Escriba la función MAKE_UNIT que toma un valor positivo N y entrega una matriz unitaria real de NxN.

Recuerde que una matriz unitaria es aquella que contiene unos un su diagonal principal y ceros en todos los demás elementos.

  1. Escriba la función MCD (usando recursión) que entrega el máximo común divisor de dos enteros no negativos. Use el algoritmo de Euclides.

mcd(x,y) = mcd(y, x mod y)    si y ? 0 mcd (x,0) = x

 

 

 

 

Operadores

 

Anteriormente dijimos que toda función comienza con la palabra reservada function seguida de un identificador, sin embargo también es posible usar como nombre de función un string de caracteres, siempre y cuando sea alguno de los siguientes operandos (entre comillas).

 

andorxor 
=

+

/

<

*

<=

&

mod

>

abs rem

>=

not

**

 

 

En  estos  casos  la  función  definirá  un  nuevo  significado  a  los  respectivos  operadores.  Para ejemplificar esto podemos reescribir la función INNER de la siguiente manera

 

function “*” (A,B:VECTOR) return REAL is

RESULT: REAL := 0.0;

 

begin

 

 

 

 

 

end “*”;


 

for I in A´RANGE loop

RESULT:= RESULT + A(I) * B(I);

end loop;

return RESULT;

 

 

Ahora podemos usar la nueva función con la sintaxis propia del operador *. Entonces, en lugar de R:=INNER(V,W);

podemos escribir

 

R:= V * W;

 

(Nótese  que  de  todas  maneras  se  puede  usar  la  notación  infijada  R:=  “*”(V,W),  pero  no  es recomendable).

Este nuevo significado del operador * se diferencia de los ya existentes (multiplicación de reales y enteros) por el contexto dado por los tipos de los parámetros V y W, y por el tipo requerido por R.

Al  hecho  que  un  cierto  operador  tenga  varios  significados  se  le  conoce  como  sobrecarga (overloading). La sobrecarga de operadores predefinidos no es nuevo, ha existido en los lenguajes de alto

 

 

 

 

nivel desde hace mucho tiempo, lo nuevo de Ada es que permite que el programador también pueda realizar sobrecarga de operadores, funciones y procedimientos de acuerdo a las necesidades del problema a resolver.

A pesar que los operadores pueden ser sobrecargados, no está permitido el cambiar la sintaxis de la llamada, ni cambiar su nivel de precedencia (jerarquía) con respecto a los demás operadores. Por ejemplo, el operador * siempre debe tener dos parámetros, los cuales pueden ser de cualquier tipo y el resultado puede ser de cualquier tipo. Es decir, la sintaxis (forma de escribir) se mantiene, pero la semántica (acción del operador) puede cambiar.

Dos operadores (funciones, procedimientos) sobrecargados son DIFERENTES. Si no entendemos esto claramente podríamos considerar que la función “*” recién definida es recursiva. Pero puesto que el producto de vectores es diferente (es otro operador con el mismo nombre) al producto de reales, tenemos que A(I)*B(I) no es una llamada recursiva, sino una llamada al producto de reales dentro del producto de vectores. Existe un riesgo de escribir accidentalmente operadores recursivos cuando se está reemplazando un operador preexistente en lugar de crear uno nuevo.

Notemos que dos operadores sobrecargados existen simultáneamente, ninguno de ellos oculta al otro como ocurre cuando, por ejemplo, una variable es declarada con un mismo nombre dentro de un bloque (o loop ) interno. En este último caso la variable más interna “cubre” a la interna. Dos (o más) operadores sobrecargados coexisten y se sabe cuando se está usando uno u otro de acuerdo al contexto. Por ejemplo:

 

 

declare

 

 

 

 

begin

 

 

 

 

 

 

 

 

 

end;


 

 

U,V,W:VECTOR;

A,B,C,E:REAL; I:INTEGER;

 

— se asignan valores adecuados a V, W, B y C. A:=V*W;     — correcto

A:=B*C;                               — correcto

B:=B*REAL(I);                  — correcto

U:=V*W;                              — incorrecto

I:=B*C;                                — incorrecto

E:= 1.0 + A*C;                    — correcto

A:= C*W;                             — incorrecto

 

 

Por último notemos que para efecto de nombrar operadores las mayúsculas y minúsculas no se diferencian, por ejemplo, al sobrecargar el operador or se puede usar “or” o “OR” o incluso “Or”.

 

Ejercicio:

 

  1. Escriba las funciones “+” y “*” para sumar y multiplicar dos valores de tipo complejo.
  2. Escriba las funciones “+” y “*” pasa sumar y multiplicar un complejo con un real. Suponga que el real siempre es el segundo operando.

 

 

 

 

 

Procedimientos

 

Los procedimientos son subprogramas que no retornan un valor y son llamados como instrucciones. La sintaxis es similar a la de las funciones. Se comienza con una palabra reservada (procedure en este caso), luego viene un identificador (que no puede ser un operador) , le sigue la lista de parámetros, no existe un tipo de retorno y el resto es equivalente a lo dicho para las funciones.

Los parámetros pueden ser de tres tipos: in, out o in out. Si no se indica el modo se asume que el parámetro es de entrada (in).

 

1)    in: El parámetro formal es una constante y se permite sólo la lectura del valor asociado al parámetro real.

2)    in out: El parámetro formal es una variable y se permite tanto la lectura como la actualización del valor asociado al parámetro real.

3)    out: El parámetro formal es como una variable, se permite sólo la actualización del valor asociado al parámetro real. No se puede leer su valor.

 

En el caso de las funciones sólo puede haber parámetros de entrada, por lo que los ejemplos anteriores podrían escribirse

 

function SQRT(X: in REAL) return REAL;

function “*” (A,B: in VECTOR) return REAL; Veamos el funcionamiento de los modos in y out.

procedure ADD(A,B: in INTEGER; C: out INTEGER) is

 

 

 

 

 

begin end;


 

 

C:=A+B;

 

con

 

P,Q:INTEGER;

. . . ADD(2+P,37,Q);

 

Al ser llamado el procedimiento ADD, en primer lugar, se evalúan las expresiones 2+P y 37 (en cualquier orden) y los respectivos resultados son pasados a A y B que en adelante se comportan como constantes. Luego se evalúa A+B y el valor se asigna al parámetro formal C. Al terminar el valor de C se asigna a la variable Q. Esto equivale, más o menos, a haber escrito.

 

 

declare

 

 

 

 

begin

 

 

end;


 

 

A: constant INTEGER:= 2+P;     — in B: constant INTEGER := 37; — in C:INTEGER;                                  — out

 

C:=A+B;                                         — cuerpo

Q:=C;                                              — out

 

 

Veamos un ejemplo con modo in out.

 

procedure INCREMENT(X: in out INTEGER) is begin

 

 

end;


X:=X+1;

 

 

con

 

I:INTEGER;

. . . INCREMENT(I);

 

Al llamar a ADD el valor de I es asignado a la variable X, luego el valor de X se incrementa en 1. Al terminar, el nuevo valor de X se asigna al parámetro real I. Esto equivale a haber escrito

 

 

declare begin

 

end;


X:INTEGER.=I; X:=X+1;

I:=X;

 

 

Para cualquier tipo escalar (como es el caso de los INTEGER) el modo in equivale a copiar un valor en la llamada, el modo out equivale a copiar un valor al terminar de ejecutar el procedimiento y el modo in out equivale a la composición de los anteriores.

Si el modo es in, entonces el parámetro real puede ser cualquier cualquiera expresión del tipo indicado en el parámetro formal. En los otros casos el parámetro real debe ser necesariamente una variable del tipo adecuado. La identidad de dicha variable queda determinada cuando se produce la llamada y no puede ser cambiada dentro del procedimiento.  Por ejemplo:

 

I:INTEGER;

A:array (1 .. 10) of INTEGER;

procedure SILLY (X: in out INTEGER) is begin

 

 

 

end;


I:= I+1;

X:=X+1;

 

 

entonces al ejecutar

 

A(5):=1; I:=5; SILLY(A(I));

 

 

 

 

 

 

el valor final en A(5) será 2, I tomará el valor 6, pero A(6) no será afectado. En otras palabras, al momento de la llamada la variable X se asocia a A(5) no a A(I), por lo que cualquier cambio en I no influye en X.

Todos los parámetros (in, out o in out) y los valores de retorno de las funciones deben pertenecer a un cierto tipo (o subtipo) y cumplir todas las restricciones que en la definición del tipo (o subtipo) correspondiente se hayan especificado.

Si un parámetro formal es de tipo array restringido (es decir, sus límites están predeterminados), los límites del parámetro formal deben coincidir, no basta con que el número de componentes sea idéntico en cada una de las dimensiones. Como se puede ver, el mecanismo de paso de parámetros es más riguroso que la instrucción de asignación. Para los arreglos no restringidos los límites se toman de los parámetros reales, incluso para el modo out.

Consideremos otro ejemplo, un procedimiento que resuelve la ecuación ax2 + bx +c = 0

 

procedure QUADRATIC(A,B,C:in REAL; ROOT_1,ROOT_2: out REAL;

OK:out BOOLEAN) is

D:constant REAL:=B**2 – 4*A*C;

 

begin


 

if  D < 0.0 or A = 0.0 then

OK:=FALSE;

return;

 

end if;

ROOT_1:=(-B+SQRT(D))/(2.0*A);

ROOT_2:=(-B-SQRT(D))/(2.0*A); OK:=TRUE;

end QUADRATIC;

 

Si las raíces son reales se entregan en ROOT_1 y ROOT_2 y la variable OK queda con valor TRUE. En cualquier otro caso OK queda en FALSE y ROOT_1 y ROOT_2 quedan indefinidas.

Nótese el uso de la instrucción return. Puesto que un procedimiento no retorna un valor, si se usa la instrucción return (una o más veces) esta no puede ir seguida de una expresión. Cuando se llega a un return, el procedimiento termina y el control pasa a la unidad de donde se hizo la llamada. A diferencia de las funciones, en un procedimiento puede no haber instrucciones return, en este caso el procedimiento se ejecuta hasta su última instrucción y luego el control retorna al punto de llamada.

Recordemos que un parámetro out no es una variable propiamente tal puesto que no es posible leer (utilizar) su valor asociado. Por ejemplo, no podríamos “mejorar” el procedimiento QUADRATIC de la siguiente manera

 

procedure QUADRATIC(A,B,C:in REAL; ROOT_1,ROOT_2: out REAL;

OK:out BOOLEAN) is

D:constant REAL:=B**2 – 4*A*C;

 

begin


 

OK:= (D >= 0.0 and  A /= 0.0);

if  OK then                  — error: NO SE PUEDE LEER UN PARAMETRO OUT!!!

ROOT_1:=(-B+SQRT(D))/(2.0*A);

ROOT_2:=(-B-SQRT(D))/(2.0*A);

 

else

 

end if;


 

return;

 

end QUADRATIC;

 

Los parámetros formales de un subprograma   pueden ser utilizados en la parte procedural como parámetros reales en llamadas a subprogramas. Por ejemplo:

 

procedure P(I:in INTEGER; J:out INTEGER; K:in out INTEGER) is

procedure Q(L: in INTEGER; M: out INTEGER; N: in out  INTEGER) is begin

 

 

 

 

 

begin


 

 

end;


N:=N+L;

M:=N+L;

 

 

Q(I,I,I);                 — error: I es constante. No puede ser un parámetro real para

— parámetros formales out o in out.

Q(I,J,J);                 — error: J es variable especial, no puede ser leída. No puede ser

— un parámetro real para parámetros formales in out.

Q(I,J,K);                — correcto

Q(K,J,K);               — correcto: K no es pasada como variable, sólo se pasa su valor

— al parámetro formal L.

 

 

 

 

end; Entonces

 

declare begin

 

 

end;


 

A,B,C:INTEGER:=0;

 

P(A,B,C);              — correcto

P(A+B,5,C);         — incorrecto: el segundo parámetro debe ser una variable P(3, A,B);   — correcto.

 

 

 

El mecanismo utilizado por Ada para los parámetros de modo out e in out difiere sustancialmente de pasaje de parámetros por referencia de Pascal, o el uso de punteros en C. En estos últimos casos todos los cambios que ocurren en un parámetro se van reflejando paralelamente en la variable referenciada, esto puede ocasionar problemas cuando la variable utilizada como parámetro real por referencia es, además, utilizada como una variable global dentro del subprograma. Se tendría en este caso dos (o más) identificadores para un mismo objeto (lugar físico en la memoria). Esto es parecido a tener dos o más punteros apuntando a un mismo nodo, lo que no es erróneo en sí, pero si no es bien administrado puede conducir a errores. En Ada por otro lado, también es posible usar una variable global dentro de un subprograma, la que a su vez ha sido usada como parámetro real in out. Pero en este caso se tiene dos objetos diferentes (dos lugares físicos de memoria). Si bien en Ada esta situación pareciera ser menos “riesgosa” es preferible evitarla.

 

Ejercicio:

 

  1. Escriba un procedimiento de nombre SWAP para intercambiar los valores de dos variables reales.
  2. Escriba un procedimiento SORT que ordene un arreglo de enteros.

 

 

 

 

 

Parámetros con nombre y por omisión.

 

Hasta ahora las llamadas a los subprogramas se han hecho entregando todos los parámetros en su respectivo orden. También es posible entregar los parámetros indicando su correspondiente nombre (formal). En este caso no es necesario seguir el orden en que aparecen en la especificación del subprograma. Por ejemplo, en lugar de

 

QUADRATIC(L,M,N,P,Q,STATUS); INCREMENT(1);

 

podemos escribir

 

QUADRATIC(B => M,A => L,C => N, ROOT_1 => P, ROOT_2 => Q, OK => STATUS);

INCREMENT(X =>1);

 

o incluso podríamos escribir

 

INCREMENT(X => X);

 

También es posible mezclar la forma posicional de entrega de parámetros con el uso de nombres. En este caso los parámetros sin nombre deben ir al comienzo, y cuando se comienza a utilizar nombres debe continuarse así hasta el final. Por ejemplo, podemos escribir

 

QUADRATIC(L,M,N, ROOT_2 => Q, ROOT_1 => P, OK => STATUS);

 

El dar nombre a los parámetros al momento de la llamada tiene dos usos principales. El primero es para hacer más legible el código, por ejemplo, al escribir

 

QUADRATIC(L,M,N, ROOT_1 => P, ROOT_2 => Q, OK => STATUS);

 

podemos inferir por el nombre del procedimiento y los nombres de los parámetros que en P y Q están las raíces de la ecuación cuadrática.

El segundo uso dice relación con los valores por omisión de los parámetros. A veces ocurre que un parámetro in toma generalmente un mismo valor en cada llamada. En este caso es posible dar un valor por omisión,  el  que  será  pasado  al  parámetro  formal  en  el  momento  de  la  llamada,  a  menos  que  se  de

 

 

 

 

explícitamente un valor.  Por ejemplo, al chequear a los pasajeros que llegan por vuelo internacional a Chile, es de esperar que la mayoría de ellos sean chilenos y que,  además, la mayoría viaja con fines turísticos.

 

type MOTIVO is (NEGOCIOS, TURISMO, OTROS);

type PAIS is (CHILE, CHINA, INGLATERRA, etc);

 

procedure CHEQUEO_DESEMBARCO (NOMBRE: in NAME; ORIGEN: in PAIS;

DESTINO:in PAIS:= CHILE; MOTIVO_DE_VIAJE:in MOTIVO:=TURISMO);

Podríamos hacer las siguientes tipos de llamadas: CHEQUEO_DESEMBARCO(“J.MARTINEZ”, CHINA);

CHEQUEO_DESEMBARCO(“J.ESCOBAR”, INGLATERRA,CHINA);

CHEQUEO_DESEMBARCO(“J.MORA”, CHINA, MOTIVO_DE_VIAJE => NEGOCIOS);

 

Para un buen uso de los valores por omisión, todos aquellos parámetros candidatos con esta características deben colocarse al final de la lista de parámetros en la especificación del subprograma. De esta forma es posible colocar todos aquellos obligatorios en su respectivo orden al comienzo de la llamada. Luego vienen los parámetros con valores por omisión, los cuales pueden no estar presentes en su totalidad. Si uno de ellos no está presente, y no es el último, los demás parámetros deben ser dados con su respectivo nombre.

El valor por omisión puede no ser constante, puede ser en general una expresión del tipo correspondiente, la cual es evaluada cada vez que se realiza una llamada al subprograma. En nuestro ejemplo, el parámetro ORIGEN podría tener como valor por omisión ORIGEN_VUELO(Nro_VUELO), en este caso, si no se da un valor explícito el parámetro tomará el valor entregado por la función ORIGEN_VUELO, la que debería entregar un valor de tipo PAIS.

 

 

Ejercicio:

 

  1. Escriba una función de nombre ADD que suma dos valores enteros y que toma el entero 1 como valor por omisión para el segundo parámetro. ¿De cuántas formas diferentes es posible llamar a la función ADD para que entregue N+1, donde N es el primer parámetro?

 

  1. ¿Qué ocurrirá si todos los parámetros tienen valores por omisión y se hace uso de todos ellos?

 

 

Sobrecarga

 

Ya hemos visto que es posible definir nuevos significados a los operadores predefinidos del lenguaje.  De hecho esta “sobrecarga” semántica se extiende a todos los subprogramas en general.

Un programa sobrecargará a uno definido con anterioridad en la medida que sea lo suficientemente diferente. Por otro lado, si el orden y los tipos de los parámetros y el resultado (para las funciones) es el mismo, entonces en lugar de sobrecarga (overloading) habrá ocultamiento (hiding). Obviamente, un procedimiento no puede ocultar una función  ni una función a un procedimiento. Obsérvese que los nombres y modos de los parámetros, y la presencia o ausencia de valores por omisión no son relevantes al momento de determinar si existe sobrecarga u ocultamiento. Es posible declarar dos o más subprogramas sobrecargados en un  misma parte declarativa.

Los subprogramas y los literales de enumeración pueden sobrecargarse unos a otros. De hecho un literal de enumeración es formalmente una función sin parámetros con resultado del tipo de enumeración. (Por ejemplo, el literal de enumeración SUN formalmente es una función de nombre SUN que no tiene parámetros y  que entrega un valor constante del tipo DAY).

Existen dos tipos de identificadores: los sobrecargables y los no-sobrecargables. En cualquier punto de un programa Ada un identificador hace referencia a un (y sólo un) objeto no-sobrecargable o a uno o más objetos sobrecargables. La declaración de un identificador de un tipo oculta las posibles definiciones previas del otro tipo y no pueden  aparecer en una misma parte declarativa.

 

 

Declaraciones, ámbito (scope) y visibilidad

 

Ya hemos dicho que a veces es necesario entregar la especificación de un subprograma sin su cuerpo. (Recuérdese que el cuerpo incluye la especificación.) Un ejemplo concreto en que esto es necesario se presenta cuando hay recursividad mutua entre subprogramas. Supongamos que queremos declarar dos procedimientos F y G donde cada uno llama al otro. Debido a la regla de la “elaboración lineal de declaraciones” no podemos escribir la llamada a F en el cuerpo de G sin antes haber declarado F y viceversa. Claramente esto es imposible de realizar si escribimos los cuerpos, porque necesariamente uno de ellos habrá de ir segundo. Sin embrago, podemos escribir.

 

procedure F(…);                — declaración de F

 

 

 

 

procedure G(…) is             — cuerpo de G

begin

 

 

end G;


F(…);

 

 

procedure F(…) is              — cuerpo de F repite

begin                                     — su especificación G(…);

end F;

 

y todo funcionará correctamente.

Si la especificación se repite debe haber una total correspondencia. Técnicamente, diremos que las dos especificaciones se corresponden. Pueden haber pequeñas variaciones, por ejemplo los valores por omisión (que deben ser escritos dos veces) no afectan la correspondencia puesto que son evaluadas sólo cuando se llama al subprograma.

A veces, para lograr mayor claridad en el código, es conveniente escribir todas las declaraciones de los subprogramas juntas para que actúen como un sumario; y a continuación se escriben todos los cuerpos.

Los cuerpos de los subprogramas y las demás declaraciones no pueden mezclarse en forma arbitraria. Los cuerpos deben estar a continuación de cualquier otra declaración. De esta forma se evita que las declaraciones “pequeñas” se pierdan entre los cuerpos de los subprogramas.

Puesto que los subprogramas se escriben en las partes declarativas y a su vez poseen partes declarativas, es posible anidar subprogramas. Las normas de ocultamiento explicadas anteriormente para los bloques también rigen para los subprogramas. Consideremos.

 

procedure P is

I:INTEGER:=0;

 

procedure Q is

K:INTEGER:=I; I: INTEGER;   J: INTEGER;

 

 

 

 

 

 

end P;


begin

 

end Q;

. . .


 

. . .

 

 

Como ya hemos visto la variable I interna “oculta” a la externa, sin embargo, la primera sigue existiendo y es posible utilizarla mediante la notación punto, usando como prefijo el nombre del subprograma donde está declarada la variable. En este caso el nombre completo de la variable I externa es P.I, y podríamos, por ejemplo, inicializar J  escribiendo

 

J:INTEGER:=P.I;

 

El nombre completo de la variable I interna es P.Q.I, y podría ser referenciada de esa manera para, por ejemplo, explicitar su naturaleza.

(Recuérdese que los bloques también pueden tener nombres y utilizando la notación punto es posible referenciar cualquier variable que haya sido ocultada por una redefinición. Lo mismo puede hacerse con las iteraciones.)

Como hemos visto los subprogramas pueden alterar variables globales y de este modo generar efectos colaterales. (Un efecto colateral es una consecuencia de la llamada a un subprograma en su entorno y que no está relacionada con el mecanismo de parámetros.) En general se considera que los efectos colaterales son una  mala práctica de programación, especialmente en el caso de las funciones.

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *