Manual for HACKER
Table of Contents
1 Getting started
1.1 About the program
HACKER
is a program that models the way humans think about
strategies, and how they re-organize that knowledge as they progress
from novice to expert.
The setting for HACKER
is a world full of building blocks. When you
ask HACKER
to build various structures out of blocks, it will
attempt to comply by making and executing a plan. Because HACKER
is
initially a novice, however, it will make many mistakes along the
way. (For example, it might try to grab a block at the bottom of a
tall pile, or make a plan where some of the steps are out of order.)
Whenever HACKER
notices a mistake, it will work to figure out
exactly what went wrong, then modify the plan to avoid making similar
mistakes in the future. In this way, HACKER
learns and abstracts.
HACKER
has several kinds of knowledge:
- Knowledge about building blocks, and the sort of things you can do with them. For example, you can stack one block on top of another.
- General knowledge about strategies. For example, one strategy is: to do something to an entire group, you can individually do something to each member of the group.
- General knowledge about things that can go wrong. For example, one type of mistake is to try to do something before you've made all the necessary preparations.
It is important to notice that these types of knowledge are entirely
separate within HACKER
. This separation of knowledge means that
HACKER
is domain-general: if you want to make your own HACKER
program which learns about a new domain, all you have to do is replace
the blocks world knowledge with knowledge about some other domain. The
rest of HACKER
's knowledge — about strategies and bugs — will
apply just as well to this new domain.
1.2 Installing HACKER
HACKER
is written in the Clojure programming language. To run it,
you will need:
- Java
- Clojure
- A copy of the program, which you can download here.
Once you have these things, you can run HACKER
by opening a terminal
window on your computer and typing
java -cp clojure-1.7.0.jar clojure.main
to initialize the Clojure read-eval-print loop. Then, run
(in-ns ai.logical.hacker)
to enter the HACKER
namespace, which import all of the HACKER
definitions and enable you to run all of the HACKER
functions.
1.3 Activities
To test out the program, run the sample demonstration:
(run-demo-1)
This will set up a scenario that looks like this:
Then, the function will ask HACKER
to put block A onto block B. Here
is the definition of run-demo-1
; it sets up the scene and then poses
the problem.
(defn run-demo-1 [] (let [scene (-> (scene-init) (add-block (block 'A 1)) (add-block (block 'B 1)) ) )] (hacker-solve scene ;; Set up the scenario '[make [on A TABLE]] '[make [on B TABLE]] ;; Ask to put make block A on block B. '[make [on A B]] ) ))
Without hesitation, HACKER
performs the movement to achieve the
goal. The scene now looks like:
The second demonstration becomes slightly more complex. To run the second demonstration, call
(run-demo-2)
Here, we have a new scene set up like this:
HACKER
is asked to make block B on block C.
(defn run-demo-2 ([] (t2 true)) ([restart?] (when restart? (restart!)) (let [scene (-> (scene-init) (add-block (block 'A 1)) (add-block (block 'B 1)) (add-block (block 'C 1)) )] (hacker-solve scene ;; Set up the scenario '[make [on B TABLE]]] '[make [on C TABLE]]] '[make [on A B]]] ;; Ask to make block B on block C. '[make [on B C]]] ) )))
HACKER
will try to reach for block B and move it, but will fail
because block A is stacked on top of it. This causes HACKER
to call
its bug-classifier to see what the matter is.
2 How HACKER
works
The following sections describe how each line of code in HACKER
works.
2.1 What novice HACKER
knows
HACKER's knowledge is stored in five databases:
- The Answer Library
- which contains programs that
HACKER
has written to solve its problems. Sometimes,HACKER
will modify programs in the answer library to fix a bug. - The Hacker Notebook
- where
HACKER
keeps comments about the programs that it has written, so that it can remember what each line is for. Referring to the Hacker Notebook helps preventHACKER
from making certain kinds of mistakes when revising programs. - Programming techniques
- which contains very general
problem-solving strategies.
HACKER
uses these when it doesn't know how to solve a problem already and has to devise a new strategy. - Patch types
- which is a separate list of strategies for fixing bugs in code.1
- Blocksworld knowledge
- is domain-specific knowledge about how building blocks work, and the sort of things you can do with them.
The Answer Library and Hacker Notebook in particular will change as
HACKER
gains more blocks-world expertise. The following section of
code sets up these variables, and describes their initial values
(i.e. what HACKER
knows in the very beginning).
(def answer-library (atom [])) (def hacker-notebook (atom [])) (def blocksworld-knowledge (atom [])) (def programming-techniques (atom [])) (def patch-types (atom [])) (def ^:const default-answer-library (vector '[to [make [on :a :b]] [program make-on [line :line1 [put-on :a :b]]]] '[to [test [place-for :x :y]] [program wrapper-test-place-for [line :line2 [test-place-for :x :y]]]] '[to [test [cleared-top :x]] [program wrapper-test-cleared-top [line :line3 [test-cleared-top :x]]]] '[to [test [not [= :x :y]]] [program wrapper-test-inequality [line :line4 [test-not= :x :y]]]] '[to [test [on :x :y]] [program wrapper-test-on [line :line4 [test-on :x :y]]]] '[to [test [possibly :condition]] [program wrapper-possibly [line :line4 [possibly :condition]]]] )) (def ^:const default-hacker-notebook (vector '[goal make-on [make [on :a :b]]] '[purpose :line1 [make [on :a :b]] make-on] )) (def ^:const default-blocksworld-knowledge (vector ;; To put x on y, there must be a place for x on y. '[fact [prerequisite [put-on :x :y] [place-for :x :y]]] ;; An :expression requires the top of :object to be cleared ;; if executing the :expression causes the :object to move. ;; ("have" is an existential quantifier.) '[fact [prerequisite :expr [cleared-top :object]] [have [] [moves :expr :object]]] ;; Executing [put-on :x :y] causes :x to move. '[fact [moves [put-on :x :y] :x]] ;; [cleared-top :x] means that there is no object :y on :x. '[fact [meaning-of [cleared-top :x] [not [exists [:y] [on :y :x]]]]] ;; To show that x is not on y, it is enough to show that x is on ;; some different object z. '[fact [suffices-for [not [on :x :y]] [exists [:z] [not [= :z :y]] [on :x :z]]]] )) (def ^:const default-programming-techniques (list ;; A test for [not [= :x :y]] is just [not [= :x :y]] '[fact [code [test [not [= :x :y]]] [not [= :x :y]]]] ;; A test for whether an expression is possible is to check whether ;; its negation is protected. '[fact [code [test [possibly :expr]] [not [protected? [not :expr]]]]] ;; If an argument has a synonymous meaning, you can replace the ;; argument with its synonym. '[fact [code [:function :argument] :script] [have [:meaning] [meaning-of :argument :meaning] [replace-code :script [:function :meaning]]]] ;; If an argument has a sufficient condition, you can replace the ;; argument with the sufficient condition. '[fact [code [:function :argument] :script] [have [:condition] [suffices-for :argument :condition] [replace-code :script [:function :condition]]]] ;; To remove all instances of variables matching pattern, iterate ;; over all variables matching pattern, and make them not-pattern. '[fact [code [achieve [not [exists :vars :expr]]] [until :vars [cannot [assign :vars :expr]] [make [not :expr]]]]] ;; To make one of the objects satisfy a condition, choose one of ;; the objects for which it is possibly to satisfy the condition, ;; then make it satisfy the condition. '[fact [code [make [exists :vars :qualification :action]] [choose :vars [and [test :qualification] [test [possibly :action]]] [make :action]]] ] ) ) (def ^:const default-patch-types (list '[fact [patch [!prerequisite-missing :program :line# :prerequisite] [add-newline! :program [achieve :prerequisite] :line# ]]] ))
To reset HACKER
's knowledge to its default state, you can run the
function restart!
.
(defn restart! [] (swap! answer-library (constantly default-answer-library)) (swap! hacker-notebook (constantly default-hacker-notebook)) (swap! blocksworld-knowledge (constantly default-blocksworld-knowledge)) (swap! programming-techniques (constantly default-programming-techniques)) (swap! patch-types (constantly default-patch-types)) )
2.2 Setting up a blocks world scene
A few simple functions are used to set up blocks world scenes.
(def *table* 'TABLE) (defn block ([name width] {:width width :name name}) ([width] (block width (gensym "block")))) (defn table [] {:width Double/POSITIVE_INFINITY :name *table*}) (defn scene-init "Create a new blocks world scenario, containing only a table." [] {:facts [] :blocks {*table* (table)}}) (defn add-block ([scene block] (alter-val scene :blocks #(assoc % (:name block) block))))
For example, you can use Clojure's threading macro to set up a scene
containing a TABLE
2, two small blocks (A,B) of length one and one large block
(C) of length three, as follows:
(-> (scene-init)
(add-block (block 'A 1))
(add-block (block 'B 1))
(add-block (block 'C 3)))
As created in this scene, the blocks are currently "floating" — not
resting on any particular object. You can ask HACKER
to put a block
on the table using a command like the following (more on this later):
(hacker-solve your-scene-here '(make (on A TABLE)))
2.3 How HACKER
represents problems
2.3.1 Every problem has a scene and a callstack.
2.3.2 The callstack
Because problems often generate subproblems, HACKER
makes use of a
simulated call stack
containing function
pointers.
Basically, the stack consists of a list of subroutines that are currently running. A subroutine is a program (with variables filled in), along with a number which tells you which line of the code is currently being run. Every time a program calls another subroutine, that subroutine gets added to the top of stack. The top-most subroutine is the one that is currently active.
There are several functions for interrogating or manipulating the stack:
(def current-program (comp peek :callstack)) (defn current-line [state] (let [[program-name line# [& lines]] (current-program state)] (nth lines line#))) (defn prev-line [state] (let [[program-name line# [& lines]] (current-program state)] (nth lines (dec line#)))) (defn current-program-name [state] (let [[program-name line# [& lines]] (current-program state)] program-name)) (defn goto-next-line "Return a new state object where the latest function pointer has advanced one line forward." [state] (let [[program-name line# [& lines]] (current-program state)] ;; TODO: perhaps clear the "return" and/or "error" flags. (alter-val state :callstack (fn [stack] (conj (pop stack) [program-name (inc line#) lines]))))) (defn end-of-program? [state] (let [[program-name line# [& lines]] (current-program state)] (<= (count lines) line#)))
As part of its debugging process, HACKER
may need to modify either
programs stored on the state's callstack or programs stored in its
Answer Library.
(defn add-newline! [state program-name expr purpose] (let [new-line# (keyword (gensym "line"))] (swap! answer-library (constantly (for [[_ goal [_ name & lines :as program] :as entry] @answer-library] (if-not (= name program-name) entry ['to goal (apply conj ['program] program-name ['line new-line# expr] lines)]) ))) (swap! hacker-notebook #(conj % ['purpose new-line# expr purpose])) (assoc state :return true) )) (defn replace-line-here "Replace the current line in the currently running program with expr, but don't alter the code for the program in the database." [expr state] (let [ [_ line# _](current-line state)] (let [state* (alter-val state :callstack (fn [stack] (conj (pop (vec stack)) (let [[_ _ lines :as top] (last stack)] (conj (vec (take 2 top)) (for [[_line line## goal] lines] [_line line## (if (= line## line#) expr goal)]))))))] state* ))) (defn replace-line! "Modify the program with the given name by replacing the line named line# with the expression in expr." [program-name line# expr] (swap! answer-library (constantly (for [[_to goal [_program name & lines :as program] :as entry] @answer-library] (if-not (= name program-name) entry [_to goal (apply conj [_program name] (for [l lines] (if (= line# (second l)) [(first l) (second l) expr] l ) ))] )))))
2.3.3 Errors and return values
States may optionally have an error that is currently being thrown, and/or a current return value. Retrieve them using standard hash-map keys:
(:error state) (:return state)
You can also check to see whether the state's most recent subroutine
has returned successfully by using the predicate
successful-return?
. As you can see, successful-return?
returns
true if the state hasn't thrown an error and has a true(ish) return
value.
(defn successful-return? "Returns true if the state is not in error mode, and if (pred return-val) is true." ([state] (successful-return? state (comp not not))) ([state pred] (and (not (:error state)) (pred (:return state)))))
2.4 Solving a problem
2.4.1 The execute
function decides what to do with the current problem
The heart of HACKER
is the program execute
. Execute takes a
particular state and decides what to do with it:
If the state has an error,
HACKER
passes it to the bug classifier by callingexecute-debug-classifier
.If the topmost program on the stack has finished running,
HACKER
pops it off the stack and executes the next program.Otherwise,
HACKER
looks at the call stack to see what its current problem is.- First,
HACKER
checks to see if the problem is something it can solve in one step. (There are certain basic actions, called primitive operations, whichHACKER
can immediately perform. These are actions such as putting a block onto another block.) - If the problem is too complex to be solved in one step,
HACKER
checks the Answer Library to see if it already knows a program that can solve the current problem. IfHACKER
can match the current problem to a solution in its Answer Library,HACKER
tries out the solution. - If
HACKER
has tried all of the solutions in its library and none of them has worked,HACKER
passes the state to the code writer by callingexecute-code-writer
. The code-writer attempts to write a program to solve the current problem. - If both the Answer Library and the code writer fail to produce a
solution,
HACKER
is stuck.
- First,
(defn execute ([state] (execute state false)) ([state silently?] (cond (:error state) (execute-bug-classifier state) (empty? (:callstack state)) state (end-of-program? state) (recur (alter-val state :callstack pop) silently?) :else (let [[program-name line# [& lines]] (peek (get state :callstack)) goal (nth (current-line state) 2) state* (goto-next-line state) _ (when (and (not silently?) (#{'make} (first goal)) ) (print-current-line! state)) ] (cond (primitive-operator? goal) (recur (execute-primitive-operator state goal) silently?) (= 'have (first goal)) (parse-existential state goal) :else ;; find and iterate over all canned subroutines ;; for solving this problem (let [ strategies (->> @answer-library (map (fn [answer] (if-let [ptn (pattern-match (take 2 answer) ['to goal])] (pattern-bind ptn answer)))) (remove nil?) (map (fn [[_ _ [_ program-name & lines]]] (alter-val state* :callstack #(conj % [program-name 0 lines])) )) ) ] (if (empty? strategies) ;; TODO: ask the code-writer to code up a solution ;; TODO : call exec on this so that it gets ;; passed to the debugger before returning (execute-code-writer state) ;; TODO: Iteratively evaluate each strategy, ;; returning the first result without a fatal ;; error. If all results have fatal errors, throw ;; a fatal error. (execute (first strategies)) ) ) ) )))) (defn hacker-solve ([scene expr & exprs] (let [state {:scene scene :callstack [['USER_INPUT 0 (for [e (cons expr exprs)] ['line (gensym "line") e]) ]]}] (execute state) )))
2.4.2 Performing basic one-step actions (primitives)
(defn primitive-operator? [expr] (->> expr first #{'possibly 'put-on 'test-not= 'test-place-for 'test-cleared-top 'test-on 'add-newline! 'until 'assign 'cannot 'choose 'and })) (defn execute-primitive-operator [state [f & args :as expr]] (let [;;proceed one line forward in the current subroutine state* (goto-next-line state)] (cond (= f 'and) (do ;;(println "AND" (vec args)) (assoc state* :return (every? successful-return? (map (partial (eval 'hacker-solve) (:scene state)) args))) ) (= f 'not) (assoc state* :return (not (successful-return? (execute (:scene state) (first args))))) (= f 'test-not=) (assoc state* :return (not (apply = args))) (= f 'choose) (execute-choice state* expr) (= f 'possibly) (assoc state* :return true) ;;; TODO: Protection mechanism (= f 'until) ((eval 'execute-until-loop) state (cons f args)) (= f 'assign) ((eval 'execute-assign) state (cons f args)) (= f 'cannot) (let [x ((eval 'hacker-solve) (:scene state) (first args))] (if (successful-return? x) (assoc state* :error [!failed-conditional (first args)] :return (:return x) ) state* )) :else (apply (eval f) state* args)) ))
The following code supplies blocks world utility functions which comprise the primitive functions:
(defn toppings "Return a list of names of objects on the given surface." [scene surface] (filter (fn [expr] (when-let [[f x y pos] expr] (and (= 'on f) (= surface y) ;; todo: reference by name/object ))) (:facts scene))) (defn vacancies ([scene surface object] (let [intervals (vacancies scene surface) width (get-in scene [:blocks object :width] 0) ] (->> intervals (filter (fn [[a b]] (<= width (- b a)))) (map (fn [[a b]] (if (Double/isInfinite b) a (/ (+ a b (- width)) 2) )))))) ([scene surface] (let [toppings (toppings scene surface) object-endpoints (map (fn [[_ obj _ n]] [n (+ n (get-in scene [:blocks obj :width] 0))]) (sort-by second toppings)) left-endpoint (when object-endpoints (apply min 0 (map first object-endpoints))) right-endpoint (when object-endpoints (apply max Double/POSITIVE_INFINITY (map second object-endpoints))) fenceposts (flatten (concat [0] object-endpoints [(get-in scene [:blocks surface :width])])) ] (remove (partial apply =) ;; remove zero-width intervals (partition 2 fenceposts)) ))) (defn test-place-for [state object surface] (assoc state :return (-> state :scene (vacancies surface object) first nil? not))) (defn test-cleared-top [state surface] (assoc state :return (-> state :scene (toppings surface) empty?))) (defn test-on [state x y] (->> state :scene :facts (filter (partial pattern-match ['on x y])) first (assoc state :return)) )
(defn place-for? [scene object surface] (first (vacancies scene surface object))) (defn put-on [state object surface] (println "* Putting" object "on" surface) (let [;;_ (println object surface) scene (get state :scene) tops (toppings scene object) ] ;;;(println "PUT ON" object surface) (if-let [[_ stuff] (first tops)] ;; if the object has stuff on it ;; Can't move an object if it has stuff on it. (assoc state :error [!unsatisfied-prerequisite ['not ['on stuff object]]]) (if-let [x (first (vacancies scene surface object))] (assoc state :scene (-> scene ;; remove object from all other surfaces (alter-val :facts (partial remove #(let [[fn x] %] (and (= 'on fn) (= x object))))) ;; place object on new surface (alter-val :facts #(conj % (list 'on object surface x) ;; (list 'on object surface) ;; TODO: DXH DANGER HERE )) )) (assoc state :error [!fatal-error 'ERROR_NO_SPACE_ON_BLOCK surface])))))
2.4.3 Searching for variables that make an expression true
To solve an assignment operator such as
(assign [:x :y] (on :x :y))
HACKER
will first find all possible interpretations of the variables
:x
and :y
. Then, it will check each interpretation to see if any
is true3. It will return
the first set of successful bindings, or nil if no solution exists.
The find-all-interpretations
routine handles to the problem of
finding all possible values of the variables.
(defn unbound-vars "Returns a list of all variables in the expression." [expr] (if (coll? expr) (distinct (mapcat unbound-vars expr)) (when (variable? expr) [expr]) )) (defn find-all-interpretations "Returns a list of all possible ways to bind the unbound variables in expr to objects in state, without checking whether the bindings make logical sense." ([state expr] (let [ vars (unbound-vars expr) universe (-> state :scene :blocks keys) ] (loop [open vars accumulated [expr]] (if (empty? open) accumulated (recur (rest open) (for [e accumulated x universe] (pattern-bind {(first vars) x} e))))))))
(defn execute-assign [state expr] (let [[_assign variables expr*] expr state* (goto-next-line state) candidates (->> expr* (find-all-interpretations state*) (map (comp (partial (eval 'hacker-solve) (:scene state*)) (partial vector 'test))) (filter :return)) candidate (when-let [x (first candidates)] (->> x :return (pattern-match expr*) (filter-keys (set variables)))) ;;_ (println "ASSIGN" expr* (->> expr* (find-all-interpretations state))) ] (assoc state* :return candidate) ))
The assignment operator is a kind of existential quantifier. Other existential quantifiers work in a similar way:
(defn parse-existential [state goal] (if-let [[_ vars expr] goal] (let [matching-facts (map (comp #(pattern-align % expr) second) @blocksworld-knowledge)] (assoc state :return (some (comp not nil?) matching-facts)))))
The source code for the original HACKER
was written in the
domain-specific PLANNER
language. This language was able to
simultaneously search for bindings that made an expression true, while
also modifying the expression based on the bindings it found. To
replicate this functionality in a stateless/functional way, I wrote
the parse-in-environment
function.
(To see an example of parse-in-environment
in action, run the demo
program.)
(defn parse-in-environment "If the fact has any qualifications or environmental conditions, returns a (potentially empty) list of all the ways of making them hold. Otherwise, returns a singleton list containing fact." [state fact] (let [[_fact goal & environment] fact] (if-let [e (first environment)] (let [;;_ (clojure.pprint/pprint ["ENV" fact e] ) [_have vars condition & exprs :as existential] e ] ;; SEARCHING FOR SOLUTIONS TO EXISTENTIAL QUANTIFIER (->> @blocksworld-knowledge (map (fn [[_fact x :as entry]] (when-let [bind (pattern-match* condition x)] (if-let [e (first exprs)] (let [reverse-bind (exchange-keyvals (apply dissoc bind vars)) command (pattern-bind reverse-bind (pattern-bind bind (thread-in goal e))) ;_ (println [condition x (pattern-match* condition x)]) ] (apply (eval (first command)) (rest command)) ) goal) ) )) (remove nil?) )) [goal] )))
2.4.4 Diagnosing and repairing a malfunctioning program
Whenever the main loop execute
runs into a problem, it activates
sequence of repair actions. The execute-bug-classifier
function is
the first step in repairing a malfunctioning program; it identifies
the source of the bug. Once the source of the bug is determined, the
patch writer is employed to fix the specific problem.
(defn execute-bug-classifier [state] (cond (= !failed-conditional (first (:error state))) state (= !fatal-error (first (:error state))) state :prerequisite-missing (let [_ (println "error" (:error state)) [_ line# goal :as badline] (prev-line state) matching-prereqs (->> @blocksworld-knowledge (map (fn [answer] (when-let [ptn (pattern-match (take 2 (second answer)) ['prerequisite goal])] (pattern-bind ptn answer) ))) (remove nil?) ) test-prereqs (for [fact* (take 2 matching-prereqs) fact (find-all-interpretations state fact*)] (let [[_fact [_prereq _ goal*] & preconditions] fact] ;; TODO: LESS KLUDGY PRECONDITION CHECK (when (or (empty? preconditions) (successful-return? (hacker-solve (:scene state) (first preconditions)))) [(hacker-solve (:scene state) ['test goal*]) goal*]) )) ] ;; (map (partial take 2) @blocksworld-knowledge)) (if-let [bug-cause (some (fn [[ret goal]] (and (successful-return? ret not) goal)) (remove nil? test-prereqs))] (execute-patch (assoc state :error ['!prerequisite-missing (current-program-name state) (second (prev-line state)) bug-cause])) (alter-val state :error (partial vector !fatal-error))) ;;state )))
The patch writer makes use of the Patch Types library to find a
solution for the bug in the currently running program. To make the
repairwork general, the patch writer employs
current-program-bindings
; current-program-bindings
returns a
hashmap describing the relationship between the instantiated program
running on the stack (e.g. (make-on A B)
) and its general instance
in the Answer Library (e.g. (make-on :x :y)
).
(defn find-program-by-name [program-name] (->> @answer-library (map (fn [[_ _ [_ name & _ :as program]]] (when (= name program-name) program))) (some identity))) (defn refresh-program "Replace the top program on the stack with its most up-to-date version, and reset the function pointer to the start of that program." [variables state] (if-let [[_ program-name & lines] (pattern-bind (exchange-keyvals variables) (find-program-by-name (current-program-name state))) ] (-> state (alter-val :callstack (fn [stack] (conj (pop stack) [program-name 0 (map vec lines)]))) (dissoc :error) ) state )) (defn current-program-bindings "Return a list of bindings made by the top function pointer on the stack." [state] (let [program (current-program state) [_ _ & lines] (find-program-by-name (first program))] (pattern-match* (nth program 2) lines) ))
(defn execute-patch [state] (println "patching error: " (:error state)) (let [[bug-type program-name line# arg :as error] (:error state) variables (current-program-bindings state) var-error (pattern-bind variables error) matching-patches (->> @patch-types (map (fn [answer] (when-let [ptn (pattern-match (take 2 (second answer)) ['patch var-error])] (pattern-bind ptn answer)))) (remove nil?) ) ] (if-not variables (assoc state :error [!fatal-error "Patcher can't unbind variables."]) (-> (some identity matching-patches) ;; TODO: search over /all/ patches second (nth 2) ((partial hacker-solve (:scene state))) :return (when (execute (refresh-program variables state))) ) )))
3 Appendix
3.1 Utility functions
3.2 Pattern-matching
HACKER
uses pattern-matching to match specific problems with general
solutions. Matching takes place between two expressions. A successful
match will return a map of bindings. For example, you can match the
following two expressions:
(pattern-match '(put-on :x :y) '(put-on A B))
You can use the function pattern-bind
to replace every variable in
an expression with its bound counterpart.
(def variable? keyword?) (defn merge-bindings [bind-1 bind-2] (when (and bind-1 bind-2) (loop [b1 bind-1 b2 bind-2 accumulate nil] (cond (empty? b1) (merge b2 accumulate) (empty? b2) (merge b1 accumulate) :else (let [k (first (keys b1))] (when (or (not (get b2 k)) (= (get b1 k) (get b2 k))) (recur (dissoc b1 k) (dissoc b2 k) (assoc accumulate k (get b1 k))))))))) (defn pattern-match "Match two s-expressions, returning a map of bindings. Pattern variables are indicated by keywords." ([ptn expr] (pattern-match ptn expr {})) ([ptn expr bindings] (cond (and (coll? ptn) (coll? expr)) (if (or (empty? ptn) (empty? expr)) bindings (if-let [x (pattern-match (first ptn) (first expr) bindings)] (recur (rest ptn) (rest expr) x))) (variable? ptn) (merge-bindings bindings {ptn expr}) (and (not (coll? expr)) (not (coll? ptn))) (when (= ptn expr) bindings) ))) (defn pattern-match* "Match two s-expressions, returning a map of bindings. Pattern variables are indicated by keywords --- or by single-letter symbols." ([ptn expr] (pattern-match* ptn expr {})) ([ptn expr bindings] (cond (and (coll? ptn) (coll? expr)) (if (or (empty? ptn) (empty? expr)) bindings (if-let [x (pattern-match* (first ptn) (first expr) bindings)] (recur (rest ptn) (rest expr) x))) (or (variable? ptn) (and (symbol? ptn) (= 1 (count (str ptn))))) (merge-bindings bindings {ptn expr}) (and (not (coll? expr)) (not (coll? ptn))) (when (= ptn expr) bindings) ))) (defn pattern-bind [bind expr] (if (coll? expr) ;; (if (every? variable? expr) expr ;; hack for existential quantifiers (map (partial pattern-bind bind) expr) ;;) (get bind expr expr))) (defn pattern-bind* [bind expr] (if (coll? expr) (if (= 'have (first expr)) (conj (pattern-bind* bind (drop 2 expr)) (second expr) (first expr)) (map (partial pattern-bind* bind) expr)) (get bind expr expr))) (defn pattern-align "Attempts to match the pattern with the expression using pattern-match. If successful, returns the bound match. If unsuccessful, returns nil." [ptn expr] (when-let [b (pattern-match ptn expr)] (pattern-bind b expr))) (defn pattern-search "Returns a list of all expressions in coll which match ptn, having been bound." [ptn coll] (remove nil? (map (partial pattern-align ptn) coll)))
Footnotes:
For example, if the bug is that you try to take an action before making all the necessary preparations, you can add a new line to the beginning of the program where you make the necessary preparations first.
All scenes contain an infinitely wide TABLE
by default
This is a generate-and-test paradigm.