• No results found

Clearer Trigger for Impracticability

In document Saving Charitable Settlements (Page 39-43)

En el dominio de la matemática hay varias maneras de representar una operación binaria. Cada una asume una convención acerca de la posición del operador y los operandos. En

notación preja, el operador precede a los operandos. En notación suja16, el operador

sucede a los operandos. En notación inja, el operador se pone entre los dos operandos. Por ejemplo, podemos representar la suma de dos operandos x e y como sigue:

 Preja: +xy  Suja: xy+  Inja: x + y

La notación preja se aproxima a las matemáticas tradicionales. El resultado de evaluar una función f con dos operandos x e y se denota comúnmente como f(x, y). Del mismo modo, la composición funcional se denota como h(f(x, y).

Las notaciones preja y suja permiten una cantidad arbitraria de operandos. Esta es una gran ventaja para denotar funciones multivariables de índole cualquiera. Además, cuando hay varias operaciones, las notaciones preja y suja permiten expresar todas las operaciones sin necesidad de paréntesis. Por ejemplo, la forma suja de x ∗ (y + z) es

yz + x∗.

Para evaluar una expresión preja o suja se utiliza una pila. El algoritmo para evaluar expresiones sujas se describe como sigue:

Algoritmo 2.2 (Evaluación de expresión suja) La entrada del algoritmo es una se- cuencia conteniendo una expresión suja compuesta por operadores y operandos numéri- cos.

La salida es el valor correspondiente al resultado nal.

El algoritmo utiliza una pila para guardar resultados parciales y una variable llamada cha actual o simplemente la cha. La cha contiene el símbolo, operando u operador, que se está procesando.

Algoritmo

1. Repita mientras la cha no esté al nal de la secuencia.

(a) Si la cha es un operador =⇒ saque dos valores de la pila, efectúe la operación dada por el operador leído y almacene el resultado en la pila.

(b) De lo contrario, si la cha es operando =⇒ métalo en la pila.

2. Cuando se alcance el nal de la secuencia, el resultado almacenado en la pila es el resultado de la expresión. Si la pila está vacía o ésta contiene más de dos valores, entonces la expresión suja contenía algún error.

Un algoritmo similar puede deducirse para evaluar expresiones prejas.

La notación suja junto con el algoritmo 2.2 es común en algunas calculadoras de bolsillo, antiguos procesadores y algunos lenguajes de programación.

Por supuesto, la notación inja es la más familiar, pero ésta sólo permite a lo sumo dos operandos. En añadidura, la precedencia de las operaciones puede requerir el uso de paréntesis. A causa de esto, expresiones complicadas son normalmente más largas en forma inja que sus equivalentes prejo o sujo. Este hecho sugiere que manipular y evaluar expresiones injas es más costoso en espacio y en tiempo que en las otras dos notaciones. Posiblemente por esta razón, algunos fabricantes de calculadoras de mano preeren la notación suja.

Existe un algoritmo muy eciente para evaluar una expresión inja, el cual requiere dos pilas. Una pila almacena operandos y resultados parciales y la otra almacena operadores y paréntesis que representan la precedencia.

Antes de explicar el algoritmo requerimos alguna maquinaria de base que efectúe el procesamiento de la cadena de caracteres y que nos indique si estamos en presencia de

un operando u operador. Para ello denimos los posibles valores de chas que pueden encontrarse en una cadena:

108a htipo de cha 108ai≡

enum Token_Type { Value, Operator, Lpar, Rpar, End, Error };

Value representa un operando, Operator representa uno de los operadores +, −, ∗, o /, Lpar y Rpar representan los paréntesis izquierdo y derecho respectivamente, End representa el n de la cadena y, nalmente, Error indica que se encontró un símbolo que no puede ser considerado operando ni operador.

Ahora podemos denir una rutina que lea la cadena y nos indique qué tipo de cha se está leyendo:

108b hrutina lectora de chas 108bi

Token_Type lexer(char *& str, size_t & len) {

hcuerpo de lexer108ci }

lexer() inspecciona secuencialmente la cadena de caracteres contenida en str a partir del símbolo str + len. Al nal de la llamada, str apunta al primer símbolo de la cha. El valor de str puede modicarse, pues puede haber espacios en blanco que no deben procesarse.

lexer() retorna el tipo de cha que se ha detectado. El parámetro len se modica para indicar la longitud en caracteres de la cha actual encontrada.

Los valores de los parámetros se inician del siguiente modo:

108c hcuerpo de lexer 108ci≡ (108b) 108e .

str += len; len = 1;

hignorar espacios en blanco 108di

Por denición, la inspección comienza en str + len. Al inicio, la longitud de la cha que se encuentre será al menos de una unidad.

Iniciada la inspección, la rutina debe ignorar los espacios en blancos. Esto se dene fácilmente como:

108d hignorar espacios en blanco 108di≡ (108c)

while (isblank(*str)) str++;

isblank() es una extensión GNU de la biblioteca estándar del lenguaje C y retorna un valor diferente de cero si el símbolo de parámetro corresponde a un blanco o tabulador.

Por simplicidad, nuestro evaluador sólo acepta los operadores +, −, ∗ y /, los cuales representan las operaciones aritméticas clásicas. De la misma manera, los operandos sólo son números enteros. Salvo un operando, la longitud de una cha es uno. Vericamos, pues, si el símbolo encontrado corresponde a un operador:

108e hcuerpo de lexer 108ci+ (108b) / 108c 109a .

switch (*str) {

case '(': return Lpar; case ')': return Rpar; case '+':

case '-': case '*':

case '/': return Operator; case '\0': return End; }

Si el ujo sale del switch, entonces la única posibilidad correcta es encontrar un operando. Nos cercioramos entonces de que la cadena corresponda a un operando:

109a hcuerpo de lexer108ci+≡ (108b) / 108e 109b .

if (not isdigit(*str)) return Error;

isdigit() es una función de la biblioteca estándar del lenguaje C y retorna un valor diferente de cero si el símbolo de parámetro corresponde a un dígito.

Si el ujo no retorna error, entonces contabilizamos la cantidad de dígitos por la derecha y retornamos el código de cha Value:

109b hcuerpo de lexer108ci+ (108b) / 109a

char* base = str + 1; while (isdigit(*base++))

len++; return Value;

Cuando lexer() retorna un operador, debemos tomar acciones según la precedencia del operador. Para ello elaboramos una función que, dado un operador, determine su prece- dencia:

109c hfunción de precedencia 109ci

unsigned precedence(const char & op) // $ < ( < +- < */ { switch (op) { case '$': return 0; case '(': return 1; case '+': case '-': return 2; case '/': case '*': return 3; } }

precedence() tiene como parámetro un símbolo. Puesto que los operadores son de un solo símbolo, podemos identicar la ocurrencia del operador mediante inspección directa de la cadena.

Hay un operador cticio, $, con la menor precedencia, cuyo rol se explicará más ade- lante. El paréntesis izquierdo se considera un operador en el siguiente nivel de precedencia. La suma y resta ocupan el próximo nivel de precedencia. Finalmente, los operadores con más precedencia son el producto y la división.

Si lexer() retorna Value, entonces necesitamos obtener una cadena de caracteres que contenga el número. Tal función la efectuamos del siguiente modo:

109d hfunción convertir cha a cadena109di

char * str_to_token(char * token_str, const size_t & len) {

static char buffer[256];

strncpy(buffer, token_str, len); buffer[len] = '\0';

return buffer; }

str_to_token() extrae una copia del número contenido en token_str. El algoritmo de evaluación usará dos pilas como sigue:

110a hpilas de evaluación 110ai≡ (111a)

ArrayStack<int> val_stack; ArrayStack<char> op_stack;

Uses ArrayStack 101a.

El algoritmo efectúa operaciones en línea conforme analiza la cadena de entrada. La operación fundamental es sacar dos operandos de la pila val_stack y un operador de la pila op_stack; luego, efectuar la operación y, nalmente, almacenar el resultado de nuevo en la pila val_stack. Este patrón se modulariza en la siguiente rutina:

110b hfunción de ejecución de operación en pilas 110bi≡

void apply(ArrayStack<int>& val_stack, ArrayStack<char>& op_stack) {

const char the_operator = op_stack.pop(); const int right_operand = val_stack.pop(); const int left_operand = val_stack.pop(); int result;

switch (the_operator) {

case '+': result = left_operand + right_operand; break; case '-': result = left_operand - right_operand; break; case '*': result = left_operand * right_operand; break; case '/': result = left_operand / right_operand; break; }

val_stack.push(result); }

Uses ArrayStack 101a.

apply() asume que el operando derecho fue insertado en la pila después del izquierdo. Esto es lo normal si la cadena de entrada se analiza de izquierda a derecha.

Ahora disponemos de las herramientas necesarias para desarrollar una rutina que evalúe una expresión inja. Nuestra rutina se llamará eval() y tendrá la siguiente estructura: 110c hfunción de evaluación de expresión inja 110ci≡

int eval(char* input) {

hvariables de eval111ai

hinicialización de eval111bi while (true)

{

hleer próxima cha 111ci switch (current_token)

{

case Value:

hprocesar operando 111di case Lpar:

hprocesar paréntesis izquierdo 112bi case Operator:

case Rpar:

hprocesar paréntesis derecho 112ci case End:

hprocesar n de entrada 112di

} } }

input es la cadena de caracteres que contiene la expresión inja. eval() requiere siguientes variables:

111a hvariables de eval111ai≡ (110c)

hpilas de evaluación 110ai Token_Type current_token; size_t token_len = 0;

current_token es la cha actual observada en la cadena de entrada y token_len es la longitud de la cha en caracteres. Inicialmente no hay ninguna cha, por lo que token_len debe ser cero.

La pila de operadores requiere un centinela especial que sea de la menor precedencia posible. Tal centinela es el valor `$` que se inserta inicialmente en op_stack:

111b hinicialización de eval111bi≡ (110c)

op_stack.push('$');

Después de la inicialización, la rutina comienza a iterar. El cuerpo iterativo lee las chas presentes en la entrada y luego procesa la cha según su tipo. Leer la cha consiste simplemente en una invocación a lexer():

111c hleer próxima cha111ci≡ (110c)

current_token = lexer(input, token_len);

Cuando la cha es un operando, lo introducimos en la pila de operandos:

111d hprocesar operando 111di (110c)

{

const int operand = atoi(str_to_token(input, token_len)); val_stack.push(operand);

break; }

Previamente hay que efectuar la conversión de la cadena de caracteres a su representación numérica.

Podemos introducir el operador en la pila, pero antes podemos ejecutar todas las operaciones que están en la pila de operadores con mayor o igual precedencia que el operador actual. Esta alternativa es preferible porque disminuye el consumo en espacio de las pilas.

Para ejemplicar, consideremos la expresión 100 − 20 ∗ 5 + 7 y supongamos que la cha actual es el operador de resta. En este momento, val_stack contiene < 100 > y op_stack

<$ > y no podemos efectuar ninguna operación porque el operador cticio $ tiene menor

precedencia que el operador −. Cuando encontramos el operador ∗, tampoco podemos efectuar ninguna operación porque el producto tiene mayor precedencia que la resta. Fi- nalmente, cuando nos encontramos el operador +, val_stack contiene < 100, 20, 5 > y op_stack < $, −, ∗ >. Aquí efectuamos el producto, pues éste tiene mayor precedencia que la suma, lo que nos deja las pilas en los estados < 100, 100 > y < $, − > respectivamente. Posteriormente, efectuamos la resta, pues ésta tiene la misma precedencia que la suma. Al

término de esta operación introducimos el operador + en la pila lo que nos deja el estado de las pilas en < 0 > y < $, + >.

La conducta anterior se modeliza entonces bajo la siguiente forma:

112a hprocesar operador 112ai≡ (110c)

{

while (precedence(op_stack.top()) >= precedence(*input)) apply(val_stack, op_stack);

op_stack.push(*input); // introducir operador en op_stack break;

}

Los paréntesis en la expresiones injas son utilizados para modicar la precedencia por omisión de los operadores. Cuando la cha sea un paréntesis izquierdo, lo introducimos en la pila de operadores:

112b hprocesar paréntesis izquierdo 112bi≡ (110c)

{

op_stack.push(*input); // introducir parentesis en op_stack break;

}

El paréntesis derecho indicará el nal de una expresión que debe ser evaluada. Cuando encontremos el paréntesis derecho, ejecutaremos todas las operaciones entre los paréntesis. Para ello llamamos sucesivamente apply() hasta que el paréntesis izquierdo sea encontrado en el tope de la pila:

112c hprocesar paréntesis derecho 112ci≡ (110c)

{

while (op_stack.top() != '(') apply(val_stack, op_stack);

op_stack.pop(); /* saca el parentesis izquierdo */ break;

}

El n de la entrada lo indica lexer() cuando retorna el valor End. En este momento, si la expresión fue correcta, nos falta evaluar las operaciones que se encuentran en la pila de operandos, lo cual se dene de la siguiente manera:

112d hprocesar n de entrada 112di≡ (110c)

{

while (op_stack.top() != '$') apply(val_stack, op_stack); op_stack.pop(); // debe ser '$' const int ret_val = val_stack.pop(); return ret_val;

}

In document Saving Charitable Settlements (Page 39-43)

Related documents