I am continuing to translate the examples from On Lisp by Paul Graham into Clojure.
I have placed the examples from the first 6 chapters on GitHub. The readme links to all of the posts in this series. (Except this one... a fact without a time is incomplete).
Section 6.1 Networks
I have represented the nodes as a map of maps.(def nodes {:people {:question "Is the person a man?" :yes :male :no :female} :male {:question "Is he dead?" :yes :deadman :no :liveman } :deadman {:question "Was he American?" :yes :us :no :them} :us {:question "Is he on a coin?" :yes :coin :no :cidence} :coin {:question "Is the coin a penny?" :yes :penny :no :coins} :penny {:answer "Lincoln"}}) (defn ask-question [question] true)
Since the network is incomplete, I decided not to implement the IO. Making ask-question always return true did require one change from Graham's example. Instead of asking if the person is living, I ask if he is dead, since I only go down the true line.
(defn run-node [name nodes] (let [n (name nodes)] (if-let [result (get n :answer)] result (if (ask-question (:question n)) (recur (:yes n) nodes) (recur (:no n) nodes))))) (run-node :people nodes)
Of course, we want to be able to add nodes programmatically. Instead of optional parameters, in the Clojure implementation we can define a multiple arity function to add both branches and leaves.
(defn add-node ([nodes tag answer] (conj nodes {tag {:answer answer}})) ([nodes tag question yes no] (conj nodes {tag {:question question :yes yes :no no}})))
Because nodes is immutable, the following two calls each return a new map that is the original map, plus their one node.
(add-node nodes :liveman "Is he a former president" :texas :california) (add-node nodes :texas "George W Bush")
The Clojure threading macro, ->, makes it easy to insert the result of one function as a parameter of a second function. The following block creates a new set of nodes with the :liveman tag and passes this to the function that adds the :texas tag. In the end, we get a new map that has both tags added.
(-> (add-node nodes :liveman "Is he a former president" :texas :california) (add-node :texas "George W Bush"))
Section 6.2 Compiling Networks
In this section, Graham rewrote the network, adding the function calls to the nodes themselves.The add-node function becomes
(defn add-node2 ([nodes tag answer] (conj nodes {tag answer})) ([nodes tag question yes no] (conj nodes {tag (if (ask-question question) (yes nodes) (no nodes))})))
I added a couple of nodes, and was surprised by the results:
(def node2 (-> (add-node2 {} :people "Is the preson a man?" :male :female) (add-node2 :male "Is he dead?" :deadman :liveman))) node2 ;; => {:male nil, :people nil}
I decided to start adding from the bottom up:
(def node2 (-> (add-node2 {} :penny "Lincoln") (add-node2 :coin "is the coin a penny?" :penny :coins) (add-node2 :us "Is he on a coin" :coin :cindence))) node2 ;; => {:us "Lincoln", :coin "Lincoln", :penny "Lincoln"}
I tried rewriting my add-node2 function.
(defn add-node2 ([nodes tag answer] (conj nodes {tag answer})) ([nodes tag question yes no] (conj nodes {tag (if ((fn [x] (ask-question x)) question ) (yes nodes) (no nodes) )})))
I still got the same results.
I tried declaring, but not defining ask-question. When I called add-node2 I got an error that ask-question had not been defined. I tried referring to node2 from another namespace, and still every node evaluated to "Lincoln".
I rewrote the ask-question function to actually ask a question:
(defn prompt [text] (do (println text) (read-line))) (defn ask-question [question] (prompt question))
Now, I get prompted with the question for each node I add. Again, I tried this from a different namespace, and again I was prompted.
I wonder if we have reached the limit of functions. Stay tuned, Chapter 7 begins our journey into the world of macros.
No comments:
Post a Comment