Jakie są najlepsze praktyki / najczęstsze / idiomatyczne sposoby zgłaszania błędów w Mathematica?

W szczególności, jakie są najlepsze sposoby zgłaszania błędów dotyczących nieprawidłowych argumentów do funkcji? Sprawdzanie poprawnych argumentów jest stosunkowo łatwe przy użyciu wzorców, ale sposób zgłaszania informacji o błędzie specyficznym dla danej sytuacji nie jest oczywisty.

Większość wbudowanych funkcji zwróci wartość bez wartości, gdy argumenty są niepoprawne, podczas wyświetlania komunikatu o błędzie.

Punkty do rozważenia:

  • Łatwy do dodania do już zaimplementowana funkcja nie zmieniając wiele, łatwo zachować raportowanie błędów oddzielnie od tego, co funkcja faktycznie robi
  • solidna i łatwa w utrzymaniu
  • dobrze gra z funkcjami, które przyjmują Opcje

Uwaga: mogę wymyślić kilka sposobów osiągnięcia tego zachowania. Ale zamiast zaczynać od zera i uczyć się na błędach, chciałbym wykorzystać doświadczenie i wiedzę społeczności, aby poznać sprawdzone metody.

Author: Mr.Wizard, 2011-07-02

2 answers

Uważam, że jest to dobra okazja, aby wymienić kilka technik sprawdzania błędów. Omówię te, które znam, i proszę, nie krępuj się edytować ten post i dodać więcej. Myślę, że główne pytania, na które należy odpowiedzieć, to to, co chcielibyśmy, aby funkcja wróciła w przypadku błędu, i jak to zrobić technicznie.

Co zwracać w przypadku błędu

Widzę tu 3 różne alternatywy

Funkcja wyświetla komunikat o błędzie i zwraca bez oceny.

Ten jest bardziej odpowiedni w środowisku symbolicznym i odpowiada semantyce, że większość funkcji Mathematica ma błędy w. r. t. Chodzi o to, że środowisko symboliczne jest o wiele bardziej wyrozumiałe niż tradycyjne, a zwracając to samo wyrażenie, wskazujemy, że Mathematica po prostu nie wie, co z tym zrobić. Pozostawia to szansę na późniejsze wywołanie funkcji, na przykład gdy niektóre z symbolicznych argumentów uzyskają wartości liczbowe lub inne.

Technicznie można to osiągnąć, wykorzystując semantykę wzorców warunkowych. Oto prosty przykład:
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]  

Chodzi o to, że na końcu wzór jest uważany za nie dopasowany (ponieważ test w stanie nie ocenia się do jawnego True), ale Message jest wywoływany w procesie. Sztuczka ta może być również używana z wieloma definicjami - ponieważ wzór na końcu jest uważany za nie dopasowany, pattern-matcher testuje inne reguły na liście DownValues. Może to być pożądane lub nie, w zależności od okoliczności.

Charakter funkcji jest taki, że bardziej odpowiednie jest zwrócenie $Failed (jawne niepowodzenie).

Typowymi przykładami przypadków, w których może to być właściwe, są błędy zapisu do pliku lub znalezienia pliku na dysku. Ogólnie rzecz biorąc, argumentowałbym, że takie zachowanie jest najbardziej odpowiednie dla funkcji używanych w inżynierii oprogramowania (innymi słowy te funkcje, które nie przesyłają wyników bezpośrednio do innej funkcji, ale wywołują inne funkcje, które powinny zwracać i tworzyć stos wykonawczy). Należy zwrócić $Failed (ewentualnie również komunikat o błędzie), gdy nie ma sensu kontynuowanie wykonywania po stwierdzeniu błędu danej funkcji.

Zwracanie $Failed jest również przydatne jako środek zapobiegawczy przeciwko sporadycznym błędom regresji wynikającym ze zmian w implementacji, na przykład gdy niektóre funkcja została zrefaktorowana, aby zaakceptować lub zwrócić inną liczbę i / lub typy argumentów, ale funkcja do wywołania jej nie została szybko zaktualizowana. W językach silnie typowanych, takich jak Java, kompilator wychwytuje tę klasę błędów. W Mathematica jest to zadanie programisty. Dla niektórych funkcji wewnętrznych w pakietach, zwracanie $Failed wydaje się bardziej odpowiednie w takich przypadkach niż wysyłanie komunikatów o błędach i zwracanie bez oceny. Również w praktyce jest to o wiele łatwiejsze - niewiele osób dostarczanie komunikatów o błędach do wszystkich ich wewnętrznych funkcji (co i tak jest prawdopodobnie złym pomysłem, ponieważ użytkownik nie powinien martwić się o jakieś wewnętrzne problemy z kodem), podczas gdy zwracanie $Failed jest szybkie i proste. Gdy wiele funkcji pomocniczych zwraca $Failed zamiast zachować ciszę, debugowanie jest znacznie łatwiejsze.

Technicznie najprostszym sposobem jest zwrócenie $Failed bezpośrednio z ciała funkcji, używając Return, jak w tym przykładzie niestandardowe Importowanie plików funkcja:

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
]];

Jednak bardzo często wygodniej jest używać pattern-matcher, w sposób opisany w odpowiedzi @Verbeia. Jest to najłatwiejsze w przypadku nieprawidłowych argumentów wejściowych. Na przykład, możemy łatwo dodać regułę catch-all do powyższej funkcji w następujący sposób:

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

Istnieją bardziej interesujące sposoby użycia pattern-matcher, patrz poniżej.

Ostatni komentarz tutaj jest taki, że jeden problem z funkcji łańcuchowych, z których każda może zwrócić $Failed jest to, że wiele kod kotła typu {[42] } jest potrzebny. W końcu skorzystałem z tej funkcji wyższego poziomu, aby rozwiązać ten problem: [66]}

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

Zatrzymuje wykonanie za pomocą wyjątku i zwraca $Failed, gdy tylko jakiekolwiek pośrednie wywołanie funkcji spowoduje $Failed. Na przykład:

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

Zamiast zwracać $Failed, można wyrzucić wyjątek, używając Throw.

Ta metoda jest IMO prawie nigdy nie nadaje się do funkcji najwyższego poziomu które są narażone na działanie użytkownika. Wyjątki Mathematica nie są sprawdzane (w sensie np. sprawdzane wyjątki w Javie), a mma nie jest mocno wpisywane, więc nie ma dobrego, obsługiwanego przez język sposobu, aby powiedzieć użytkownikowi, że w jakimś przypadku wyjątek może zostać wyrzucony. Jednak może być bardzo przydatny dla wewnętrznych funkcji w pakiecie. Oto przykład zabawki:

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) &]];

I jakiś przykład użycia:

 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

Znalazłem technika ta jest bardzo przydatna, ponieważ przy konsekwentnym stosowaniu pozwala bardzo szybko zlokalizować źródło błędu. Jest to szczególnie przydatne podczas korzystania z kodu po kilku miesiącach, kiedy nie pamiętasz już wszystkich szczegółów.

Czego nie zwracać

  • Nie zwracaj Null. Jest to niejednoznaczne, ponieważ Null może być znaczącym wyjściem dla jakiejś funkcji, niekoniecznie błędem.

  • Nie zwracaj Komunikatu o błędzie wydrukowanego za pomocą Print (w ten sposób Return Null).

  • Nie zwracaj Message[f::name] (zwracaj Null ponownie).

  • O ile w zasadzie mogę sobie wyobrazić, że można chcieć zwrócić pewną liczbę różnych "kodów zwrotnych" odpowiadających różnym rodzajom błędów( coś w rodzaju enum wpisz w C lub Javie), to w praktyce nigdy nie potrzebowałem tego w mma (może być, to tylko ja. Ale w tym samym czasie używałem tego dużo w C i Javie). Moim zdaniem staje się to bardziej korzystne w silniejszym (i być może również statycznie).

Używanie pattern-matcher do uproszczenia kodu obsługi błędów

Jeden z głównych mechanizmów został już opisany w odpowiedzi @Verbeia - użyj względnej ogólności wzorców. Jeśli chodzi o to, mogę wskazać na przykład ten pakiet, w którym często korzystałem z tej techniki, jako dodatkowe źródło roboczych przykładów tej techniki.

Problem z wieloma wiadomościami

The sama technika może być używana dla wszystkich 3 przypadków zwrotu omówionych powyżej. Jednak w pierwszym przypadku zwracania funkcji bez wartości istnieje kilka subtelności. Jednym z nich jest to, że jeśli masz wiele komunikatów o błędach dla wzorców, które" nakładają się", prawdopodobnie chciałbyś" zwarć " błąd dopasowania. Zilustruję problem, zapożyczając dyskusję z tutaj . Rozważmy funkcję:

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

Nazywamy to niepoprawnie:

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]

Oczywiście wynikowe wiadomości są sprzeczne, a nie to, co byśmy chcieli. Powodem jest to, że ponieważ w tej metodzie reguły sprawdzania błędów są uważane za nie dopasowane, pattern-matcher kontynuuje i może wypróbować więcej niż jedną regułę sprawdzania błędów, jeśli wzory nie są skonstruowane zbyt starannie. Jednym ze sposobów na uniknięcie tego jest uważne konstruowanie wzorców, aby nie nakładały się na siebie (wzajemnie się wykluczają). Omówiono kilka innych sposobów wyjścia we wspomnianym wątku. Chciałem tylko zwrócić uwagę na tę sytuację. Zauważ, że nie stanowi to problemu podczas jawnego zwracania $Failed lub rzucania wyjątku.

Za pomocą Module, Block i With ze współdzielonymi zmiennymi lokalnymi

Ta technika opiera się na semantyce definicji z wzorami warunkowymi, z wykorzystaniem konstruktów zakresowych Module, Block lub With. Jest on wspomniany tutaj . Dużą zaletą tego typu konstrukcji jest to, że pozwala na wykonać pewne obliczenia i dopiero wtedy, gdzieś w środku oceny funkcji, ustalić fakt błędu. Niemniej jednak pattern-matcher zinterpretuje go tak, jakby wzór nie był dopasowany i kontynuuje inne zasady, tak jakby nigdy nie doszło do oceny ciała dla tej zasady (to znaczy, jeśli nie wprowadziłeś skutków ubocznych) . Oto przykład funkcji, która znajduje "krótką nazwę" pliku, ale sprawdza, czy plik należy do danego katalogu (negatyw na która jest uważana za porażkę):

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];

(w kontekście, w którym go używam, stosownym było Throw wyjątek). Czuję, że jest to bardzo potężna technika i często z niej korzystam. W tym wątku podałem jeszcze kilka wskazówek do przykładów jego użycia, których jestem świadomy.

Funkcje z opcjami

Przypadek funkcji otrzymujących opcje jest IMO niezbyt szczególny, w tym sensie, że wszystko, co powiedziałem do tej pory, odnosi się również do nich. Jedna rzecz, która jest trudna jest do sprawdzenia błędów przeszedł opcje. Podjąłem próbę zautomatyzowania tego procesu za pomocą pakietów CheckOptions i PackageOptionChecks (które można znaleźć tutaj ). Używam ich od czasu do czasu, ale nie mogę powiedzieć, w jaki sposób, czy mogą być przydatne dla innych.

Meta-programowanie i automatyzacja

Być może zauważyłeś, że wiele kodów sprawdzających błędy jest powtarzalnych(kod kotła). Naturalną rzeczą wydaje się automatyzacja procesu tworzenia definicji sprawdzania błędów. Będę Podaj jeden przykład, aby zilustrować moc meta-programowania mma poprzez automatyzację sprawdzania błędów dla przykładu zabawki z wewnętrznymi wyjątkami omówionymi powyżej.

Oto funkcje, które automatyzują proces:

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]]; 

Oto jak nasz poprzedni przykład zostałby napisany na nowo:

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]
];

Widać, że jest teraz znacznie bardziej kompaktowy i możemy skupić się na logice, zamiast rozpraszać się sprawdzaniem błędów lub innymi szczegółami księgowymi. Dodatkowe korzyści jest to, że możemy użyć symbolu wygenerowanego Module jako znacznika, tym samym zamykając go (nie wystawiając na najwyższy poziom). Oto przypadki testowe:

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  
Wiele zadań sprawdzania błędów i raportowania błędów może być zautomatyzowanych w podobny sposób. W drugim poście tutaj, @WReach omówił podobne narzędzia.
 100
Author: Leonid Shifrin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:35:03

Użyłbym faktu, że dopasowanie wzoru Mathematica przechodzi od specyficznego do ogólnego .

Mathematica określa, że nowa reguła transformacji jest bardziej szczegółowa niż reguła już obecna i nigdy nie byłaby używana, gdyby została umieszczona po tej regule. W tym przypadku nowa reguła jest umieszczana przed starą. Zauważ, że w wielu przypadkach nie jest możliwe ustalenie, czy jedna reguła jest bardziej szczegółowa niż inna; w takich przypadkach Nowa reguła jest zawsze wstawiana na koniec.

Załóżmy, że miałem funkcję, która pozwalała tylko na dodatnie liczby całkowite jako parametr (np. do określenia liczby kroków w iteracji. Następnie zdefiniowałbym funkcję jako:

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

Przypuśćmy, że chciałem wiadomości, Jeśli x nie była dodatnia, a inna, która mówi, że musi być zarówno liczbą całkowitą, jak i dodatnią.

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`";

Możesz następnie zdefiniować następujące wartości dla nie dodatnich liczb całkowitych x:

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

I to na wszystko else.

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

To utrzymuje dopasowanie wzorca oddzielnie od rzeczywistej funkcji. Rzeczywista funkcja będzie zawsze pasować, gdy x jest dodatnią liczbą całkowitą, ponieważ jest to najbardziej specyficzny wzór.

 21
Author: Verbeia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-02 23:51:12