Search code examples
clojureoptional-parametersoptional-arguments

Clojure: succinctly forward optional values


I've written a probability function in Clojure that takes an optional hash-map of options:

(defn roll-lte
  ([n d] (/ n d))
  ([n d options]
    (let [p (/ n d)
          roll-type (:type options :normal)]
      (cond
        (= roll-type :advantage) (- (* p 2) (* p p))
        (= roll-type :disadvantage) (* p p)
        (= roll-type :normal) p
        :else (throw (IllegalArgumentException. "Invalid roll type."))))))

This works as intended, but the idea is to write other functions that build off of this one -- for example:

(defn roll-gte
  ([n d] (roll-lte (- d n -1) d))
  ([n d options] (roll-lte (- d n -1) d options)))

The two arities in roll-lte make building off of the function awkward and repetitive, especially in cases like the above where options is simply being forwarded to roll-lte. Is there a more concise and less repetitive way to achieve this?


Solution

  • When I have functions with multiple arities, I usually try to have the lower-arity versions call the higher-arity versions with safe default arguments. The "main" implementation of the function usually ends up being the highest-arity body:

    (defn roll-lte
      ([n d] (roll-lte n d nil))
      ([n d {:keys [type]
             :or   {type :normal}}]
       (let [p (/ n d)]
         (case type ;; used case instead of cond here
           :advantage (- (* p 2) (* p p))
           :disadvantage (* p p)
           :normal p
           (throw (IllegalArgumentException. "Invalid roll type."))))))
    

    I also used :or in the options map destructuring above to set the default value for type, which allows the lower-arity functions to just pass a nil options map.

    (defn roll-gte
      ([n d] (roll-gte n d nil))
      ([n d options] (roll-lte (- d n -1) d options)))
    
    (roll-gte 3 4) ;=> 1/2
    (roll-gte 3 4 {:type :advantage}) ;=> 3/4