Parâmetro processual - Procedural parameter

Na computação , um parâmetro procedural é um parâmetro de um procedimento que é ele próprio um procedimento.

Este conceito é uma ferramenta de programação extremamente poderosa e versátil , pois permite aos programadores modificar certas etapas de um procedimento de biblioteca de maneiras arbitrariamente complicadas, sem ter que entender ou modificar o código desse procedimento.

Esta ferramenta é particularmente eficaz e conveniente em linguagens que suportam definições de funções locais , como Pascal e do moderno dialeto GNU de C . É ainda mais quando encerramentos de funções estão disponíveis. A mesma funcionalidade (e mais) é fornecida por objetos em linguagens de programação orientadas a objetos , mas a um custo significativamente mais alto.

Os parâmetros procedimentais estão um tanto relacionados aos conceitos de função de primeira classe e função anônima , mas são distintos deles. Esses dois conceitos têm mais a ver com a forma como as funções são definidas, do que como são usadas.

Conceito básico

Na maioria das linguagens que fornecem esse recurso, um parâmetro procedural f de uma sub-rotina P pode ser chamado dentro do corpo de P como se fosse um procedimento comum:

procedure P(f):
    return f(6,3) * f(2,1)

Ao chamar a sub-rotina P , deve-se dar a ela um argumento, que deve ser alguma função previamente definida compatível com a forma como P usa seu parâmetro f . Por exemplo, se definirmos

procedure plus(x, y):
    return x + y

então podemos chamar P ( mais ), e o resultado será mais (6,3) * mais (2,1) = (6 + 3) * (2 + 1) = 27. Por outro lado, se definirmos

procedure quot(u, v):
    return u/v

então a chamada P ( quot ) retornará quot (6,3) * quot (2,1) = (6/3) * (2/1) = 4. Finalmente, se definirmos

procedure evil(z)
    return z + 100

então, a chamada P ( mal ) não fará muito sentido e pode ser sinalizada como um erro.

Detalhes sintáticos

Algumas linguagens de programação que possuem esse recurso podem permitir ou exigir uma declaração de tipo completa para cada parâmetro procedural f , incluindo o número e tipo de seus argumentos e o tipo de seu resultado, se houver. Por exemplo, na linguagem de programação C, o exemplo acima pode ser escrito como

int P(int (*f)(int a, int b)) {
    return f(6,3) * f(2,1);
}

Em princípio, a função real actf que é passada como argumento quando P é chamada deve ser compatível com o tipo declarado do parâmetro do procedimento f . Isso geralmente significa que actf e f devem retornar o mesmo tipo de resultado, devem ter o mesmo número de argumentos e os argumentos correspondentes devem ter o mesmo tipo. Os nomes dos argumentos não precisam ser os mesmos, entretanto, conforme mostrado pelos exemplos de adição e aspas acima. No entanto, algumas linguagens de programação podem ser mais restritivas ou mais liberais a esse respeito.

Escopo

Em linguagens que permitem parâmetros procedurais, as regras de escopo são geralmente definidas de forma que os parâmetros procedimentais sejam executados em seu escopo nativo. Mais precisamente, suponha que a função actf seja passada como argumento para P , como seu parâmetro procedural f ; e f é então chamado de dentro do corpo de P . Enquanto actf está sendo executado, ele vê o ambiente de sua definição.

A implementação dessas regras de escopo não é trivial. No momento em que actf é finalmente executado, os registros de ativação onde suas variáveis ​​de ambiente residem podem estar arbitrariamente no fundo da pilha. Este é o chamado problema de funarg para baixo .

Exemplo: classificação de inserção genérica

O conceito de parâmetro procedural é melhor explicado por exemplos. Uma aplicação típica é a seguinte implementação genérica do algoritmo de classificação por inserção , que leva dois parâmetros inteiros a , be dois parâmetros procedurais prec , swap :

procedure isort(a, b, prec, swap):
    integer i, j;
    ia;
    while ib do
        ji;
        while j > a and prec(j, j−1) do
            swap(j, j−1);
            jj−1;
        ii+1;

Este procedimento pode ser usado para classificar os elementos x [ a ] até x [ b ] de algum array x , de tipo arbitrário , em uma ordem especificada pelo usuário. Os parâmetros prec e swap devem ser duas funções , definidas pelo cliente , ambos levando dois inteiros r , s entre a e b . A função prec deve retornar verdadeiro se e somente se os dados armazenados em x [ r ] devem preceder os dados armazenados em x [ s ], na ordem definida pelo cliente. A troca de função deve trocar o conteúdo de x [ r ] e x [ s ], e voltar sem resultado.

Pela escolha adequada das funções prec e swap , o mesmo procedimento isort pode ser usado para reordenar arrays de qualquer tipo de dados, armazenados em qualquer meio e organizados em qualquer estrutura de dados que forneça acesso indexado a elementos de array individuais. (Observe, no entanto, que existem algoritmos de classificação que são muito mais eficientes do que a classificação por inserção para grandes matrizes.)

Classificando números de ponto flutuante

Por exemplo, podemos classificar uma matriz z de 20 números de ponto flutuante, z [1] a z [20] em ordem crescente, chamando isort (1, 20, zprec , zswap ), onde as funções zprec e zswap são definidas como

procedure zprec(r, s):
    return (z[r] < z[s]);

procedure zswap(r, s):
    float t;
    tz[r];
    z[r] ← z[s];
    z[s] ← t

Classificação de linhas de uma matriz

Para outro exemplo, seja M uma matriz de inteiros com 10 linhas e 20 colunas, com índices começando em 1. O código a seguir reorganizará os elementos em cada linha para que todos os valores pares venham antes de todos os valores ímpares:

integer i
procedure eoprec(r, s):
    return (M[i, r] mod 2) < (M[i, s] mod 2);

procedure eoswap(r, s):
    integer t;
    tM[i,r];
    M[i,r] ← M[i,s];
    M[i,s] ← t;

for i from 1 to 10 do
    isort(1, 20, eoprec, eoswap);

Observe que os efeitos de eoprec e eoswap dependem do número da linha i , mas o procedimento isort não precisa saber disso.

Procedimento de classificação de vetores

O exemplo a seguir usa isort para definir um procedimento vecsort que leva um inteiro n e um vetor inteiro v com os elementos v [0] a v [ n −1] e os classifica em ordem crescente ou decrescente, dependendo se um terceiro parâmetro é incrementado é verdadeiro ou falso , respectivamente:

procedure vecsort(n, v, incr):

    procedure vprec(r, s):
        if incr then
            return v[r] < v[s];
        else
            return v[r] > v[s];

    procedure vswap(r, s):
        integer t;
        tv[r];
        v[r] ← v[s];
        v[s] ← t

    isort(0, n−1, vprec, vswap);

Observe o uso de definições de função aninhadas para obter uma função vprec cujo efeito depende do incremento do parâmetro passado para vecsort . Em linguagens que não permitem definições de funções aninhadas, como o padrão C, a obtenção desse efeito exigiria um código bastante complicado e / ou thread-inseguro .


Exemplo: mesclando duas sequências

O exemplo a seguir ilustra o uso de parâmetros procedimentais para processar estruturas de dados abstratas independentemente de sua implementação concreta. O problema é mesclar duas sequências ordenadas de registros em uma única sequência ordenada, onde a natureza dos registros e o critério de ordenação podem ser escolhidos pelo cliente. A implementação a seguir assume apenas que cada registro pode ser referenciado por um endereço de memória, e há um "endereço nulo" Λ que não é o endereço de nenhum registro válido. O cliente deve fornecer os endereços A , B dos primeiros registros em cada sequência e as funções prec , next e append , a serem descritas posteriormente.

procedure merge(A, B, prec, nextA, appendA, nextB, appendB):
    address ini, fin, t
    ini ← Λ; fin ← Λ
    while A ≠ Λ or B ≠ Λ do
        if B = Λ or (A ≠ Λ and B ≠ Λ and prec(A, B)) then
            tnextA(A)
            fin ← appendA(A, fin); if ini = Λ then inifin
            At
        else
            tnextB(B)
            finappendB(B, fin); if ini = Λ then inifin
            Bt
    return ini

A função prec deve levar os endereços r , s de dois registros, um de cada seqüência, e retornar verdadeiro se o primeiro registro vier antes do outro na seqüência de saída. A função nextA deve pegar o endereço de um registro da primeira sequência e retornar o endereço do próximo registro na mesma sequência, ou Λ se não houver nenhum. A função appendA deve anexar o primeiro registro da sequência A à sequência de saída; seus argumentos são o endereço A do registro a ser anexado e o endereço fin do último registro da lista de saída (ou Λ se a lista ainda estiver vazia). O procedimento appendA deve retornar o endereço atualizado do elemento final da lista de saída. Os procedimentos nextB e appendB são análogos para a outra sequência de entrada.

Mesclando listas vinculadas

Para ilustrar o uso do procedimento de merge genérico, aqui está o código para mesclar dois simples listas ligadas , começando com nós em endereços R , S . Aqui, assumimos que cada registro x contém um campo inteiro x . INFO e um campo de endereço x . NEXT que aponta para o próximo nó; onde os campos de informações estão em ordem crescente em cada lista. As listas de entrada são desmontadas pela fusão e seus nós são usados ​​para construir a lista de saída.

procedure listmerge(R, S):

    procedure prec(r, s):
        return r.INFO < s.INFO

    procedure next(x):
        return x.NEXT

    procedure append(x, fin)
        if fin ≠ Λ then fin.NEXTx
        x.NEXT ← Λ
        return x
     
    return merge(R, S, prec, next, append, next, append)

Mesclando vetores

O código a seguir ilustra a independência do procedimento de mesclagem genérico da representação real das sequências. Ele funde os elementos de duas matrizes ordinárias U [0] a U [ m −1] e V [0] a V [ n −1] de números de ponto flutuante, em ordem decrescente. As matrizes de entrada não são modificadas e a sequência mesclada de valores é armazenada em um terceiro vetor W [0] a W [ m + n −1]. Como na linguagem de programação C, assumimos que a expressão "& V " produz o endereço da variável V , "* p " produz a variável cujo endereço é o valor de p , e que "& ( X [ i ])" é equivalente a "& ( X [0]) + i " para qualquer matriz X e qualquer inteiro i .

procedure arraymerge(U, m, V, n, W):

    procedure prec(r, s):
        return (*r) > (*s)

    procedure nextU(x):
        if x = &(U[m−1]) then return Λ else return x + 1

    procedure nextV(x):
        if x = &(V[n−1]) then return Λ else return x + 1

    procedure append(x, fin)
        if fin = Λ then fin ← &(W[0])
        (*fin) ← (*x)
        return fin + 1
        
    if m = 0 then U ← Λ
    if n = 0 then V ← Λ
    return merge(U, V, prec, nextU, append, nextV, append)

Exemplo: integral definida

Integrando em um intervalo

O procedimento a seguir calcula a integral aproximada f ( x ) d x de uma dada função de valor real f em um determinado intervalo [ a , b ] da reta real . O método numérico usado é a regra do trapézio com um determinado número n de passos; os números reais são aproximados por números de ponto flutuante.

procedure Intg(f, a, b, n):
    float t, x, s; integer i
    if b = a then return 0
    xa; sf(a) / 2;
    for i from 1 to n−1 do
        ti/(n+1); x ← (1−t) * a + t * b;
        ss + f(x)
    sf(b) / 2
    return (ba) * s / n

Integrando em um disco

Considere agora o problema de integrar uma determinada função , com dois argumentos, sobre um disco com determinado centro ( ) e dado raio . Este problema pode ser reduzido a duas integrais de variável única aninhadas pela mudança de variáveis

O código a seguir implementa a fórmula do lado direito :

procedure DiskIntg(g, xc, yc, R, n)

    procedure gring(z):

        procedure gpolar(t):
            float x, y
            xxc + z * cos(t)
            yyc + z * sin(t)
            return g(x, y)

        integer mround(n*z/R)
        return z * Intg(gpolar, 0, 2*π, m)

    return Intg(gring, 0, R, n)

Este código usa o procedimento de integração Intg em dois níveis. O nível externo (última linha) usa Intg para calcular a integral de para variando de 0 a . O nível interno (penúltima linha) é definido como sendo a linha integral de sobre o círculo com centro e raio .

História

Os parâmetros procedimentais foram inventados antes da era dos computadores eletrônicos, pelo matemático Alonzo Church , como parte de seu modelo de cálculo lambda .

Parâmetros procedurais como um recurso de linguagem de programação foram introduzidos pelo ALGOL 60 . Na verdade, o ALGOL 60 tinha um poderoso mecanismo de passagem de parâmetros " chamada por nome " que poderia simplificar alguns usos de parâmetros procedurais; veja o Dispositivo de Jensen .

Parâmetros procedurais foram uma característica essencial da linguagem de programação LISP , que também introduziu o conceito de fechamento de função ou funarg . A linguagem de programação C permite que ponteiros de função sejam passados ​​como parâmetros, que realizam o mesmo fim e são freqüentemente usados ​​como retornos de chamada em programação orientada a eventos e como manipuladores de erros. No entanto, apenas alguns compiladores C modernos permitem definições de funções aninhadas, de modo que seus outros usos são relativamente incomuns. Parâmetros procedurais foram fornecidos também em Pascal, junto com definições de procedimentos aninhados; no entanto, como o Pascal padrão não permitia a compilação separada, o recurso também era pouco usado nessa linguagem.

Veja também