Previniendo una avalancha de errores de tiempo de ejecución en Mathematica

Una situación típica que encuentro cuando el portátil crece más allá de un par de funciones: evalúo una expresión, pero en lugar de la respuesta correcta, recibo un pitido seguido de docenas de advertencias inútiles seguidas por “más resultados de … serán suprimidos”

Una cosa que encontré útil: usar funciones internas de “afirmación” similares a las de Python para reforzar la coherencia interna. ¿Algún otro consejo?

Assert[expr_, msg_] := If[Not[expr], Print[msg]; Abort[], None] 

edit 11/14 Una causa general de una avalancha de advertencia es cuando una subexpresión se evalúa como un valor “malo”. Esto hace que la expresión padre evalúe un valor “malo” y esta “maldad” se propaga hasta la raíz. Incorporados evaluados a lo largo del camino notan la maldad y producen advertencias. “Malo” podría significar una expresión con encabezado incorrecto, una lista con un número incorrecto de elementos, una matriz definida negativa en lugar de definida positiva, etc. En general, es algo que no encaja con la semántica de la expresión padre.

Una manera de lidiar con esto es redefinir todas sus funciones para regresar sin evaluar en “entrada incorrecta”. Esto se ocupará de la mayoría de los mensajes producidos por los complementos. Los incorporados que realizan operaciones estructurales como “Parte” aún intentarán evaluar su valor y pueden generar advertencias.

Tener el depurador configurado para “interrumpir los mensajes” previene una avalancha de errores, aunque parece una exageración tenerlo activado todo el tiempo

Como han señalado otros, hay tres formas de lidiar con los errores de manera consistente:

  1. escribir correctamente los parámetros y configurar las condiciones bajo las cuales se ejecutarán las funciones,
  2. manejando correcta y consistentemente con los errores generados, y
  3. simplificando su metodología para aplicar estos pasos.

Como señaló Samsdram , teclear correctamente sus funciones será de gran ayuda. No se olvide de la : forma de Pattern ya que a veces es más fácil express algunos patrones en esta forma, por ejemplo, x:{{_, _} ..} . Obviamente, cuando eso no es suficiente, PatternTest s ( ? ) Y Condition s ( /; ) son el camino a seguir. Samdram lo cubre bastante bien, pero me gustaría añadir que puedes crear tu propia prueba de patrones a través de funciones puras, por ejemplo f[x_?(Head[#]===List&)] es equivalente a f[x_List] . Tenga en cuenta que los paréntesis son necesarios cuando se usa la forma ampersand de funciones puras.

La forma más sencilla de lidiar con los errores generados es, obviamente, Off , o más localmente Quiet . En general, todos podemos estar de acuerdo en que es una mala idea cerrar por completo los mensajes que no queremos, pero Quiet puede ser extremadamente útil cuando sabes que estás haciendo algo que generará quejas, pero por lo demás es correcto.

Throw and Catch tiene su lugar, pero creo que solo deben usarse internamente, y su código debe comunicar los errores a través de las instalaciones de Message . Los mensajes se pueden crear de la misma manera que la configuración de un mensaje de uso. Creo que la clave para una estrategia de error coherente se puede construir usando las funciones Check , CheckAbort , AbortProtect .

Ejemplo

Un ejemplo de mi código es OpenAndRead que protege contra dejar las secuencias abiertas al abortar una operación de lectura, de la siguiente manera:

 OpenAndRead[file_String, fcn_]:= Module[{strm, res}, strm = OpenRead[file]; res = CheckAbort[ fcn[strm], $Aborted ]; Close[strm]; If[res === $Aborted, Abort[], res] (* Edited to allow Abort to propagate *) ] 

que, hasta hace poco, tenía el uso

 fcn[ file_String,  ] := OpenAndRead[file, fcn[#, ]&] fcn[ file_InputStream,  ] :=  

Sin embargo, esto es molesto de hacer todo el tiempo.

Aquí es donde entra en juego la solución de belisarius , al crear un método que puede usar de manera consistente. Desafortunadamente, su solución tiene un defecto fatal: pierde soporte de las funciones de resaltado de syntax. Entonces, aquí hay una alternativa que surgió para engancharme a OpenAndRead desde arriba

 MakeCheckedReader /: SetDelayed[MakeCheckedReader[fcn_Symbol, symbols___], a_] := Quiet[(fcn[file_String, symbols] := OpenAndRead[file, fcn[#, symbols] &]; fcn[file_Symbol, symbols] := a), {RuleDelayed::"rhs"}] 

que tiene uso

 MakeCheckedReader[ myReader, a_, b_ ] := {file$, a, b} (*as an example*) 

Ahora, al verificar la definición de myReader obtienen dos definiciones, como deseamos. Sin embargo, en el cuerpo de la función, el file debe denominarse file$ . (Todavía no he descubierto cómo nombrar el archivo var como me gustaría).

Editar : MakeCheckedReader funciona al no hacer nada en realidad. En cambio, la TagSet ( /: TagSet le dice a Mathematica que cuando se encuentra MakeCheckedReader en el LHS de un SetDelayed entonces reemplácelo con las definiciones de funciones deseadas. Además, tenga en cuenta el uso de Quiet ; de lo contrario, se quejaría de los patrones a_ y b_ aparecen en el lado derecho de la ecuación.

Editar 2 : Leonid señaló cómo poder usar el file no file$ al definir un lector verificado. La solución actualizada es la siguiente:

 MakeCheckedReader /: SetDelayed[MakeCheckedReader[fcn_Symbol, symbols___], a_] := Quiet[(fcn[file_String, symbols] := OpenAndRead[file, fcn[#, symbols] &]; SetDelayed @@ Hold[fcn[file_Symbol, symbols], a]), {RuleDelayed::"rhs"}] 

El razonamiento para el cambio se explica en esta respuesta suya. Al definir myReader , como se myReader arriba, y al verificar su definición, obtenemos

 myReader[file$_String,a_,b_]:=OpenAndRead[file$,myReader[#1,a_,b_]&] myReader[file_Symbol,a_,b_]:={file,a,b} 

Llego tarde a la fiesta, con una respuesta aceptada y todo, pero quiero señalar las definiciones de la forma:

 f[...] := Module[... /; ...] 

son muy útiles en este contexto. Las definiciones de este tipo pueden realizar cálculos complejos antes de finalmente rescatar y decidir que la definición no era aplicable después de todo.

Ilustraré cómo esto se puede usar para implementar varias estrategias de manejo de errores en el contexto de un caso específico de otra pregunta SO . El problema es buscar una lista fija de pares:

 data = {{0, 1}, {1, 2}, {2, 4}, {3, 8}, {4, 15}, {5, 29}, {6, 50}, {7, 88}, {8, 130}, {9, 157}, {10, 180}, {11, 191}, {12, 196}, {13, 199}, {14, 200}}; 

para encontrar el primer par cuyo segundo componente es mayor o igual que un valor especificado. Una vez que se encuentra ese par, se debe devolver su primer componente. Hay muchas maneras de escribir esto en Mathematica, pero aquí hay una:

 f0[x_] := First @ Cases[data, {t_, p_} /; p >= x :> t, {1}, 1] f0[100] (* returns 8 *) 

La pregunta, ahora, es ¿qué ocurre si se llama a la función con un valor que no se puede encontrar?

 f0[1000] error: First::first: {} has a length of zero and no first element. 

El mensaje de error es críptico, en el mejor de los casos, no ofrece pistas sobre cuál es el problema. Si esta función se llamó profundamente en una cadena de llamadas, es probable que ocurra una cascada de errores similarmente opacos.

Existen diversas estrategias para tratar casos tan excepcionales. Una es cambiar el valor de retorno para que un caso de éxito se pueda distinguir de un caso de falla:

 f1[x_] := Cases[data, {t_, p_} /; p >= x :> t, {1}, 1] f1[100] (* returns {8} *) f1[1000] (* returns {} *) 

Sin embargo, existe una fuerte tradición de Mathematica para dejar la expresión original sin modificaciones siempre que una función se evalúe con argumentos fuera de su dominio. Aquí es donde el Módulo [… /; …] patrón puede ayudar:

 f2[x_] := Module[{m}, m = Cases[data, {t_, p_} /; p >= x :> t, {1}, 1]; First[m] /; m =!= {} ] f2[100] (* returns 8 *) f2[1000] (* returns f2[1000] *) 

Tenga en cuenta que f2 se libera completamente si el resultado final es la lista vacía y la expresión original se devuelve sin evaluar, lo que se logra con el simple procedimiento de agregar un /; condición a la expresión final.

Uno podría decidir emitir una advertencia significativa si ocurre el caso “no encontrado”:

 f2[x_] := Null /; Message[f2::err, x] f2::err = "Could not find a value for ``."; 

Con este cambio, se devolverán los mismos valores, pero se emitirá un mensaje de advertencia en el caso “no encontrado”. El valor de retorno nulo en la nueva definición puede ser cualquier cosa, no se usa.

Uno podría decidir que el caso “no encontrado” simplemente no puede ocurrir, excepto en el caso de un código de cliente con errores. En ese caso, uno debería hacer que el cálculo abortara:

 f2[x_] := (Message[f2::err, x]; Abort[]) 

En conclusión, estos patrones son lo suficientemente fáciles de aplicar para que uno pueda tratar con argumentos de funciones que están fuera del dominio definido. Al definir funciones, vale la pena tomar unos minutos para decidir cómo manejar los errores de dominio. Paga en tiempo de depuración reducido. Después de todo, prácticamente todas las funciones son funciones parciales en Mathematica. Considere: una función se puede llamar con una cadena, una imagen, una canción o enjambres itinerantes de nanobots (en Mathematica 9, tal vez).

Una nota final de advertencia … Debo señalar que al definir y redefinir funciones usando múltiples definiciones, es muy fácil obtener resultados inesperados debido a las definiciones “sobrantes”. Como principio general, recomiendo encarecidamente las funciones predefinidas con Clear :

 Clear[f] f[x_] := ... f[x_] := Module[... /; ...] f[x_] := ... /; ... 

El problema aquí es esencialmente uno de tipos. Una función produce una salida incorrecta (tipo incorrecto) que luego se alimenta a muchas funciones posteriores que producen muchos errores. Si bien Mathematica no tiene tipos definidos por el usuario como en otros idiomas, puede hacer la coincidencia de patrones en los argumentos de la función sin demasiado trabajo. Si la coincidencia falla, la función no se evalúa y, por lo tanto, no emite pitidos con errores. La pieza clave de syntax es “/” que va al final de algún código y es seguido por la prueba. Algunos ejemplos de código (y el resultado está debajo).

 Input: Average[x_] := Mean[x] /; VectorQ[x, NumericQ] Average[{1, 2, 3}] Average[$Failed] Output: 2 Average[$Failed] 

Si la prueba es más simple, hay otro símbolo que hace pruebas de patrones similares “?” y va justo después de una discusión en una statement de patrón / función. Otro ejemplo es a continuación.

 Input: square[x_?NumericQ] := x*x square[{1, 2, 3}] square[3] Output: square[{1, 2, 3}] 9 

Puede ayudar a definir una definición de catchall para recoger las condiciones de error e informarlo de una manera significativa:

 f[x_?NumericQ] := x^2; f[args___] := Throw[{"Bad Arguments: ", Hold[f[args]]}] 

Para que tus llamadas de nivel superior puedan usar Catch [], o simplemente puedes permitir que suba.

 In[5]:= f[$Failed] During evaluation of In[5]:= Throw::nocatch: Uncaught Throw[{Bad Args: ,Hold[f[$Failed]]}] returned to top level. >> Out[5]= Hold[Throw[{"Bad Args: ", Hold[f[$Failed]]}]] 

Lo que me gustaría obtener es una forma de definir un procedimiento general para detectar la propagación de errores sin la necesidad de cambiar radicalmente la forma en que escribo las funciones en este momento, preferentemente sin agregar tipado sustancial.

Aquí hay una prueba:

 funcDef = t_[args___] :c-: a_ :> ReleaseHold[Hold[t[args] := Check[a, Print@Hold[a]; Abort[]]]]; Clear@v; v[x_, y_] :c-: Sin[x/y] /. funcDef; ?v v[2, 3] v[2, 0] 

El: c-: es por supuesto Esc c- Esc, un símbolo no utilizado (\ [CircleMinus]), pero cualquiera lo haría.

Salida:

 Global`v v[x_,y_]:=Check[Sin[x/y],Print[Hold[Sin[x/y]]];Abort[]] Out[683]= Sin[2/3] During evaluation of In[679]:= Power::infy: Infinite expression 1/0 encountered. >> During evaluation of In[679]:= Hold[Sin[2/0]] Out[684]= $Aborted 

Lo que cambiamos es

  v[x_, y_] := Sin[x/y] 

por

  v[x_, y_] :c-: Sin[x/y] /. funcDef; 

Esto casi satisface mis premisas.

Editar

Quizás también sea conveniente agregar una definición “desnuda” para la función, que no se someta a la comprobación de errores. Podemos cambiar la regla de funcDef a:

 funcDef = t_[args___] \[CircleMinus] a_ :> {t["nude", args] := a, ReleaseHold[Hold[t[args] := Check[a, Print@Hold[a]; Abort[]]]] }; 

conseguir para

  v[x_, y_] :c-: Sin[x/y] /. funcDef; 

esta salida

 v[nude,x_,y_]:=Sin[x/y] v[x_,y_]:=Check[Sin[x/y],Print[Hold[Sin[x/y]]];Abort[]]