📜 ⬆️ ⬇️

Return codes vs exception: a view from the bell tower

After reviewing the post Return codes vs exceptions and comments to it, I noticed that one thread was missed in the discussion, the short thesis of which is the following: in some languages ​​this problem is not even worth it, because The question “what to choose, return codes or exceptions” in such a language is low-level. As, for example, there is no question how to implement the “foreach” construction. Because for a programmer using the same “foreach”, it makes no difference whether the creators of the while or for language or something else used the implementation of this operator. The main thing is the pattern that represents this very operator.

Stop talking about foreach. I will show directly on the example of two very similar to each other operator, one of which uses the “exception” as the implementation, the other - the “return codes”.

process-exception [ mapping & body ]
where mapping is a map of the form {exception 1 <-> processing method 1, exception 2 <-> processing method 2, ...}, body is the body of the operator in which an exception may occur.

and the second statement:
process-retcode-answer [ mapping & body ]
where mapping is a map of the form {return code 1 <-> processing method 1, return code 2 <-> processing method 2, ...}, body - the body of the operator, which ends with the response of the subroutine or of any called system that is needed process based on the return code.
')
Let's see, they work.

process-retcode-answer



Suppose we have functions for handling return codes of 0, -1, -2, and the processing logic for the remaining codes:

( defn ok-processor [ result ]
( println ( str "ok. result:" result ) ) )

( defn error-processor [ result ]
( println ( str "error. result:" result ) ) )

( defn another-error-processor [ result ]
( println ( str "another error. result:" result ) ) )

( defn unknown-error-processor [ result ]
( println ( str "unknown error. result:" result ) ) )


We define the map of return codes for the names of the functions processing them:

( def result-mapping { 0 'ok-processor
- 1 'error-processor
- 2 'another-error-processor
: other 'unknown-error-processor } )


Now we create test subroutines that return different return codes and the corresponding result:

( defn test-call-ok [ ]
[ 0 "test result" ] )

( defn test-call- error [ ]
[ - 1 "test result" ] )

( defn test-call-another- error [ ]
[ - 2 "test result" ] )

( defn test-call-unknown- error [ ]
[ - 1000 "test result" ] )


The work of our operator in this case looks like this:

( process-retcode-answer result-mapping ( test-call-ok ) )
ok . result : test result

( process-retcode-answer result-mapping ( test-call- error ) )
error . result : test result

( process-retcode-answer result-mapping ( test-call-another- error ) )
another error . result : test result

( process-retcode-answer result-mapping ( test-call-unknown- error ) )
unknown error . result : test result


Here in each body consists of only one method. In reality, you can insert any sequence of functions instead.

The advantage of this operator is that the processing of new codes or the modification of existing handlers is carried out transparently by implementing handlers and making the appropriate changes to the mapping, which can be in a separate file.

No ifs. This approach implements a fairly flexible return code processing pattern.

process-exception



By analogy with the previous example. There are some exception handling functions:

( defn arithmetic exception-processor [ e ]
( println ( str "Arithmetic exception." ) ) )

( defn nullpointer-exception-processor [ e ]
( println ( str "Nullpointer exception." ) ) )

( defn another-exception-processor [ e ]
( println ( str "Other exception." ) ) )


Map exceptions to the names of the functions that process them:

( def exception-mapping { java . lang . ArithmeticException 'arithmetic-exception-processor
java . lang . NullPointerException 'nullpointer-exception-processor
java . lang . Exception 'another-exception-processor } )


We create test subroutines that generate various exceptions:

( defn test-call-ok [ ]
"test result" )

( defn test-throw-arithmetic-exception [ ]
( throw ( new java . lang . ArithmeticException ) )
"test resutl" )

( defn test-throw-nullpointer-exception [ ]
( throw ( new java . lang . NullPointerException ) )
"test resutl" )

( defn test-throw-other-exception [ ]
( throw ( new java . lang . ClassNotFoundException ) )
"test resutl" )


The work of our operator:

( process-exception exception-mapping
( test-call-ok ) )
"test result"

( process-exception exception-mapping
( test-throw-arithmetic-exception ) )
Arithmetic exception .

( process-exception exception-mapping
( test-throw-nullpointer-exception ) )
Nullpointer exception .

( process-exception exception-mapping
( test-throw-other-exception ) )
Other exception .


The remarks at the end of the description of the previous statement apply here.

findings



These operators are very similar and, in principle, implement the same response processing pattern, but with a different implementation. Here I prefer process-retcode-answer . Although in other languages, options with return codes are not always beneficial in comparison with options that use exceptions (of course, depends on the conditions of the problem and the language itself - this has already been discussed).

Here is the implementation of the above operators:

( defmacro process-exception [ mapping & body ]
( let [ catch-items ( map ( fn [ m ])
` ( catch ~ ( first m ) e #
( ~ ( eval ( second m ) ) e # ) ) )
( eval mapping ) ) ]
` ( try ~ @ body
~ @ catch-items ) ) )


( defmacro process-retcode-answer [ mapping & body ]
` ( let [ answer # ( do ~ @ body )
retcode # ( first answer # )
result # ( second answer # )
processor # ( get ~ mapping retcode # )
processor # ( if ( nil ? processor # ) ( : other ~ mapping ) processor # ) ]
( ( eval processor # ) result # ) ) )


This rather exaggerated example shows that the basic elements of the language are not very important, as is the ability to extend the language with new constructions and also have the same functions of high order, lambda, and closures in it. This language allows the programmer to become an artist. He does not write patterns over and over. He simply "creates" a language in which he can most naturally, visually and briefly formulate a solution to the task set before him. Profession becomes not a craft, but an art.

Source: https://habr.com/ru/post/131129/


All Articles