Dynamic variables in Clojure libraries

Viewed 300

TL;DR: Is the following a good pattern for a library?

(def ^{:dynamic true} *var*)
(defn my-fn [{:keys [var]}]
  (do-smth (or var *var*)))

--

Say I want to write a sentiment analysis library.

Is it good design in get-sentiment fn to accept optional sentiment labels but provide default one as dynamic var?

(def ^:dynamic *sentiment-level-labels*
  ["Very Negative" "Negative" "Neutral" "Positive" "Very Positive"])

;;...

(defn get-sentiment-scores
  "Takes text and gives back a 0 to 4 sentiment score for each sentences."
  [text]
  ;;...)

(defn get-sentiment 
  "Gives back a sentiment map with sentences scores, 
   average score, rounded score and labeled score.
   Can accepts custom sentiment level labels under :labels opt."
  [text & {:keys [labels]}]
  (let [scores (get-sentiment-scores text)
        average-score (get-average scores)
        rounded-score (Math/round average-score)
        label (get (or labels *sentiment-level-labels*) rounded-score)]
    {:scores scores
     :average-score average-score
     :rounded-score rounded-score
     :label label}))

Clojure library coding standards official page says:

If you present an interface that implicitly passes a parameter via dynamic binding (e.g. db in sql), also provide an identical interface but with the parameter passed explicitly.

https://dev.clojure.org/display/community/Library+Coding+Standards

In my example, I provided only one interface but with opt argument.

Is this okay? Are there better ways to handle this?

Thank you!

1 Answers

Dynamic vars are full or pitfalls. They push your API code towards implicit environmental coupling, and often force your calling code to add a lot of (binding ...) clauses, which kind of defeats the purpose of concision for using Dynamic vars in the first place. They also lead to tricky edge cases if control is passed from one thread to another.

In your case, I would recommend simply passing the labels in a params map argument:

(def default-sentiment-level-labels
  ["Very Negative" "Negative" "Neutral" "Positive" "Very Positive"])

(defn get-sentiment 
  "Gives back a sentiment map with sentences scores, 
   average score, rounded score and labeled score.
   Can accepts custom sentiment level labels under :labels opt."
  [text {:as params, :keys [labels] :or {labels default-sentiment-labels}}]
  ...))

Note that the usage of a map can be interesting, because a map is opaque to intermediaries: you can have various components of your algorithm read only from the params map the keys that concern them.

Related