Precedencia de la función en el algoritmo de desviación

9

Estoy trabajando a través del algoritmo Shunting-yard , como lo describe wikipedia.

La descripción del algoritmo cuando se trata con operadores es la siguiente:

Si el token es un operador, o1, entonces:

mientras hay una ficha de operador, o2, en la parte superior de la pila de operadores, y

o1 is left-associative and its precedence is less than or equal to
that of o2, or

o1 is right associative, and has precedence less than that of o2,

luego saque o2 de la pila del operador, en la cola de salida;

empuje o1 en la pila del operador.

Sin embargo, dan el siguiente ejemplo:

Entrada: sin max 2 3 / 3 * 3.1415

Cuando el algoritmo llega al /token, la descripción de lo que debería suceder es la siguiente:

Token |        Action       |   Output (in RPN) |   Operator Stack
...
/     | Pop token to output | 2 3 max           | / sin 
...

Están apareciendo el símbolo de la función maxde la stacky puesta en el queue. Según su algoritmo, esto parecería significar que el token de función es un operador y tiene una precedencia menor que la del /operador.

No hay explicación sobre si este es el caso o no. Entonces, para el Shunting-yardalgoritmo, ¿cuál es la precedencia de una función? ¿Son las funciones derecha o izquierda asociativas? ¿O es wikipedia simplemente incompleto / inexacto?

Destino Espejado
fuente

Respuestas:

5

Creo que la respuesta directa es simplemente que las funciones no son operadores. Desde la página que ha vinculado:

Si el token es un token de función, entonces empújalo en la pila.

Esto es todo lo que necesita decir, ya que el caso de la función (prefijo a postfix) es mucho más simple que el caso del operador (infijo a postfix).

Para las preguntas de seguimiento: las nociones de precedencia y asociatividad solo son necesarias debido a la ambigüedad hereditaria en cualquier expresión con múltiples operadores de infijo. Los tokens de función ya están usando la notación de prefijo, por lo que simplemente no tienen ese problema. No necesita saber si tiene "mayor prioridad" sino maxtiene que darse cuenta de que maxprimero debe evaluarse; ya está claro por el orden de los tokens. Es por eso que las computadoras prefieren la notación pre / postfix para comenzar, y por qué tenemos este algoritmo para convertir infix a pre / postfix.

Es necesario tener algún tipo de regla sobre dónde comienzan y terminan los argumentos de una función cuando no hay paréntesis, por lo que se podría decir que las funciones "tienen prioridad" sobre los operadores o viceversa. Pero a diferencia de los operadores infijos, una sola regla consistente para todas las funciones es suficiente para que sus composiciones sean completamente inequívocas.

Ixrec
fuente
Su algoritmo es correcto, entonces; Es su ejemplo el que es incorrecto. La notación infija debe incluir paréntesis envolviendo las funciones:sin( max( 2 3) / 3 * 3.1415)
MirroredFate
No estoy seguro de si lo llamaría incorrecto, pero este es un argumento fuerte a favor de los idiomas que requieren paréntesis y comas en torno a todas las llamadas a funciones.
Ixrec
Creo que es incorrecto ya que es imposible analizar el infijo usando el algoritmo como lo describen.
MirroredFate
@Ixrec No veo la línea "Si el token es un token de función, entonces empújalo en la pila". en la página de Wikipedia. Puede ser editado por ahora. ¿Pero quiere decir que puedo tratar una función igual que un número en el algoritmo?
Abhinav
3

Hay dos casos diferentes a considerar, dependiendo de la sintaxis de su idioma. Si su idioma usa paréntesis para indicar la aplicación de funciones (por ejemplo f(2+1)), entonces la precedencia es irrelevante. La función debe insertarse en la pila y desplegarse después (para el ejemplo anterior, el resultado es 2 1 + f). Alternativamente, puede tratar la función como un valor y generarla de inmediato, y generar una operación de invocación de función después del paréntesis de cierre (que de lo contrario debería tratarse de la misma manera que cualquier otro paréntesis), por ejemplo f 2 1 + $, dónde $está la operación de invocación de función.

Sin embargo, si su lenguaje no utiliza paréntesis para indicar la invocación de la función, sino que coloca el argumento directamente después de la función sin ninguna puntuación especial (por ejemplo f 2 + 1), como aparentemente es el caso del ejemplo de Wikipedia, entonces las cosas son un poco más complicadas. Tenga en cuenta que la expresión que acabo de dar ás como ejemplo es ambigua: ¿se aplica f a 2 y 1 agregado al resultado, o agregamos 2 y 1 juntos y luego llamamos f con el resultado?

De nuevo, hay dos enfoques. Simplemente puede empujar la función a la pila de operadores cuando la encuentre y asignarle cualquier precedencia que desee. Este es el enfoque más simple, y aparentemente es lo que ha hecho el ejemplo citado. Sin embargo, hay problemas prácticos. En primer lugar, ¿cómo identificas una función? Si tiene un conjunto finito, es fácil, pero si tiene funciones definidas por el usuario, esto significa que su analizador también necesita retroalimentar su entorno, lo que puede volverse desordenado rápidamente. ¿Y cómo manejas las funciones con múltiples argumentos?

Creo que para este estilo de sintaxis, el uso de funciones como valores que son más útiles para un operador de aplicación de funciones tiene mucho más sentido. Luego, simplemente puede inyectar el operador de la aplicación cada vez que lea un valor y lo último que leyó también fue un valor, por lo que no necesita ninguna forma especial de saber qué identificadores son funciones. También puede trabajar con expresiones que devuelven funciones (lo cual es difícil o imposible con el estilo de función como operación). Y esto significa que puede usar curry para manejar múltiples funciones de argumento, lo cual es una simplificación masiva sobre tratar de manejarlas directamente.

Lo único que debe decidir es cuál es la precedencia de la aplicación de funciones. La elección depende de usted, pero en todos los idiomas que he utilizado que funcionan de esta manera, ha sido el operador más vinculante en el lenguaje y ha sido correcto asociativo. (La única variación interesante es Haskell, que además de tener la versión fuertemente vinculante descrita, también tiene un sinónimo con el símbolo $que es el operador más débilmente vinculante en el lenguaje, permitiendo expresiones como f 2 + 1aplicar f a 2 y f $ 2 + 1aplicar a todo el resto de la expresión)

Jules
fuente
3

Implementé las "funciones solicitadas en el patio de maniobras" después de leer el pensamiento original de Dijkstra (páginas 7-11 en el documento del compilador Algol 60, https://ir.cwi.nl/pub/9251 ), y necesito una solución sólida. hizo lo siguiente:

análisis:

  • Presione el descriptor de función
  • Empuje un paréntesis izquierdo "[" al comienzo de los argumentos como su paréntesis de inicio de subexpresión.
  • Leer una secuencia de lista de argumentos equilibrados "(" a ")" de la entrada
  • Empuje esto a la secuencia de token de salida
  • Empuje un soporte derecho al final de los argumentos "]" al igual que su "soporte de cierre compensador"

Infix-to-postfix (patio de maniobras):

  • Agregue otra pila, la pila de funciones, al igual que la pila del operador
  • Al escanear el nombre de una función, inserte la información de la función en la pila de funciones
  • Cuando se ve un corchete derecho de fin de argumento, abre la pila de funciones para generar

Funciona perfectamente en pruebas robustas y escenarios complicados. En mi aplicación (un expansor de expresiones que contiene argumentos de línea de comandos), soporto funciones de múltiples argumentos y un token "," de coma para separarlos, y estos fluyen a través de todo el proceso.

Los ejemplos se parecen a "sqrt (3 ^ 2 + 4 ^ 2)" que se convierte en "3 2 ^ 4 2 ^ + sqrt" y, en última instancia, "5", que es lo que el programa piensa que es el argumento. Es bignum, así que "" binomial (64, 32) / mcd (binomial (64, 32), binomial (63, 31)) "==> cosas grandes ==>" 2 "es útil". 123456 ^ 789 " tiene 40,173 dígitos y el tiempo muestra "evaluación = 0.000390 segundos" en mi MacBookPro, muy rápido.

También lo uso para expandir datos en tablas, y lo encuentro útil. De todos modos, este es mi consejo para manejar cuidadosamente las llamadas a funciones, los argumentos múltiples y el anidamiento profundo en un contexto de patio de maniobras de Dijkstra. Lo hice hoy por pensamiento independiente. No sé si puede haber mejores formas.

Miguel
fuente