Return Codes vs Exceptions: View from the Belfry

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

    Stop talking about foreach. I will show directly by way of example two operators very similar to each other, one of which uses “exceptions” as the implementation, the other uses “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 operator:
    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 - body of the operator that ends with the response of the subroutine or any called system that needs to be processed based on the return code.

    Let's see they work.

    process-retcode-answer



    Suppose we have functions for processing return codes 0, -1, -2 and the logic for processing 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 map return codes for the names of the functions that process them:

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


    Now we create test routines 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, 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 changing handlers of existing ones is carried out transparently by implementing handlers and making appropriate changes to the mapping, which can be in a separate file.

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

    process-exception



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

    (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 for the names of the functions that handle 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 routines 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.

    conclusions



    These operators are very similar and, in principle, implement the same response processing pattern, but with different implementations. Here I give preference to process-retcode-answer . Although in other languages, variants with return codes are not always advantageous compared to variants that use exceptions (of course, it depends on the conditions of the problem and the language itself - this has already been discussed).

    Here is an implementation option for 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, like the ability to expand the language with new constructs and also have the same high-order functions, lambdas, closures. Such a language allows the programmer to become an artist. He does not write patterns again and again. He simply “creates” a language in which he can most naturally, graphically and succinctly formulate a solution to his task. A profession becomes not a craft, but an art.

    Also popular now: