Freitag, 1. Juni 2012

Porting PAIP to clojure - Chapter 4: GPS - The General Problem Solver

Porting PAIP to clojure - Chapter 4: GPS - The General Problem Solver

Table of Contents

  • 1 PAIP to clojure - chapter 4
    • 1.1 Description
    • 1.2 Specification
    • 1.3 Implementation
    • 1.4 Testing
    • 1.5 Analysis
    • 1.6 Back to implementation - A more general version 2

1 PAIP to clojure - chapter 4

I continue to translate PAIP to clojure. As always, it is possible to follow the post even without reading PAIP, but I encourage you to do so. This chapter covers the GPS program, the General Problem Solver. As always, the focus lies on the iterative process of developing an (ai-) progam. Norvig presents 5 stages of an programming project, after which the sections of the chapter are numbered:
  1. Description
  2. Specification (in algorithmic terms)
  3. Implementation
  4. Testing
  5. Debugging/Analysis (optional repeat)

1.1 Description

GPS was the first program which separated its problem-solving strategy from its knowledge of particular problems. It used means-ends analysis as problem-solving strategy. In means-ends analysis, you know in what state you are right now and in what state you will be when you have solved your problem, but you consider what is between your state now and the goal-state and how this distance can be overcome. An Example: I have to write a blog-post. What Do I need to do to write an blog-post? I have to type it. What do I need to type it? I have to sit in front of my pc. What do I need to do to sit in front of my pc ? Go to the PC ….
It is obvious that we have to be able to answer "What do I need to … "or, like the book points out "What is the difference between…" from the set of all possible actions. And each actions could have itself preconditions, for which actions have to be taken befor it can be taken. This is a very brief description, I encourage you to read the section in PAIP for the full discussion.

1.2 Specification

After having a (rough) Idea of how the means-ends analysis works, it is time to come to the second step: specification. In this stage, we refine the description to something that is representible in the implementation language. I tink, the stage of specification is best described as follows: Find the most natural mapping between the problem and the implementation language!
So, let's see how means-ends analysis can be represented in clojure:
  • A state can be represented as a set of conditions, for example #{unknown poor} and #{rich famous}. We need 2 states: the current state and the goal state (try to guess which of the two above is the goal-state…?)
  • The allowable actions can naturally be represented as a collection of actions, which can be worke
  • An action needs a Name, a descriptions of what states a person has to be in in order to take the action and a description of what states of the person will change after taking the action. An action can therefore be described by a map (or later by a record) with a :action and a :precond field. The changes of the state of the acting person are trickier. Consider an action "Become rich" that has as precond being poor and changes that to being rich. It could also remove states or add new ones. The change of one state to the other can easily be represented in terms of removing the old one and adding the new one. Thus the Action will have two more fields, a :del-list and a :add-list which contains states which will be added or removed from the current state after taking the action.
  • Last, but not least, we have to specify the entry point of the application, the GPS-function. We need a start-state, a goal-state and the allowed actions. It can be represented as a function which takes a start-state and a goal states, and works with the ops var.
Now we have specified the single entities of the program. The algorithm is next to go: GPS succeeds, if it can achieve all single conditions in the goal state by applying a sequence of actions. To decide, wheter to apply an action or not, we have to look in its add list. If a goal-condition is in the add-list, the action is appropriate and GPS wants to take the action. It can only take the action, if it can achieve all preconds of the action, which is just calling itself with the preconds as goal-state, updating the current state as it goes along. If it was able to achieve every condition of the goal state, it succeedes (Note that this is the first, most simplest specification, it will be refined in part 2). Ok, this is enough for the first implementation of the program!

1.3 Implementation

First, let's have a look at a sample operations list. I find it easier to implement the program, if I see with what data it works with:
(ns PAIP2clojure.Chapter4)

(def school-ops
 [ {:action "drive-son-to-school"
    :preconds #{ "son-at-home" "car-works"}
    :add-list #{ "son-at-school"}
    :del-list #{ "son-at-home"}}
   {:action "shop-installs-battery"
    :preconds #{"car-needs-battery" "shop-knows-problem" "shop-has-money"}
    :add-list #{"car-works"}}
   {:action "tell-shop-problem"
    :preconds #{"in-communication-with-shop"}
    :add-list #{"shop-knows-problem"}}
   {:action "telephone-shop"
    :preconds #{"know-phone-number"}
    :add-list #{"in-communication-with-shop"}}
   {:action "look-up-number"
    :preconds #{"have-phone-book"}
    :add-list #{"know-phone-number"}}
   {:action "give-shop-money"
    :preconds #{"have-money"}
    :add-list #{"shop-has-money"}
    :del-list #{"have-money"}}])
(def ^:dynamic *ops* school-ops)
=> #'PAIP2clojure.Chapter4/*ops*
In the implementation, I choose not follow Norvigs route and modify the current state in the function, which would require something like an atom or agent in clojure. Instead I changed the implementation to be side-effect free and hence more ideomatic clojure. Here is the implementation from the book:
(defvar *state* nil "The current state: a list of conditions.")

(defvar *ops* nil "A list of available operators.")

(defstruct op "An operation"
  (action nil) (preconds nil) (add-list nil) (del-list nil))

(defun GPS (*state* goals *ops*)
  "General Problem Solver: achieve all goals using *ops*."
  (if (every #'achieve goals) 'solved))

(defun achieve (goal)
  "A goal is achieved if it already holds,
  or if there is an appropriate op for it that is applicable."
  (or (member goal *state*)
      (some #'apply-op 
            (find-all goal *ops* :test #'appropriate-p))))

(defun appropriate-p (goal op)
  "An op is appropriate to a goal if it is in its add list."
  (member goal (op-add-list op)))

(defun apply-op (op)
  "Print a message and update *state* if op is applicable."
  (when (every #'achieve (op-preconds op))
    (print (list 'executing (op-action op)))
    (setf *state* (set-difference *state* (op-del-list op)))
    (setf *state* (union *state* (op-add-list op)))
I had to change the return value of achieve and apply-op to an updated current-state, instead of returning a truth value and destructively modifying the current state as side effect. Also, I couldn't use every? because the state has to be updated after every application of achieve. Thus I made a helper function every-accum? which takes a 2-argument function, a start-state and a collection and reduces the collection applying the function. When one result is nil, the function returns nil.
Notice the use of the with-auto-declare macro, which I described in a previous post. This let me avoid   to add a declare, everytime I make a forward declaration. If a symbol starts with _, a forward-declaration will be automagically added for it.
 (use 'clojure.set)
 (use 'auto-declare.core)

(with-auto-declare _  
   (defn GPS [state goals]
     (let [new-state (_every-accum? _achieve state goals)]
       (if (nil? new-state)

   (defn achieve
     "return the new-state after the goal is achieved or nil
      if it could not be archieved"
     [current-state goal]
     (if (contains? current-state goal)
        (some (partial _apply-op current-state)
          (filter #(_appropriate? goal %) *ops*))))

   (defn appropriate? [goal op]
     (contains? (:add-list op) goal))

   (defn every-accum? [func start coll]
     (reduce #(if (nil? %1)
                (func %1 %2)) start coll))

   (defn apply-op [current-state op]
     (let [new-current-state (every-accum? achieve current-state (:preconds op))]
       (if (nil? new-current-state)
         (do (println (str "executing " (:action op)))
             (-> new-current-state (difference (:del-list op)) (union (:add-list op))))))))
=> #'PAIP2clojure.Chapter4/apply-op
Ok, now comes the 4. stage: Testing

1.4 Testing

Let's test the implementation with the ops defined above and see how it works. Norvig presents three sample applications:
(GPS #{"son-at-home" "car-needs-battery" "have-money" "have-phone-book"} #{"son-at-school"})
executing look-up-number
executing telephone-shop
executing tell-shop-problem
executing give-shop-money
executing shop-installs-battery
executing drive-son-to-school
executing look-up-number
executing telephone-shop
executing tell-shop-problem
executing give-shop-money
executing shop-installs-battery
executing drive-son-to-school
=> solved
(GPS #{"son-at-home" "car-needs-battery" "have-money"}
=> not-solved
(GPS #{"son-at-home" "car-works"}
executing drive-son-to-school
=> solved
The second example has no solution, because the person does not have a phone-book and thus can't come in communication with the shop and the shop cant repair the car, so he can't drive his son to school
But how does it actually work? Trace is a good way to find out.
(dotrace [GPS achieve appropriate? apply-op every-accum?] 
  (GPS #{"son-at-home" "car-needs-battery" "have-money" "have-phone-book"} #{"son-at-school"}))  
 TRACE t3410: (GPS #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} #{"son-at-school"})
 TRACE t3411: | (every-accum? #<Chapter4$eval3393$fn__3396 PAIP2clojure.Chapter4$eval3393$fn__3396@6bb0b0a0> #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} #{"car-works" "son-at-home"})
 TRACE t3412: | | (achieve #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} "car-works")
 TRACE t3413: | | | (every-accum? #<Chapter4$eval3393$fn__3396 PAIP2clojure.Chapter4$eval3393$fn__3396@6bb0b0a0> #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} #{"shop-knows-problem" "car-needs-battery" "shop-has-money"})
 TRACE t3414: | | | | (achieve #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} "shop-knows-problem")
 TRACE t3415: | | | | | (every-accum? #<Chapter4$eval3393$fn__3396 PAIP2clojure.Chapter4$eval3393$fn__3396@6bb0b0a0> #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} #{"in-communication-with-shop"})
 TRACE t3416: | | | | | | (achieve #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} "in-communication-with-shop")
 TRACE t3417: | | | | | | | (every-accum? #<Chapter4$eval3393$fn__3396 PAIP2clojure.Chapter4$eval3393$fn__3396@6bb0b0a0> #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} #{"know-phone-number"})
 TRACE t3418: | | | | | | | | (achieve #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} "know-phone-number")
 TRACE t3419: | | | | | | | | | (every-accum? #<Chapter4$eval3393$fn__3396 PAIP2clojure.Chapter4$eval3393$fn__3396@6bb0b0a0> #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} #{"have-phone-book"})
 TRACE t3420: | | | | | | | | | | (achieve #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"} "have-phone-book")
 TRACE t3420: | | | | | | | | | | => #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"}
 TRACE t3419: | | | | | | | | | => #{"car-needs-battery" "have-phone-book" "have-money" "son-at-home"}
 executing look-up-number
 TRACE t3418: | | | | | | | | => #{"car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home"}
 TRACE t3417: | | | | | | | => #{"car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home"}
 executing telephone-shop
 TRACE t3416: | | | | | | => #{"car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3415: | | | | | => #{"car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"}
 executing tell-shop-problem
 TRACE t3414: | | | | => #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3421: | | | | (achieve #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"} "car-needs-battery")
 TRACE t3421: | | | | => #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3422: | | | | (achieve #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"} "shop-has-money")
 TRACE t3423: | | | | | (every-accum? #<Chapter4$eval3393$fn__3396 PAIP2clojure.Chapter4$eval3393$fn__3396@6bb0b0a0> #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"} #{"have-money"})
 TRACE t3424: | | | | | | (achieve #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"} "have-money")
 TRACE t3424: | | | | | | => #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3423: | | | | | => #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "have-money" "son-at-home" "in-communication-with-shop"}
 executing give-shop-money
 TRACE t3422: | | | | => #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "shop-has-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3413: | | | => #{"shop-knows-problem" "car-needs-battery" "know-phone-number" "have-phone-book" "shop-has-money" "son-at-home" "in-communication-with-shop"}
 executing shop-installs-battery
 TRACE t3412: | | => #{"shop-knows-problem" "car-needs-battery" "car-works" "know-phone-number" "have-phone-book" "shop-has-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3425: | | (achieve #{"shop-knows-problem" "car-needs-battery" "car-works" "know-phone-number" "have-phone-book" "shop-has-money" "son-at-home" "in-communication-with-shop"} "son-at-home")
 TRACE t3425: | | => #{"shop-knows-problem" "car-needs-battery" "car-works" "know-phone-number" "have-phone-book" "shop-has-money" "son-at-home" "in-communication-with-shop"}
 TRACE t3411: | => #{"shop-knows-problem" "car-needs-battery" "car-works" "know-phone-number" "have-phone-book" "shop-has-money" "son-at-home" "in-communication-with-shop"}
 executing drive-son-to-school
 TRACE t3410: => solved
=> solved
The output is rather lengthy, but you can find your way through. Really great about tracing is, that you see all applications of the functions with their arguments and results printed hierachically, so that the caller of a function is less deeply nested. Therefore, it really helps in debugging and understanding code without manually adding debug statements or stepping through breakpoints etc. I am showing here the output when all functions are traced. Turning out tracing on a few functions make the output more readable, but you miss a bit about the whole picture.
As you see, it tries to achieve the goals in reverse order, because it has to solve the subproblems first. The first action it can execute is look-up-number. After this, it knows the telefone-number, can phone the shop, the shop can repair the car and it can solve the problem of driving the son to school because the car works.

1.5 Analysis

It turns out, that the GPS is not very general, at least our implementation is not. Norvig does a great analysis explaining the reasons why the program can not handle many situations. I will give a short summary of the discussion, describing the problems and a miminal explanation why the program fails.
  • Running around the block - GPS don't have any notion of actions, that add nothing to the :add-list, like 'running-around-the-block'. There has to be a state like "got-some-exercise" in the :add-list
  • Clobbered Sibling Goal - In the current implementation, GPS solves every goal independent and in sequence. That means, if solving the second problem undoes a previous goal, GPS will falsly return success. In the school-ops world, consider #{have-money son-at-school}. The program has to be modified, so that it checks after achieving all goals that the state is still a subset of the goal-state
  • Leaping before you look - sometimes, gps will fail, but executes some action before it recognizes that. In the current implementation, planning and execution are interleaved. This will be addressent by returning the list of actions instead of just printing the actions.
  • Recursive Subgoal Problem - suppose we would add another operator to the school-ops "ask-phone-number" with the precondition of being in communication with the shop. GPS will try to achieve know-phone-number with ask-phone-number, which requires to be in communication with the shop, which requires knowing the phone-number … It tries to solve the problem in terms of itself. To address this, we have to add a goal-stack, and let GPS give-up if it recognizes a circle.
  • Lack of Intermediate Information - As at the leaping before you look problem, it is better to return a list of actions instead of just solved. This makes it easier for other parts of the program to interact with GPS

1.6 Back to implementation - A more general version 2

Before we come to the implementation of the program, it helps to get a good debugging tool, so that we can see what it is doing:
(def ^:dynamic *dbg-ids*)
(defn debug
  ([id str] (debug id 0 str))
  ([id indent str]
   (when (contains? *dbg-ids* id)
    (do (dotimes [i indent]
          (print " "))
        (println str)))))
=> #'PAIP2clojure.Chapter4/debug
Here is the common-lisp code. It first transforms all the ops to a new form, which has an '(Executing action) in its add list.
   (defun executing-p (x)
  "Is x of the form: (executing ...) ?"
  (starts-with x 'executing))

  (defun starts-with (list x)
    "Is this a list whose first element is x?"
    (and (consp list) (eql (first list) x)))

(defun convert-op (op)
  "Make op conform to the (EXECUTING op) convention."
  (unless (some #'executing-p (op-add-list op))
    (push (list 'executing (op-action op)) (op-add-list op)))

(defun op (action &key preconds add-list del-list)
  "Make a new operator that obeys the (EXECUTING op) convention."
    (make-op :action action :preconds preconds
             :add-list add-list :del-list del-list)))

;;; ==============================

(mapc #'convert-op *school-ops*)

The new version of GPS will return the new state instead of printing the actions and removes all non-atoms from the state so that only (Executing action) forms are left
  (defvar *ops* nil "A list of available operators.")

(defstruct op "An operation"
  (action nil) (preconds nil) (add-list nil) (del-list nil))

(defun GPS (state goals &optional (*ops* *ops*))
  "General Problem Solver: from state, achieve goals using *ops*."
  (remove-if #'atom (achieve-all (cons '(start) state) goals nil)))

(defun achieve-all (state goals goal-stack)
  "Achieve each goal, and make sure they still hold at the end."
  (let ((current-state state))
    (if (and (every #'(lambda (g)
                        (setf current-state
                              (achieve current-state g goal-stack)))
             (subsetp goals current-state :test #'equal))

(defun achieve (state goal goal-stack)
  "A goal is achieved if it already holds,
  or if there is an appropriate op for it that is applicable."
  (dbg-indent :gps (length goal-stack) "Goal: ~a" goal)
  (cond ((member-equal goal state) state)
        ((member-equal goal goal-stack) nil)
        (t (some #'(lambda (op) (apply-op state goal op goal-stack))
                 (find-all goal *ops* :test #'appropriate-p)))))

(defun member-equal (item list)
  (member item list :test #'equal))

(defun apply-op (state goal op goal-stack)
  "Return a new, transformed state if op is applicable."
  (dbg-indent :gps (length goal-stack) "Consider: ~a" (op-action op))
  (let ((state2 (achieve-all state (op-preconds op) 
                             (cons goal goal-stack))))
    (unless (null state2)
      ;; Return an updated state
      (dbg-indent :gps (length goal-stack) "Action: ~a" (op-action op))
      (append (remove-if #'(lambda (x) 
                             (member-equal x (op-del-list op)))
              (op-add-list op)))))

(defun appropriate-p (goal op)
  "An op is appropriate to a goal if it is in its add list."
  (member-equal goal (op-add-list op)))
The function achieve-all abstracts away the (every #'achieve …) from the first version and checks whether the resulting state is still a subset of the goal-state. Also, it updates the current state for each application of achieve (destructively modifying..)
Also note, that in apply-op append and remove-if are used. They are needed because in this version, the order of the conditions in the current state counts (because they contain (Executing action) elements.
Again, I will not follow the implementation given in PAIP. Especially I want to avoid the side-effects in achieve-all and the mixing of actual conditions and (Executing action) forms, which would prevent to represent states with sets. It turned out, that I needed only three small changes from the first version of the program:
  • The state is now divided in the current-state and a list-of-actions taken so far. Thanks to clojure's destructuring, it is easy to take the state apart again.
  • there is a goal-stack which contains all goals tried so far. achieve gives up if it encounters a goal that is in the goal-stack to avoid stack-overflow-errors.
  • every-time an action is executed, it is appendet to the list-of-actions.
 (use 'clojure.set)
 (use 'auto-declare.core)

(with-auto-declare _  
   (defn GPS [state goals]
     (let [[new-state list-of-actions] (_every-accum? (partial achieve []) [state []] goals)]
       (if (nil? new-state)

   (defn achieve
     "return the new-state after the goal is achieved or nil
      if it could not be archieved"
     [goal-stack [current-state list-of-actions] goal]
     (debug :gps (count goal-stack) (str "Goal " goal))
     (cond (contains? current-state goal) [current-state list-of-actions]
           (contains? goal-stack goal) [nil list-of-actions]
           :else (some (partial _apply-op goal-stack goal [current-state list-of-actions])
                       (filter #(_appropriate? goal %) *ops*))))

   (defn appropriate? [goal op]
     (contains? (:add-list op) goal))

   (defn every-accum? [func start coll]
     (reduce #(if (nil? %1)
                (func %1 %2)) start coll))

   (defn apply-op [goal-stack goal state op]
     (debug :gps (count goal-stack) (str "Consider: " (:action op)))
     (let [[new-current-state new-list-of-actions](every-accum? (partial achieve (conj goal-stack goal))
                                                                state (:preconds op))]
       (if (nil? new-current-state)
         [nil new-list-of-actions]
         (do (debug :gps (count goal-stack) (str "Action " (:action op)))
             [(-> new-current-state (difference (:del-list op)) (union (:add-list op)))
              (conj new-list-of-actions (:action op))])))))

=> #'PAIP2clojure.Chapter4/apply-op
This implementation solves the problems mentioned above. The next parts of Chapter 4 show how the program performs in new domains. Feel free to port the ops in these domains to clojure and report whether the program worked or not. Here is one example domain: monkey and bananas:
  (def ^:dynamic *banana-ops*
  [{:action "climb-on-chair"
    :preconds #{"chair-at-middle-room" "at-middle-room" "on-floor"}
    :add-list #{"at-bananas" "on-chair"}
    :del-list #{"at-middle-room" "on-floor"}}
   {:action "push-chair-from-door-to-middle-room"
    :preconds #{"chair-at-door" "at-door"}
    :add-list #{"chair-at-middle-room" "at-middle-room"}
    :del-list #{"chair-at-door" "at-middle-room"}}
   {:action "walk-from-door-to-middle-room"
    :preconds #{"at-door" "on-floor"}
    :add-list #{"at-middle-room"}
    :del-list #{"at-door"}}
   {:action "grasp-bananas"
    :preconds #{"at-bananas" "empty-handed"}
    :add-list #{"has-bananas"}
    :del-list #{"at-door"}}
   {:action "drop-ball"
    :preconds #{"has-ball"}
    :add-list #{"empty-handed"}
    :del-list #{"has-ball"}}
   {:action "eat-bananas"
    :preconds #{"has-bananas"}
    :add-list #{"empty-handed" "not-hungry"}
    :del-list #{"has-bananas" "hungry"}}])

(def ^:dynamic *ops* *banana-ops*)
=> #'PAIP2clojure.Chapter4/*ops*
(binding [*dbg-ids* #{:gps }]
  (GPS #{"at-door" "on-floor" "has-ball" "hungry" "chair-at-door"}
 Goal not-hungry
 Consider: eat-bananas
  Goal has-bananas
  Consider: grasp-bananas
   Goal empty-handed
   Consider: drop-ball
    Goal has-ball
   Action drop-ball
   Goal at-bananas
   Consider: climb-on-chair
    Goal chair-at-middle-room
    Consider: push-chair-from-door-to-middle-room
     Goal chair-at-door
     Goal at-door
    Action push-chair-from-door-to-middle-room
    Goal at-middle-room
    Goal on-floor
   Action climb-on-chair
  Action grasp-bananas
 Action eat-bananas
=> ["drop-ball" "push-chair-from-door-to-middle-room" "climb-on-chair" "grasp-bananas" "eat-bananas"]

The Chapter in PAIP ends with a discussion of how general the GPS really is. It turns out, that it has severe limitations and I encourage everyone to read the sections in the book. Many of this issues will be addressed in later chapters using more sophisticated techniques, such as full-fledged search and backtracking, like prolog does.

Puh, that was more work than I thought. But I hope that I managed to translate it to ideomatic clojure which is not to hard to follow. Feel free to comment, if you have questions, anything is not clear, you enounter bugs or have some other advice.

Chapter 5 is next to come. It contains an implementation of the ELIZA program.

Date: 2012-06-01T20:54+0200
Author: Maik Schünemann
Org version 7.8.09 with Emacs version 23
Validate XHTML 1.0

Keine Kommentare:

Kommentar veröffentlichen