Les erreurs arrivent. En Elixir, même si l'on dit souvent « let it crash », il arrive qu'on doive rattraper l'appel de fonction pour revenir à un état connu et fiable afin de respecter un contrat logiciel. Dans certains langages, les erreurs servent de mécanisme de contrôle de flux, mais en Elixir, ce schéma est déconseillé. On peut souvent reconnaître les fonctions susceptibles de lever une erreur à leur nom : les fonctions qui lèvent des erreurs doivent avoir un ! à la fin de leur nom. À comparer avec les fonctions qui renvoient {:ok, value} ou :error. Regarde ces exemples de bibliothèque :
Map.fetch(%{a: 1}, :b)
# => :error
Map.fetch!(%{a: 1}, :b)
# => raises KeyError
Elixir propose une construction pour rattraper les erreurs avec try .. rescue.
try do
raise RuntimeError, "error"
rescue
e in RuntimeError -> :error
end
Examinons cette construction :
try.raise/2.rescue, on filtre par motif sur le nom du module de l'erreur levée
-> :
e correspond à la structure d'erreur.in est un mot-clé.RuntimeError est l'erreur que l'on veut rattraper.
_ à la place du nom du module ou omettre complètement le mot-clé in.Les erreurs (parfois aussi appelées « exceptions ») que l'on rattrape de cette façon sont des structures. On rattrape très rarement les erreurs en Elixir. En général, l'erreur rattrapée est journalisée ou envoyée à un service de supervision externe, puis relancée. Cela signifie qu'on ne se soucie généralement pas de la structure interne de l'erreur concernée.
Dans le concept Exceptions, tu en apprendras plus sur les structures d'erreur, notamment comment définir ta propre erreur personnalisée.
Pendant que tu travailles chez Instruments of Texas, on te confie une calculatrice expérimentale en notation polonaise inversée [RPN] écrite en Elixir. Ton équipe rencontre un problème : certaines opérations lèvent des erreurs et font planter le processus. On t'a demandé d'écrire une fonction qui encapsule la fonction d'opération afin que les erreurs puissent être gérées plus élégamment avec du code Elixir idiomatique.
Implémente la fonction calculate!/2 pour appeler la fonction d'opération en lui passant la pile comme seul argument. La fonction d'opération est définie ailleurs, mais tu sais qu'elle peut soit se terminer correctement, soit lever une erreur.
stack = []
operation = fn _ -> :ok end
RPNCalculator.calculate!(stack, operation)
# => :ok
stack = []
operation = fn _ -> raise ArgumentError, "An error occurred" end
RPNCalculator.calculate!(stack, operation)
# => ** (ArgumentError) An error occurred
Les noms de fonctions qui se terminent par
!avertissent les programmeurs que cette fonction peut lever une erreur
En poursuivant tes recherches, tu remarques que beaucoup de fonctions utilisent des atomes et des tuples pour indiquer leur réussite ou leur échec. Implémente calculate/2 avec cette stratégie.
stack = []
operation = fn _ -> "operation completed" end
RPNCalculator.calculate(stack, operation)
# => {:ok, "operation completed"}
stack = []
operation = fn _ -> raise ArgumentError, "An error occurred" end
RPNCalculator.calculate(stack, operation)
# => :error
Certaines erreurs contiennent des informations importantes dont tes collègues ont besoin pour garantir le bon fonctionnement du système. Implémente calculate_verbose/2 pour transmettre le message d'erreur. L'erreur est une struct qui possède un champ :message.
stack = []
operation = fn _ -> "operation completed" end
RPNCalculator.calculate_verbose(stack, operation)
# => {:ok, "operation completed"}
stack = []
operation = fn _ -> raise ArgumentError, "An error occurred" end
RPNCalculator.calculate_verbose(stack, operation)
# => {:error, "An error occurred"}
Inscris-toi sur Exercism pour apprendre et maîtriser Elixir avec 58 concepts168 exercices, et un vrai mentorat humain, le tout gratuitement.