Search code examples
if-statementclojureidioms

Better way of writing multiple if checks in a Clojure function?


I have a Clojure function that looks something like the following.

(defn calculate-stuff [data]
  (if (some-simple-validation data)
    (create-error data)
    (let [foo (calculate-stuff-using data)]
      (if (failed? foo)
        (create-error foo)
        (let [bar (calculate-more-stuff-using foo)]
          (if (failed? bar)
            (create-error bar)
            (calculate-response bar)))))))

Which works fine but is a little hard to read, so I was wondering if there was a more idiomatic Clojure way of writing this?

I thought about making some-simple-validation, calculate-stuff-using and calculate-more-stuff-using throw exceptions and using a try/catch block but that felt like using exceptions for control flow which didn't feel correct.

I can't let the exceptions escape this function either as I'm using it to map a seq of maps and I still want to continue processing the remainder.

I guess what I'm after is something like this?

(defn calculate-stuff [data]
  (let-with-checking-function
    [valid-data (some-simple-validation data)
     foo (calculate-stuff-using valid-data)
     bar (calculate-more-stuff-using foo)]
    failed?)                    ; this function is used to check each variable
      (create-error %)          ; % is the variable that failed
      (calculate-response bar)) ; all variables are OK

Thanks!


Solution

  • If a failed validation indicates an error condition, an exception (and a try-catch block) may be the best way of handling it. Especially if it is not a "normal" occurrance (i.e. invalid cust-id, etc).

    For more "normal" but still "invalid" cases, you might use some-> (pronounced "some-thread") to quietly squelch "bad" cases. Just have your validators return nil for bad data, and some-> will abort the processing chain:

    (defn proc-num [n]
      (when (number? n)
        (println :proc-num n)
        n))
    
    (defn proc-int [n]
      (when (int? n)
        (println :proc-int n)
        n))
    
    (defn proc-odd [n]
      (when (odd? n)
        (println :proc-odd n)
        n))
    
    (defn proc-ten [n]
      (when (< 10 n)
        (println :proc-10 n)
        n))
    
    (defn process [arg]
      (when (nil? arg)
        (throw (ex-info "Cannot have nil data" {:arg arg})))
      (some-> arg
        proc-num
        proc-int
        proc-odd
        proc-ten))
    

    results:

    (process :a) => nil
    
    (process "foo") => nil
    
    :proc-num 12
    :proc-int 12
    (process 12) => nil
    
    :proc-num 13
    :proc-int 13
    :proc-odd 13
    :proc-10 13
    (process 13) => 13
    
    (throws? (process nil)) => true
    

    Having said this, you are now using nil to mean "data validation failure", so you cannot have nil in your data.


    Using Exceptions For Invalid Data

    Using nil as a special value to short-circuit processing can work, but it might be easier to use plain-old exceptions, especially for cases that are clearly "bad data":

    (defn parse-with-default [str-val default-val]
      (try
        (Long/parseLong str-val)
        (catch Exception e
          default-val))) ; default value
    
    (parse-with-default "66-Six" 42) => 42
    

    I have a little macro to automate this process called with-exception-default:

    (defn proc-num [n]
      (when-not (number? n)
        (throw (IllegalArgumentException. "Not a number")))
      n)
    
    (defn proc-int [n]
      (when-not (int? n)
        (throw (IllegalArgumentException. "Not int")))
      n)
    
    (defn proc-odd [n]
      (when-not (odd? n)
        (throw (IllegalArgumentException. "Not odd")))
      n)
    
    (defn proc-ten [n]
      (when-not (< 10 n)
        (throw (IllegalArgumentException. "Not big enough")))
      n)
    
    (defn process [arg]
      (with-exception-default 42  ; <= default value to return if anything fails
        (-> arg
          proc-num
          proc-int
          proc-odd
          proc-ten)))
    
    (process nil)    => 42
    (process :a)     => 42
    (process "foo")  => 42
    (process 12)     => 42
    
    (process 13)     => 13
    

    This avoids giving a special meaning to nil or any other "sentinal" value, and uses Exception for its normal purpose of altering control flow in the presence of errors.