【问题标题】:What are the best practices / most common / idiomatic ways to report errors in Mathematica?在 Mathematica 中报告错误的最佳实践/最常见/惯用的方法是什么?
【发布时间】:2024-01-18 11:32:01
【问题描述】:

特别是,报告函数参数不正确的错误的最佳方法是什么?使用模式检查正确的参数相对容易,但如何报告特定情况的信息性错误消息并不明显。

当参数不正确时,大多数内置函数将返回未计算值,同时打印一条信息性错误消息。

需要考虑的要点:

  • 易于添加到已实现的函数中而无需进行太多更改,易于将错误报告与函数实际执行的操作分开
  • 强大且可维护
  • 与带有选项的函数配合得很好

注意:我可以想出几种方法来实现这种行为。但是,我不想从头开始并从错误中学习,而是想利用社区的经验和知识来学习一些经过时间考验的方法。

【问题讨论】:

  • 我昨天想问这个问题,但认为它会被关闭。错误处理和报告也不在任何主要的数学书籍中。 - 这不是你想添加到一次性程序中的东西。
  • @ndroock1 当您编写供他人使用的包时,这很重要。我希望你问过这个问题:)

标签: programming message-coding


【解决方案1】:

我觉得这是列出一些错误检查技术的好机会。我将讨论我知道的那些,请随时编辑这篇文章并添加更多内容。我认为这里要回答的主要问题是我们希望函数在出错的情况下返回什么,以及如何在技术上做到这一点。

出错时返回什么

我可以在这里看到 3 种不同的选择

该函数发出错误消息并返回未评估的结果。

这个更适合符号环境,并且对应于大多数 Mathematica 函数所具有的语义 w.r.t.错误。这个想法是符号环境比更传统的环境更宽容,并且通过返回相同的表达式,我们表明 Mathematica 根本不知道如何处理它。这为稍后执行函数调用留下了机会,例如当一些符号参数获取数字或其他值时。

从技术上讲,这可以通过利用条件模式的语义来实现。这是一个简单的例子:

ClearAll[f];
f::badargs = "A single argument of type integer was expected";
f[x_Integer] := x^2;
f[args___] := "nothing" /; Message[f::badargs]

f[2]
4
f[1, 2]
f::badargs: A single argument of type integer was expected

f[1, 2]  

这个想法是,最后,模式被认为不匹配(因为条件中的测试不会评估为显式 True),但Message 在此过程中被调用。这个技巧也可以用于多个定义——因为最后的模式被认为是不匹配的,模式匹配器继续测试DownValues 列表中的其他规则。这可能是可取的,也可能不是,取决于具体情况。

函数的性质使得返回$Failed(显式失败)更合适。

这可能适用的典型示例是无法写入文件或无法在磁盘上找到文件。一般来说,我认为这种行为最适合用于软件工程的函数(换句话说,那些不将其结果直接传递到另一个函数但调用其他应该返回的函数并形成执行堆栈的函数)。如果确定给定函数失败后继续执行没有意义,则应返回$Failed(可能还会发出错误消息)。

返回$Failed 也可以作为一种预防措施,以防止由于实现更改而导致的偶尔出现的回归错误,例如,当某些函数已被重构为接受或返回不同数量和/或类型的参数时,但要调用的函数它没有及时更新。在像 Java 这样的强类型语言中,编译器会捕获这类错误。在 Mathematica 中,这是程序员的任务。对于包内的某些内部函数,在这种情况下返回$Failed 似乎比发出错误消息并返回未评估更合适。此外,在实践中,它更容易 - 很少有人会向其所有内部函数提供错误消息(无论如何这也可能是一个坏主意,因为用户不应该关心代码的一些内部问题),同时返回$Failed 快速而直接。当许多辅助函数返回$Failed而不是保持沉默时,调试就容易多了。

从技术上讲,最简单的方法是使用 Return 从函数体内显式返回 $Failed,例如在此示例中的自定义文件导入函数:

ClearAll[importFile];
Options[importFile] = {ImportDirectory :> "C:\\Temp"};
importFile::nofile = "File `1` was not found during import";
importFile[filename_String, opts : OptionsPattern[]] :=
 Module[{fullName = 
     getFullFileName[OptionValue[ImportDirectory], filename], result},
   result = Quiet@Import[fullName, "Text"];
   If[result === $Failed,
      Message[importFile::nofile, Style[fullName, Red]];
      Return[$Failed],
      (* else *)
      result
]];

但是,通常以@Verbeia 的答案中概述的方式使用模式匹配器更方便。对于无效输入参数的情况,这是最简单的。例如,我们可以很容易地向上面的函数添加一条包罗万象的规则,如下所示:

importFile[___] := (Message[importFile::badargs]; $Failed)

有更多有趣的方式使用模式匹配器,见下文。

这里的最后一条评论是,每个可能返回$Failed 的链接函数的一个问题是需要大量If[f[arg]===$Failed, Return[$Failed],do-somthing] 类型的样板代码。我最终使用这个更高级别的函数来解决这个问题:

chainIfNotFailed[funs_List, expr_] :=
 Module[{failException},
  Catch[
   Fold[
    If[#1 === $Failed,
      Throw[$Failed, failException],
      #2[#1]] &,
    expr,
    funs], failException]];

它通过异常停止执行,并在任何中间函数调用导致$Failed 时立即返回$Failed。例如:

chainIfNotFailed[{Cos, #^2 &, Sin}, x]
Sin[Cos[x]^2]
chainIfNotFailed[{Cos, $Failed &, Sin}, x]
$Failed

可以使用Throw 抛出异常,而不是返回$Failed

这种方法在 IMO 几乎从不适合向用户公开的*功能。 Mathematica 异常没有被检查(在 Java 中被检查的异常的意义上),并且 mma 不是强类型的,所以没有好的语言支持的方式告诉用户在某些事件中可能会抛出异常。但是,它对于包中的内部函数可能非常有用。这是一个玩具示例:

ClearAll[ff, gg, hh, failTag];
hh::fail = "The function failed. The failure occured in function `1` ";

ff[x_Integer] := x^2 + 1;
ff[args___] := Throw[$Failed, failTag[ff]];

gg[x_?EvenQ] := x/2;
gg[args___] := Throw[$Failed, failTag[gg]];

hh[args__] :=
  Module[{result},
   Catch[result = 
     gg[ff[args]], _failTag, (Message[hh::fail, Style[First@#2, Red]];
      #1) &]];

以及一些使用示例:

 hh[1]
 1
hh[2]
hh::fail: The function failed.
The failure occured in function gg 
$Failed
hh[1,3]
hh::fail: The function failed. 
The failure occured in function ff 
$Failed

我发现这种技术非常有用,因为如果始终使用它,它可以非常快速地定位错误源。这在几个月后使用代码时特别有用,因为您不再记得所有细节。

什么不能返回

  • 不要返回Null。这是模棱两可的,因为Null 对于某些函数可能是有意义的输出,不一定是错误。

  • 不返回使用Print 打印的错误消息(从而返回Null)。

  • 不要返回Message[f::name](再次返回Null)。

  • 1234563在 mma 中(可能只是我。但同时,我在 C 和 Java 中使用了很多)。我的猜测是,这在更强大(也可能是静态)类型的语言中变得更有益。

使用模式匹配器简化错误处理代码

@Verbeia 的回答中已经描述了其中一个主要机制 - 使用模式的相对普遍性。关于这一点,我可以指出例如this 包,我经常使用这种技术,作为这种技术的工作示例的额外来源。

多条消息问题

该技术本身可用于上面讨论的所有 3 种返回情况。但是,对于返回未计算函数的第一种情况,有一些微妙之处。一个是,如果您有多个“重叠”模式的错误消息,您可能希望“短路”匹配失败。我将说明这个问题,借用here 的讨论。考虑一个函数:

ClearAll[foo]
foo::toolong = "List is too long";
foo::nolist = "First argument is not a list";
foo::nargs = "foo called with `1` argument(s); 2 expected";
foo[x_List /; Length[x] < 3, y_] := {#, y} & /@ x
foo[x_List, y_] /; Message[foo::toolong] = Null
foo[x_, y_] /; Message[foo::nolist] = Null
foo[x___] /; Message[foo::nargs, Length[{x}]] = Null

我们叫错了:

foo[{1,2,3},3]
foo::toolong: List is too long
foo::nolist: First argument is not a list
foo::nargs: foo called with 2 argument(s); 2 expected
foo[{1,2,3},3]

显然,生成的消息是相互矛盾的,而不是我们想要的。原因是,由于在这种方法中错误检查规则被认为是不匹配的,如果模式构造得不够仔细,模式匹配器会继续并尝试多个错误检查规则。避免这种情况的一种方法是仔细构建模式,使它们不会重叠(互斥)。在提到的线程中讨论了其他一些方法。我只是想提请注意这种情况。请注意,当显式返回 $Failed 或抛出异常时,这不是问题。

ModuleBlockWith 与共享局部变量一起使用

此技术基于具有条件模式的定义的语义,涉及范围构造ModuleBlockWith。提到了here。这种构造类型的一大优点是它允许执行一些计算,然后才在函数评估的中间某处确定错误的事实。然而,模式匹配器会将其解释为模式不匹配,并继续执行其他规则,就好像从未发生过针对该规则的主体评估(即,如果您没有引入副作用)。下面是一个查找文件“短名称”的函数示例,但会检查文件是否属于给定目录(否定目录被视为失败):

isHead[h_List, x_List] := SameQ[h, Take[x, Length[h]]];

shortName::incns = "The file `2` is not in the directory `1`";
shortName[root_String, file_String] :=
  With[{fsplit = FileNameSplit[file], rsplit = FileNameSplit[root]},
    FileNameJoin[Drop[fsplit, Length[rsplit]]] /;isHead[rsplit, fsplit]];

shortName[root_String, file_String]:= ""/;Message[shortName::incns,root,file];

shortName[___] := Throw[$Failed,shortName];

(在我使用它的上下文中,它适合Throw 异常)。我觉得这是一个非常强大的技术,并且经常使用它。在this 线程中,我给出了一些我知道的使用示例的指针。

带有选项的函数

函数接收选项的情况在 IMO 中并不是很特殊,因为到目前为止我所说的任何内容也适用于它们。一件很难的事情是错误检查传递的选项。我尝试使用包CheckOptionsPackageOptionChecks(可以找到here)来自动化这个过程。我时不时会使用这些,但不能说它们对其他人是否有用。

元编程和自动化

您可能已经注意到许多错误检查代码是重复的(样板代码)。尝试自动执行错误检查定义的过程似乎是一件很自然的事情。我将举一个例子来说明 mma 元编程的强大功能,它通过对上面讨论的带有内部异常的玩具示例进行自动错误检查。

以下是自动化流程的功能:

ClearAll[setConsistencyChecks];
Attributes[setConsistencyChecks] = {Listable};
setConsistencyChecks[function_Symbol, failTag_] :=
    function[___] := Throw[$Failed, failTag[function]];


ClearAll[catchInternalError];
Attributes[catchInternalError] = {HoldAll};
catchInternalError[code_, f_, failTag_] :=
  Catch[code, _failTag,
    Function[{value, tag},
      f::interr =  "The function failed due to an internal error. The failure \
           occured in function `1` ";
      Message[f::interr, Style[First@tag, Red]];
      f::interr =.;
      value]]; 

这就是我们之前示例的重写方式:

ClearAll[ff, gg, hh];
Module[{failTag},
  ff[x_Integer] := x^2 + 1;
  gg[x_?EvenQ] := x/2;
  hh[args__] := catchInternalError[gg[ff[args]], hh, failTag];
  setConsistencyChecks[{ff, gg}, failTag]
];

您可以看到它现在更加紧凑,我们可以专注于逻辑,而不是被错误检查或其他簿记细节分散注意力。额外的好处是我们可以使用Module- 生成的符号作为标签,从而封装它(不暴露于顶层)。以下是测试用例:

hh[1]
 1
hh[2]
 hh::interr: The function failed due to an internal error.
 The failure occured in function gg 
 $Failed
hh[1,3]
hh::interr: The function failed due to an internal error. 
The failure occured in function ff 
$Failed  

许多错误检查和错误报告任务可以以类似的方式自动化。在他的第二篇文章here 中,@WReach 讨论了类似的工具。

【讨论】:

  • 一如既往的好答案!
  • @Leonid,你不能只使用 FilterRules 吗?
  • @Szabolcs 谢谢!到目前为止,关于错误检查的内容肯定比我在这里说的要多。我添加了一个元编程示例来展示如何通过代码生成来自动化繁琐的错误检查过程。
  • @Verbeia 据我所知,FilterRules 仅测试选项的 l.h.s(选项名称)。我的包裹测试 r.h.s.选项,并在检测到错误时执行任意(用户指定的)代码。为选项传递错误的值会导致我不得不捕获的一些最隐蔽的错误,这是编写这些包的动机。在实践中,我通常将它们用作调试/开发工具,因为它们可以很容易地捕获错误。但是这些天我不经常使用它们。此外,它们不涵盖OptionsPattern - OptionValue,尚未更新。
  • @Leonid,您最近向我展示了如何为选项指定设置。如果没有给出参数,我不能让我的函数发送错误消息。您能否将OptionsPattern 更新为更新内容?
【解决方案2】:

我会使用fact that Mathematica's pattern matching goes from specific to general

Mathematica 确定新的转换规则比已经存在的规则更具体,如果将其放在该规则之后,则永远不会使用它。在这种情况下,新规则放在旧规则之前。请注意,在许多情况下,无法确定一个规则是否比另一个更具体;在这种情况下,新规则总是插入到最后。

假设我有一个只允许正整数作为参数的函数(例如,用于指定迭代中的步骤数。然后我将函数定义为:

myFunction[x_Integer?Positive,opts___Rule] := (*  whatever it does *)

然后假设我想要一条消息,如果 x 不是正数,另一条消息告诉您它必须是整数和正数。

myFunction::notpos=
"The first argument must be positive as well as an integer.";
myFunction::notposi=
"The first argument must be a positive integer, but you have given a `1`";

然后您可以为非正整数 x 定义以下内容:

myFunction[x_Integer,opts____]:= Message[myFunction::notpos]

其他的都是这个。

myFunction[x_,opts____]:= Message[myFunction::notposi,Head[x]]

这使模式匹配与实际功能分开。当x 为正整数时,实际函数将始终匹配,因为这是最具体的模式。

【讨论】:

  • 你知道为什么内置函数在出现问题时通常不会被评估,以及如何实现这种行为吗?尝试例如Integrate[1]。它返回Integrate[1]。 (这可能并不总是可取的,但它似乎是内置函数的常见行为。)只是好奇。我希望错误检查可能发生在Condition
  • @ndroock1 好吧,我确实在我的书中讨论过它,虽然不是很广泛,这里是:mathprogramming-intro.org/book/node388.html,这里是:mathprogramming-intro.org/book/node486.html。 David Wagner 还在他的书中关于包编写的章节中讨论了错误检查。
  • @Verbeia, @Szabolcs 我在答案的开头已经概述了如何做到这一点,诀窍是使用Condition:f[__]:="never happens"/;Message[f::err];