I'm trying to write code to solve water pouring problem in Clojure. The following code is strongly influenced by Martin Odersky's solution lectured in his coursera course, Functional Programming Principle in Scala. He first created a class Pouring with parameter capacity, which is accessible everywhere in the class. Though he made the best use of case class in his solution, I just used plain clojure map to represent the moves.I tried to solve the problem in the same way, and end up with the following code:(ns pouring)(declare capacity init-state)(defn empty [state glass]  (assoc state glass 0))(defn fill [state glass]  (assoc state glass (capacity glass)))(defn pour [state [from to]]  (let [amt (min (state from) (- (capacity to) (state to)))]    (-> state        (assoc from (- (state from) amt))        (assoc to (+ (state to) amt)))))(defn change [state move]  (if state    (cond (:empty move) (empty state (:empty move))          (:fill move) (fill state (:fill move))          (:pour move) (pour state (:pour move)))    (change init-state move)))(defn moves [capacity]  (let [glasses (range (count capacity))]    (lazy-cat     (map (fn [g] {:empty g}) glasses)     (map (fn [g] {:fill g}) glasses)     (for [from glasses, to glasses :when (not= from to)]       {:pour [from to]}))))(defn extend-path [path move]  {:history (conj (:history path) move)   :end-state (change (:end-state path) move)})(defn extend  ([paths explored]   (if-let [more (for [path paths                       next-path (map #(extend-path path %) (moves capacity))                       :when (not (contains? explored (:end-state next-path)))]                   next-path)]     (lazy-cat paths (extend more (conj explored (map #(:end-state %) more))))))  ([] (extend #{{:history [], :end-state init-state}} #{init-state})))(defn init [c]  (def capacity c)  (def init-state (vec (repeat (count c) 0))))(defn solve [capacity target]  (init capacity)  (first (for [path (extend)               :when (some #(= % target) (:end-state path))]           path)))The part I don't like is init function. Most clojurians says that using def inside a function is bad. If I don't use init function, perhaps I have to add parameter  capacity to almost every function in the code, which looks not pretty.I was considered using nested function.(defn solve [capacity target]  (defn empty ...)  (defn fill ...)  (defn pour ...)  (defn extend ...)  ...)However, using defn inside function is also bad, and the functions defined inside are actually not local functions, which are accessible outside the outer function.Yes, there's letfn in clojure. I can put every function except solve into letfn. But in this case, the code is not very readable. I think letfn is only for short (which can be expressed in 1 or 2 lines) functions.Perhaps there are better ways to do this. Any suggestions?
Water pouring challenge
clojure
I won't try to be comprehensive:The part I don't like is init function.Your instincts are right.If I don't use init function, perhaps I have to add parameter capacity to almost every function in the code, which looks not pretty.Clojure supports dynamic binding, which you can use instead of misusing def; although I'm not sure it's warranted here.Another problem I see is even though what you should do for each move (say [:pour 1 2]) is a constant, first it needs to be computed each time extend function is called, second look it up in a cond again in change. A move is a function from a state to a new state. Why not just use that function? Because we need the description for generating the history. So a move has a state transition function and a description.(defrecord Move [description change])And we can generate moves like this:(defn moves [capacity]  (let [glasses (range (count capacity))]    (concat     (for [g glasses] (Move. [:empty g] #(empty capacity % g)))     (for [g glasses] (Move. [:fill g] #(fill capacity % g)))     (for [from glasses, to glasses :when (not= from to)]       (Move. [:pour from to] #(pour capacity % [from to]) )))))We don't need change function anymore:(defn extend-path [path move]  (-> path    (update-in [:history] #(conj % (:description move)))    (update-in [:end-state] (:change move))))If you pay attention, change function was dependent on what the specific moves were. Previous version of extend-path was dependent on them also, transitive as it was dependent on change. We will return to this, later.We don't need to pass capacity to extend anymore; instead of generating moves, we can just pass them in:(defn extend  ([moves paths explored]    (if-let [more (for [path paths                        next-path (map #(extend-path path %) moves)                        :when (not (contains? explored (:end-state next-path)))]                    next-path)]      (lazy-cat paths (extend moves more (conj explored (map #(:end-state %) more)))))))Thus extend is not dependent on the specifics of this problem.       If we inline init we get something like this:(defn solve [capacity target]  (let [init-state (vec (repeat (count capacity) 0))        moves (moves capacity)        init-path #{{:history [], :end-state init-state}}]    (first (for [path (extend moves init-path #{})                 :when (some #(= % target) (:end-state path))]             path))))I initialize the explored states as empty set, as it made more sense to me. I notice that the body of solve has only the test in the :when clause left specific to this problem. The rest of the body is actually some search algorithm. (I assume it is breadth-first)we can extract it, too:(defn breadth-first [moves init-path final?]  (let [init-path #{{:history [], :end-state init-state}}]    (first (for [path (extend moves init-path #{})                 :when (final? (:end-state path))]             path))))(defn solve [capacity target]  (let [init-state (vec (repeat (count capacity) 0))        moves (moves capacity)        final? #((set %) target)]    (breadth-first moves init-state final?))This enhanced separation of concerns, as breadth-first, extend, extend-path, Move are no longer dependent on the specifics of this problem. You can move them to another source file, which would not change much as the problem requirements evolve. This also makes it easier to change the search algorithm being used.