java exception visitor callcc

Java: utilizando una RuntimeException para escapar de un visitante



visitor callcc (7)

Estoy tentado poderosamente de usar una excepción sin marcar como construcción de flujo de control de cortocircuito en un programa Java. Espero que alguien aquí me pueda aconsejar sobre una manera mejor y más limpia de manejar este problema.

La idea es que quiero acortar la exploración recursiva de subárboles por un visitante sin tener que marcar una bandera "detener" en cada llamada a un método. Específicamente, estoy construyendo un gráfico de flujo de control usando un visitante sobre el árbol de sintaxis abstracta. Una declaración de return en el AST debe detener la exploración del subárbol y enviar al visitante de vuelta al bloque de ceros if / then o loop más cercano.

La superclase Visitor (de la biblioteca XTC ) define

Object dispatch(Node n)

que llama a través de métodos de reflexión de la forma

Object visitNodeSubtype(Node n)

dispatch no está declarado para lanzar ninguna excepción, así que declaré una clase privada que amplía RuntimeException

private static class ReturnException extends RuntimeException { }

Ahora, el método de visitante para una declaración de retorno se parece a

Object visitReturnStatement(Node n) { // handle return value assignment... // add flow edge to exit node... throw new ReturnException(); }

y cada declaración compuesta necesita manejar la ReturnException

Object visitIfElseStatement(Node n) { Node test = n.getChild(0); Node ifPart = n.getChild(1); Node elsePart = n.getChild(2); // add flow edges to if/else... try{ dispatch(ifPart); } catch( ReturnException e ) { } try{ dispatch(elsePart); } catch( ReturnException e ) { } }

Todo esto funciona bien, excepto:

  1. Puede que olvide tomar una ReturnException alguna parte y el compilador no me avisará.
  2. Me siento sucio

¿Hay una mejor manera de hacer esto? ¿Existe un patrón de Java que desconozco para implementar este tipo de flujo de control no local?

[ACTUALIZAR] Este ejemplo específico resulta algo inválido: la superclase Visitor detecta y envuelve excepciones (incluso RuntimeException ), por lo que el lanzamiento de la excepción realmente no ayuda. Implementé la sugerencia para devolver un tipo de enum de visitReturnStatement . Afortunadamente, esto solo debe verificarse en un pequeño número de lugares (p. Ej., visitCompoundStatement ), por lo que en realidad es un poco menos complicado que lanzar excepciones.

En general, creo que esta sigue siendo una pregunta válida. Aunque quizás, si no está vinculado a una biblioteca de terceros, se puede evitar todo el problema con un diseño sensato.


¿Hay alguna razón por la que no solo estás devolviendo un valor? Como NULL, si realmente no quieres devolver nada? Eso sería mucho más simple y no se arriesgaría a lanzar una excepción de tiempo de ejecución no verificado.


¿Por qué devuelves un valor de tu visitante? El método apropiado del visitante es llamado por las clases que se visitan. Todo el trabajo realizado se encapsula dentro de la propia clase de visitantes, no debe devolver nada y manejar sus propios errores. La única obligación requerida de la clase de llamada es llamar al método visitXXX apropiado, nada más. (Esto supone que está utilizando métodos sobrecargados como en su ejemplo, en lugar de anular el mismo método de visita () para cada tipo).

La clase visitada no debe ser modificada por el visitante o debe tener conocimiento de lo que hace, salvo que permita que la visita se realice. Devolver un valor o lanzar una excepción violaría esto.

Patrón de visitante


¿Tienes que usar Visitor from XTC? Es una interfaz bastante trivial, y puede implementar la suya propia, que puede arrojar ReturnException marcada , que no se olvide de atrapar cuando sea necesario.


Creo que este es un enfoque razonable por algunas razones:

  • Está utilizando un tercero y no puede agregar la excepción marcada
  • Verificar los valores de retorno en todas partes en un gran grupo de visitantes cuando solo es necesario en unos pocos es una carga innecesaria

Además, hay quienes han argumentado que las excepciones no revisadas no son tan malas . Su uso me recuerda la OperationCanceledException de Eclipse, que se usa para borrar las tareas secundarias de larga ejecución.

No es perfecto, pero, si está bien documentado, me parece bien.


No he usado la biblioteca XTC que mencionas. ¿Cómo proporciona la parte complementaria del patrón de visitante: el método de accept(visitor) en los nodos? Incluso si se trata de un despachador basado en la reflexión, todavía debe haber algo que maneje la recursión en el árbol de sintaxis.

Si este código de iteración estructural es fácilmente accesible y no está utilizando el valor de retorno de sus visitXxx(node) , podría explotar un valor de retorno enumerado simple, o incluso un indicador booleano, diciendo accept(visitor) not to recurse en los nodos del niño?

Si:

  • accept(visitor) no está implementado explícitamente por nodos (hay algún campo o reflejo de acceso o reflexión, o los nodos solo implementan una interfaz de obtención de elementos para alguna lógica de control de flujo estándar, o por cualquier otra razón ...), y

  • no quiere meterse con la parte iterativa estructural de la biblioteca, o no está disponible, o no vale la pena el esfuerzo ...

luego, como último recurso, supongo que las excepciones pueden ser su única opción mientras se usa la biblioteca XTC de vanilla.

Sin embargo, es un problema interesante, y puedo entender por qué el flujo de control basado en excepciones te hace sentir sucio ...


Veo las siguientes opciones para ti:

  1. Adelante y defina esa subclase RuntimeException . Verifique si hay problemas serios al detectar su excepción en la llamada más general para dispatch e informar si llega a ser tan lejos.
  2. Haga que el código de procesamiento del nodo devuelva un objeto especial si cree que la búsqueda debe terminar abruptamente. Esto aún te obliga a verificar los valores de retorno en lugar de detectar excepciones, pero es posible que prefieras el aspecto del código de esa manera.
  3. Si la caminata del árbol debe ser detenida por algún factor externo, hágalo todo dentro de un subhebra, y establezca un campo sincronizado en ese objeto para decirle al hilo que se detenga prematuramente.

Lanzar una excepción de tiempo de ejecución como lógica de control es definitivamente una mala idea. La razón por la que te sientes sucio es porque pasas por alto el sistema de tipos, es decir, el tipo de devolución de tus métodos es una mentira.

Tienes varias opciones que son considerablemente más limpias.

1. El Functor de Excepciones

Una buena técnica para usar, cuando estás restringido en las excepciones que puedes lanzar, si no puedes lanzar una excepción marcada, devuelve un objeto que emitirá una excepción marcada. java.util.concurrent.Callable es una instancia de este funtor, por ejemplo.

Vea aquí para una explicación detallada de esta técnica.

Por ejemplo, en lugar de esto:

public Something visit(Node n) { if (n.someting()) return new Something(); else throw new Error("Remember to catch me!"); }

Hacer esto:

public Callable<Something> visit(final Node n) { return new Callable<Something>() { public Something call() throws Exception { if (n.something()) return new Something(); else throw new Exception("Unforgettable!"); } }; }

2. Unión disjunta (también conocida como The Either Bifunctor)

Esta técnica te permite devolver uno de dos tipos diferentes del mismo método. Es un poco como la técnica Tuple<A, B> que la mayoría de la gente está familiarizada para devolver más de un valor de un método. Sin embargo, en lugar de devolver valores de ambos tipos A y B, esto implica devolver un único valor de tipo A o B.

Por ejemplo, dada una enumeración Fail, que podría enumerar los códigos de error aplicables, el ejemplo se convierte en ...

public Either<Fail, Something> visit(final Node n) { if (n.something()) return Either.<Fail, Something>right(new Something()); else return Either.<Fail, Something>left(Fail.DONE); }

Hacer la llamada ahora es mucho más limpio porque no necesita probar / atrapar:

Either<Fail, Something> x = node.dispatch(visitor); for (Something s : x.rightProjection()) { // Do something with Something } for (Fail f : x.leftProjection()) { // Handle failure }

La clase Either no es muy difícil de escribir, pero la biblioteca Functional Java proporciona una implementación con todas las funciones .

3. La opción Mónada

Un poco como un nulo seguro de tipo, esta es una buena técnica para usar cuando no desea devolver un valor para algunas entradas, pero no necesita excepciones o códigos de error. Comúnmente, las personas devolverán lo que se llama un "valor centinela", pero la opción es considerablemente más limpia.

Ahora tienes...

public Option<Something> visit(final Node n) { if (n.something()) return Option.some(new Something()); else return Option.<Something>none(); }

La llamada es agradable y limpia:

Option<Something> s = node.dispatch(visitor)); if (s.isSome()) { Something x = s.some(); // Do something with x. } else { // Handle None. }

Y el hecho de que es una mónada le permite encadenar llamadas sin manejar el valor especial Ninguno:

public Option<Something> visit(final Node n) { return dispatch(getIfPart(n).orElse(dispatch(getElsePart(n))); }

La clase Option es incluso más fácil de escribir que cualquiera de los dos, pero una vez más, la biblioteca Functional Java proporciona una implementación con todas las funciones .

Vea aquí para una discusión detallada de Option y Oither.