Running Register Machines
Draft of 2016.07.28
May include: GP ↘ artificial life ↗ learning in public ↖ &c.
Continued from “Making a Register Machine”
I mentioned last time how I had a bad feeling about my tests or something. And I was right!
You’ll recall that when I was testing random-program-step
, I had stubbed out the part where a random entry in the global all-functions
collection was picked, so I wouldn’t need to worry about randomness in my tests. It turns out that when you stub something, like I did with (provided (rand-nth all-functions) => [:FOO 2]...
in this:
(fact "random-program-step" (random-program-step all-functions 10 12) => (->ProgramStep :FOO [17 17] 7) (provided (rand-nth all-functions) => [:FOO 2], (rand-int 22) => 17 (rand-int 12) => 7))
Well, Clojure and Midje are perfectly happy to let (rand-nth all-functions)
return whatever the hell you say it should. My choice of [:FOO 2]
worked fine in the test, because as intended it always returns that value, but guess what? When you actually call (rand-nth all-functions)
, an exception is raised because I wrote all-functions
as a Clojure hashmap
:
Expected: {:connectors [14 5 6], :program [:foo], :read-only [9 8 7]}::artificial_chemistry.core.RegisterMachine Actual: java.lang.UnsupportedOperationException: nth not supported on this type: PersistentArrayMap
Took me a little while scratching my head to figure that one out. This is, I believe, one of the main reasons people question whether it’s a good idea to ever stub out a function in a test. I’m still willing to overwrite random functions when they appear, since I totally trust the random number generators work as intended and I’m not here to test those, and because it’s far easier to write a unit test when you know the answer deterministically. But one should at least write a semantically correct test. That’s on me.
OK, so there are changes as a result. In fact, I realize I haven’t shared the code at all here, so let me display it for you now, in all its rangy glory.
Here is the library so far (after my fix for the stupid stub trick). It is able to construct a RegisterMachine
record, make a ProgramStep
record (including a random one, on demand), and invoke
a ProgramStep
in the context of an arbitrary RegisterMachine
.
(ns artificial-chemistry.core (:require [clojure.math.numeric-tower :as math])) (defrecord RegisterMachine [read-only connectors program]) (defn value-of "Takes a RegisterMachine record and index. The index refers to the index in the _concatenated_ `:read-only` and `connectors` vectors. Returns the value stored there." [rm idx] (nth (concat (:read-only rm) (:connectors rm)) idx)) (defn write-value "Takes a RegisterMachine record, index, and new value. Updates the value stored in the `:connectors` vector at that index." [rm idx number] (assoc-in rm [:connectors idx] number)) (defrecord ProgramStep [function args target]) (defn pdiv "Protected division; returns 1.0 when division by zero is attempted" [dividend divisor] (if (zero? divisor) 1.0 (/ dividend divisor))) (defn pow "Exponentiation" [base exponent] (math/expt base exponent)) (defn rm-not "logical not on a numerical value" [n] (if (zero? n) 1.0 0.0)) (defn rm-and "logical `and` on numerical values" [n1 n2] (let [n1z (not (zero? n1)) n2z (not (zero? n2))] (if (and n1z n2z) 1.0 0.0) )) (defn rm-or "logical `or` on numerical values" [n1 n2] (let [n1z (not (zero? n1)) n2z (not (zero? n2))] (if (or n1z n2z) 1.0 0.0) )) (def all-functions [ [+' 2], [*' 2], [-' 2], [pdiv 2], [pow 2], [rm-and 2], [rm-or 2], [rm-not 1]]) (defn random-program-step [functions readonly connectors] (let [readable (+ readonly connectors) [which-fxn arity] (rand-nth all-functions)] (->ProgramStep which-fxn (into [] (take arity (repeatedly #(rand-int readable)))) (rand-int connectors) ) )) (defn invoke [ps machine] (let [indices (into [] (concat (:read-only machine) (:connectors machine))) values (into [] (map indices (:args ps))) result (apply (:function ps) values)] (assoc-in machine [:connectors (:target ps)] result) ))
And here are the tests, which exercise the stuff I’ve noticed, and (as of this writing) all seem to pass:
(ns artificial-chemistry.core-test (:use midje.sweet) (:use [artificial-chemistry.core])) (fact "I can make a new RegisterMachine" (:read-only (->RegisterMachine [1 2 3] [4 5 6] [:foo])) => [1 2 3] (:connectors (->RegisterMachine [1 2 3] [4 5 6] [:foo])) => [4 5 6] (:program (->RegisterMachine [1 2 3] [4 5 6] [:foo])) => [:foo] ) (fact "I can read `:read-only` and `:connectors` registers" (let [testing (->RegisterMachine [1 2 3] [4 5 6] [:foo])] (value-of testing 0) => 1 (value-of testing 3) => 4 (value-of testing 5) => 6 )) (fact "I can write to `:connectors` registers" (let [testing (->RegisterMachine [1 2 3] [4 5 6] [:foo])] (:connectors (write-value testing 1 99.99)) => [4 99.99 6] )) (fact "I can make a new ProgramStep" (:function (->ProgramStep '+ [9 12] 3)) => '+ (:args (->ProgramStep '+ [9 12] 3)) => [9 12] (:target (->ProgramStep '+ [9 12] 3)) => 3 ) (fact "I can invoke a ProgramStep 'on' a RegisterMachine" (let [rm (->RegisterMachine [9 8 7] [4 5 6] [:foo])] (invoke (->ProgramStep + [5 1] 0) rm) => (->RegisterMachine [9 8 7] [14 5 6] [:foo]) (invoke (->ProgramStep - [2 1] 0) rm) => (->RegisterMachine [9 8 7] [-1 5 6] [:foo]) (invoke (->ProgramStep * [0 3] 1) rm) => (->RegisterMachine [9 8 7] [4 36 6] [:foo]) (invoke (->ProgramStep pdiv [4 3] 0) rm) => (->RegisterMachine [9 8 7] [5/4 5 6] [:foo]) )) (fact "protected division returns 1.0 instead of blowing up" (let [rm (->RegisterMachine [0 0 0] [0 0 0] [:foo])] (invoke (->ProgramStep pdiv [4 3] 0) rm) => (->RegisterMachine [0 0 0] [1.0 0 0] [:foo]) )) (fact "exponentiation doesn't blow up" (let [rm (->RegisterMachine [-2 1/4] [0] [:foo])] (Double/isNaN (first (:connectors (invoke (->ProgramStep pow [0 1] 0) rm)))) => true )) (fact "rm-not treats a numeric argument as if it were a boolean" (rm-not 9) => 0.0 (rm-not 0.0) => 1.0 (rm-not 1.0) => 0.0 (rm-not -9/13) => 0.0 ) (fact "rm-and treats its arguments as if they were booleans" (rm-and 9 8) => 1.0 (rm-and 9 0) => 0.0 (rm-and 0 0) => 0.0 (rm-and 0 1) => 0.0 (rm-and -3 -2) => 1.0 ) (fact "rm-or treats its arguments as if they were booleans" (rm-or 9 8) => 1.0 (rm-or 9 0) => 1.0 (rm-or 0 0) => 0.0 (rm-or 0 1) => 1.0 (rm-or -3 -2) => 1.0 ) (fact "random-program-step" (:args (random-program-step all-functions 10 12)) => [17 17] (provided (rand-nth all-functions) => (first all-functions) (rand-int 22) => 17 (rand-int 12) => 7) (:target (random-program-step all-functions 10 12)) => 7 (provided (rand-int 22) => 17 (rand-int 12) => 7) ) (fact "random-program-step with arity 1" (random-program-step all-functions 10 12) => (->ProgramStep rm-not [17] 7) (provided (rand-nth all-functions) => [rm-not 1], (rand-int 22) => 17 (rand-int 12) => 7)) (fact "I can create and invoke a random-program-step" (let [rm (->RegisterMachine [9 8 7] [4 5 6] [:foo])] (invoke (random-program-step all-functions 3 3) rm) => (->RegisterMachine [9 8 7] [49 5 6] [:foo]) (provided (rand-nth all-functions) => (second all-functions) (rand-int 6) => 2 (rand-int 3) => 0) ))
That last one, the one that claims “I can create and invoke a random-program-step”, that’s a good one to have. So I think it’s time now to (1) construct a whole RegisterMachine
according to the specifications in the paper, and (2) see what happens when we run it.
Picking lots of steps
There’s already a function for returning a random ProgramStep
, so the function for building a random collection of those is simply:
(defn random-program [functions readonly connectors steps] (take steps (repeatedly #(random-program-step functions readonly connectors))))
And because it’s collecting random things I test it—carefully, this time—to make sure it returns the right number of random things, and does so by calling the right function. I’m getting a little paranoid in my old age, maybe.
(fact "random-program returns a collection of ProgramStep items" (count (random-program all-functions 10 10 100)) => 100 (random-program all-functions 10 10 100) => (repeat 100 999) (provided (random-program-step all-functions 10 10) => 999) )
The first test checks the right number of items are returned, and the second ensures they’re the right thing by writing what I like to call a “crazy like a fox stub”. I stub the entire random-program-step
function, with the arguments I want it to receive, and have it return a constant 999
value. This simultaneously tests that the function is being called, and that the arguments I specified are being used as intended.
Some folks—well, OK, many folks, and sometimes even me—will tell you that this sort of test introduces fragility into a system, because now I’ve connected the functions together and if I change the name or the argument signature of random-program-step
some day then this will fail. OK, so it will fail. I’m good with that. If we were pairing, you could talk me out of it probably.
But now I have some surety of building a random-program
that’s what I want, and so it’s time to make a RegisterMachine
with that! I just want to peek at what happens here, so I write a weird sort of throw-away “fact
” just to get some feedback on how these things look:
(fact "I can make a RegisterMachine like the ones in the paper" (->RegisterMachine (into [] (take 11 (repeatedly #(rand 100.0)))) (into [] (repeat 30 0)) (random-program all-functions 11 30 100) ) => 99 )
In my experience, a lot of people who write code with tests will do this, but I find people rarely seem to talk about it. The idea here is to use the test as if it were a kind of REPL interface: to intentionally crap out on this test (of course it doesn’t return 99
) so you can examine the real output.
In moments of weakness, sometimes we even copy the real output and paste it into the test in place of 99
. Like when the thing is doing some complicated structure-shuffling, or arithmetic and you can’t be bothered to open a calculator and do it ahead of time.
But here, I literally have a curiosity about what the Clojure structure I’ve built will look like, so I just want to see the thing itself. Here, this is the thing itself:
FAIL "I can make a RegisterMachine like the ones in the paper" at (core_test.clj:133) Expected: 99 Actual: {:connectors [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], :program ({:args [15 3], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 9} {:args [37 5], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 28} {:args [19 22], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 9} {:args [10 19], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 14} {:args [18 11], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 8} {:args [31 1], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 18} {:args [23 0], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 15} {:args [5 35], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 29} {:args [10 35], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 18} {:args [5], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 3} {:args [24 16], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 17} {:args [28 35], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 20} {:args [26], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 15} {:args [18 8], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 11} {:args [22 3], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 4} {:args [22 17], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 22} {:args [17 25], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 19} {:args [0 4], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 25} {:args [14 5], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 7} {:args [23 32], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 17} {:args [16 39], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 5} {:args [16], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 23} {:args [21 16], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 4} {:args [23 18], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 7} {:args [1 4], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 25} {:args [4 36], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 8} {:args [33 23], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 1} {:args [29 24], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 6} {:args [10 30], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 18} {:args [32 13], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 18} {:args [14 34], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 0} {:args [5 15], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 28} {:args [32 10], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 20} {:args [32 9], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 21} {:args [28 11], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 1} {:args [28 16], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 13} {:args [24 23], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 4} {:args [6 28], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 3} {:args [3 39], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 15} {:args [26 3], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 15} {:args [0 30], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 9} {:args [0], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 8} {:args [32 27], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 1} {:args [2 4], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 28} {:args [25 8], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 15} {:args [0 16], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 16} {:args [9 24], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 17} {:args [33 26], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 16} {:args [31 40], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 0} {:args [23 14], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 16} {:args [2 24], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 14} {:args [30 18], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 22} {:args [2 4], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 17} {:args [8 25], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 4} {:args [22 7], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 4} {:args [31 21], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 27} {:args [22 1], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 0} {:args [39 37], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 0} {:args [1 34], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 16} {:args [9 14], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 11} {:args [20 40], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 23} {:args [0 33], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 21} {:args [32 23], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 18} {:args [23], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 22} {:args [25 11], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 19} {:args [30 4], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 2} {:args [5 33], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 6} {:args [35 30], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 7} {:args [23 27], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 27} {:args [29 13], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 17} {:args [37 0], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 17} {:args [17 18], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 8} {:args [22 28], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 19} {:args [6], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 21} {:args [10 31], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 12} {:args [17 7], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 3} {:args [16 36], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 16} {:args [30 2], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 23} {:args [19 31], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 14} {:args [15 14], :function #object[artificial_chemistry.core$rm_or 0x48376fc5 "artificial_chemistry.core$rm_or@48376fc5"], :target 15} {:args [30 9], :function #object[clojure.core$_PLUS_ 0x3505eb68 "clojure.core$_PLUS_@3505eb68"], :target 10} {:args [22], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 4} {:args [40 15], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 14} {:args [12 35], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 6} {:args [13 13], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 0} {:args [25 15], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 11} {:args [37 3], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 29} {:args [21 25], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 20} {:args [7 4], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 20} {:args [39 40], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 4} {:args [11 20], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 29} {:args [26 27], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 16} {:args [39 35], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 10} {:args [29 37], :function #object[artificial_chemistry.core$rm_and 0x309fc348 "artificial_chemistry.core$rm_and@309fc348"], :target 9} {:args [39], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 27} {:args [29 39], :function #object[artificial_chemistry.core$pow 0x4551edf4 "artificial_chemistry.core$pow@4551edf4"], :target 10} {:args [10 20], :function #object[clojure.core$_STAR_ 0x19638f9d "clojure.core$_STAR_@19638f9d"], :target 24} {:args [26 2], :function #object[clojure.core$_ 0x50c2fee3 "clojure.core$_@50c2fee3"], :target 14} {:args [18], :function #object[artificial_chemistry.core$rm_not 0x59c7e0f8 "artificial_chemistry.core$rm_not@59c7e0f8"], :target 26} {:args [9 9], :function #object[artificial_chemistry.core$pdiv 0x30feaa72 "artificial_chemistry.core$pdiv@30feaa72"], :target 25}), :read-only [14.608882916190336 72.77706244541449 1.1217437718844425 56.47567615716837 88.0451523399481 2.8517936589013915 45.233015689666146 14.980252218834922 12.877310012294174 47.68121767603063 81.97836054355415]}::artificial_chemistry.core.RegisterMachine FAILURE: 1 check failed. (But 36 succeeded.)
There are several things here that I like seeing, though I really will want to test them rather than just inspecting this crappy pile of internal hash numbers:
- I see the
:connectors
vector is filled with zeroes, as I intended. I always get(repeat 0 30)
and(repeat 30 0)
backwards in Clojure. The numbers are zero, by the way, because in the paper the authors specify all “connection registers” are set to zero initially, unless they’re loaded with inputs. - The
:read-only
registers are[14.608882916190336 72.77706244541449 1.1217437718844425 56.47567615716837 88.0451523399481 2.8517936589013915 45.233015689666146 14.980252218834922 12.877310012294174 47.68121767603063 81.97836054355415]
. Again, this is following the paper’s model; there are 11 of these, and they’re supposed to be evolved constants. So I’ve set them to random numbers (we have no info from the paper what the values should be), on the assumption that this is how one might make a “random initialRegisterMachine
”. - There are a bunch of program steps, and they all seem to have different
:target
and:args
numbers, and also seem to have a variety of different functions drawn fromall-functions
.
By George, I think we’ve got it.
But I don’t actually want to build these with explicit arguments like that, so I delete the test entirely and get back to the business of running these dudes. There are a few things I’ll want, which are also sparked by glancing at that pile of Clojure internal representation:
- I’ll need to be able to set input values from training cases
- I’ll want to be able to watch some specific “output” register
- I’ll want to be able to
invoke
one and many random-chosenProgramStep
items, iteratively
I think I want to start at the end of that list.
Running a “program”
I decide to cannibalize the look-and-see test I wrote above, and invoke
one random step of my random RegisterMachine
’s program on itself. Rather than farting around with provides
stubs, I concoct a plan—probably a bit too cunning a plan—for checking that a change is made even without knowing what all the random values are:
(fact "I can invoke a random program step of a RegisterMachine, and it will make a change" (let [rm (->RegisterMachine (into [] (take 5 (repeatedly #(rand 100.0)))) (into [] (take 5 (repeatedly #(rand-int 100)))) (random-program all-functions 5 5 20))] (count (clojure.set/intersection (into #{} (:connectors (invoke-any-step rm))) (into #{} (:connectors rm)))) => 4 ))
That is, when I make a random RegisterMachine
(this time with only five :connector
registers, all set to numerical values), and invoke-any-step
of its program on itself, then one of the :connectors
values should change. Because every function in all-functions
… and as I’m about to write “…will always change a value”, I realize why one does want to stub random code. Because of course it’s possible for an arbitrary function here to add 0
to the original value, or multiply by 1
, or just randomly add two other numbers whose sum is the same as the value originally stored in that register. So no, this test does not in fact test reliably, it’s more of a strong suggestion.
I fret a minute, and realize: But it’s enough for now. I want to test the invoke-any-step
function, so I write that now and see if it works:
(defn invoke-any-step "Takes a RegisterMachine, picks a random ProgramStep from its `program`, and `invoke`s that step on the machine" [rm] (invoke (rand-nth (:program rm)) rm))
To be honest, I run it a few dozen times by hand, and it never seems to fail. I guess that’s just probability theory, though. Just to make absolutely sure sure, I add a line to the check that will certainly fail informatively:
(:connectors rm) => (:connectors (invoke-any-step rm))
The error is encouraging
FAIL "I can invoke a random program step of a RegisterMachine, and it will make a change" at (core_test.clj:141) Expected: [6 78 21 69 48] Actual: [6 78 33 69 48] Diffs: in [2] expected 21, was 33 FAILURE: 1 check failed. (But 37 succeeded.)
So I delete that failing check and move on. I trust it. Now it’s time to run lots of steps.
Do I want to run a RegisterMachine
for a certain number of steps and just look at its state? Or do I want to watch its state unfold as the numbers in the registers change over time?
(defn invoke-many-steps "Takes a RegisterMachine and a number of iterations. In each iteration, it applies `invoke-any-step`" [rm steps] (nth (iterate invoke-any-step rm) steps)) (defn rm-trace "Takes a RegisterMachine and a number of iterations. It returns the lazy sequence of all the `:connectors` registers, recorded once for each step of iteration." [rm steps] (map :connectors (take steps (iterate invoke-any-step rm))))
I decide to return just the values of :connectors
from the latter function, since to be frank they’re the only thing that can possibly change in the RegisterMachine
state anyway. But I’m left in a bit of a bind here. How do I test this one?
I’ve already tested that the individual functions called in iterating work as expected, including the fact that the random stuff does random things. So really all I am left to do here is check that the structure of the things themselves matches my expectations. The result of invoke-many-steps
should be a new RegisterMachine
, and the result of rm-trace
should be a collection of steps
vectors of numbers, each the same size as :connectors
was in the starting RegisterMachine
. So:
(fact "I can examine the state after a specified number of steps with invoke-many-steps" (let [rm (->RegisterMachine (into [] (take 5 (repeatedly #(rand 100.0)))) (into [] (take 5 (repeatedly #(rand-int 100)))) (random-program all-functions 5 5 20))] (class (invoke-many-steps rm 0)) => (class rm) )) (fact "I can trace steps with rm-trace" (let [rm (->RegisterMachine (into [] (take 11 (repeatedly #(rand 100.0)))) (into [] (take 30 (repeatedly #(rand-int 100)))) (random-program all-functions 11 30 100))] (count (rm-trace rm 50)) => 50 (distinct (map class (rm-trace rm 50))) => [clojure.lang.PersistentVector] (distinct (map count (rm-trace rm 50))) => [30] ))
And those pass… but something’s not write [sic]. Now and then when I run the tests, I’ve noticed that the testing framework seems to “freeze up”. That is, it starts testing, but it doesn’t respond for so long that I get really bored and force-quit it.
“What could be happening?” I ask, leadingly.
I said it would blow up. Were you not listening? You were right there.
I swear I didn’t know this would happen when I said this might blow up. But then again, I’ve built maybe two dozen different genetic programming systems from scratch through the years, and I’ve learned a few things along the way.
The symptom is that the tests seem to be running, and in fact they’re not failing… but they’re also not terminating either. At least not for a long, long time. But not every time, just once in a while, maybe one time in twenty or so. So I can infer that it’s something about the RegisterMachine
I’ve (randomly) constructed that’s causing the long run-times.
Just to make sure they’re not, you know, failing failing but stumbling because they’re composing some kind of huge error message, I make a subtle change in the test that exercises the rm-trace
function. Then I re-run the tests a bunch of times until it seems to get in this “stuck” mode, and then I walk around the neighborhood with my wife so she can catch some Pokémon.
(fact "I can trace steps with rm-trace" (let [rm (->RegisterMachine (into [] (take 11 (repeatedly #(rand 100.0)))) (into [] (take 30 (repeatedly #(rand-int 100)))) (random-program all-functions 11 30 100))] (rm-trace rm 50) => 99 ; (count (rm-trace rm 50)) => 50 ; (distinct (map class (rm-trace rm 50))) => [clojure.lang.PersistentVector] ; (distinct (map count (rm-trace rm 50))) => [30] ))
And here’s a single :connectors
vector I find in the response when I get back:
... [58 0.520704770566933 1.0 61 14 1.0 87 0.7433862407257725 149.79697596216596 122 1.0 37 1.0 11647959936727295322712875898152823502603547712118306886793907649438221766012580937527544221757160761361546849951718340019218605951013042047510893392721122559597595455079317504N 147058360708756871439882659022184708249860489398581357686168901873545793226089633163178268658336693172442127155631160179434595178149674189504261292699534608935525437972026365170962900837155042159349412213125914888475006657801175939791901889546369212408920659216804402387207887189847296517068902304409117928454160714315888979908227954141579012911731899447486806414698253720291380194460744494156806933474839773128682664150835189475181339402654048626327582760961474188209656725205756672674864160937925923503857979978150572567324840430088747151817815486138948077896496124276944684122518911271817738934299084881010118383508711540538596388211912058441102201643708686771354917129420646700192183243875520549713956873427433242861409201371307063630976038502187254814937834385270250729875038179521351733847043168038596367265127529748228923489273307419766293699958767656542611641115501826772866807883468654509596580717175341224103664117289457018837819203734960217582083299815843419723375798025003830247374174102864048164788087031427955733439926372234757021200016376310940808526419651122263403147737201804354931326036250354555480211493059157203451253632579028635352156424527460613930212768913465549455426089122544308943692444346533794776748436206133771374031923140953745771241115531094709148294754441128662755480176613012418917755740846876059167953782318961923641926414957570605897954279481439413770567065125273724018454918829998681794755576095239527889516364116217216675576076229149287481445191016245249338958371405542020101137899743399774878630507619578845869677659730140717320426312179473660198584079398770916627552171568397759723147841876612240962344028279187469313214261405927320464672420777975494666662955994404268790383581566594655611250857988532059001679526979366004637406520705341940818422877289449637326454396088961207983084384581883869606294504452732823592507696894923390951779010940636494457084663283556180672739675334352216250025774711505621572504958470034724430779067782830689156952154354388314528026117777748718863804717592623869997839935102755864860231820705463976696048889248431887262075391561726949293758923755722112318454908794608417670456508287044483748335661174370916852670803380859180352734534420728193629492977340619097091896933957252211290805529679368252430957678820207278357059477645285631904346555016425055785573419487113104441875155330044323318542754884015630531100397774327971612326059619331893058960890333574042757144333004702026609150667812619840708417425789324277299720452160233639282405078570332010508321729461615947281196824349130219017567200812923000731802786495417220711722357564811035096154575916919485119515918124036079444111421431421191381497552603269266643198796038287105408890568505487442509606085964218681875037570540985641561873238887107745653484275907794427473831870147433521133628325491282650129101614220403515428222844378292244265892452575570088999180382441470465242823042270984557519625157436560057158036806839269959363120156014587244209724316580108788596128688589467206356469573443547762888452014842697810492508288453976464630913467568410781312894951844555438123461712103466820678055373228202331589488766431428272161443801519620091906169909023404400539459127342412849230634754945014980831117031997133937700357310613487044796415605567653144091835896255439659469604821624968042604658357302329959966290736851522126646126197736805598570517136722100665180814477647188820080350780114531401914000799078060268492026426649673950919969319546709459267285410547228666448409030501796493687861833331404581593577214808652941735283939908355339914469158084925229950369400563535984003859759068271926584271918060244891838087005825139053460425115288656670856671779327174943080363574952117046071731796595803702087160577109289672156133079408487580491064270586622427864442640679736879226342665557306196472125683106550131766548134139421052696743565860026288643263266896402268391092021292509729971345345724868461233175344276959099701813133136171215641862892646540010174733593902377024502946303633508107396231852209602746703466389466440964206929825943321499608531660269441744078548793388606251452142083872188786457179036429958741893620667244384590352824412708104903941997924156477090188961861300546646483098880359492402341426757262562173870402042402570874776680157044931324015989344200879340798915103258711303353355501951600662965965493332308141546084868030115120352078066896904734872299190571139626385479541451537138946586426705864892176816656868134975629643955818275120575821089628798691046528815474199750268264630431295946818548368966784143505765317666318331835201792851545218187798730884435249572409813066854852526188090723448564932952843319181556509609777477650353341250300774846001601667405794823689933043618665999948617521442430782043166335520026394225273269229162571922591173399227850588811502113910951517934680668356577562954899380529709296632476816914413315302631239326790216290036422219947820834379067758721157581328107463010413895553941967814321857896826389871693054839219788611213949650466960147938852162829966251041488742461355894307250648094989440786280111152786810561104314988425719648320069609679567183222007509369267900632314157979957843361973693952052389262273995121230024913880590155994009891690682304637262391281404830504806813049044894461859988201639430285212677697762065347039699236629391687774143750249198152025449766291018546178927369269630038354679020897634964666911975695313752139787076375874876870373542679713362364443519852825760568046759165004679798346498434704037131381692983172667349022278295298646035003600605714149249168069843320967040006971033336734205937176790922522110871191396157687003745233493036440243659111038985997012850513280100126491716508292611663179713890290187932433376628822683877632222548877390838197896200638235455849199473539626665259284046557082707957380924225688490724646558733201702724522549235994968499651235099652311806470961167330933992199786253220667325183147769071417706350684466978325288794867547655583665331125071031114851601357927232793678812128628601253814014053371259518892547142493792758487971991913644590778039842967523848938182210913105203180870207442815013100874577340135680738682015242463504931499812236247483769116216418295912329870062330822140476180211238673469908488244428715867091212483685630244784493615573352694451543505167522640634076964451592489974608173129883694312236020684699758808225844617449325863248239343848023330568345258119393075766982147621643070830314636888002313046291039584374144050739788288552591471197225656867083765666212896874432033267819349015774524817973058154842851545629656661869636375724648303889198959707105652635108772018232788957910839683314766225427260556421851811462849110393049607616666307076495836911021699463216722465315957270267770660967892443808184448511949888890980815743250403339275476856983831966755065829054283984715857208756056482110789489703850965431157789290625771727544263402246991552137257276353069871005406722614754195138187473424652731755204915860795254705728079788606221903826327800616447603988450561562339526592773019646424851440128715752487456888226341461444971127051073411326111915648010104849214666441949997972784812306850246939612742841527937444389690009431113928960295921127882635995937263080122670131220643819029434526011711152890324463658166914376036492820893805942238670504890797039851465887468404152383951157697435270059903971348137899447915285001219752469122332071289631804269760115575598298451498352125184864480952976905175498325316027045376574217916777833877532799811674770755910197392574157813322820695474356799877242508386507320818653988328356057507170053691997021109274765070580592267728224015495476136599353126039469081176377369649029441403730948757860439039357619837382147747718277885862984418458395039204179370220219788179633061364997243134583498563060369408847443945910243936610944712344243601594875480036321492385480109004144030490092900836284969940024073711624047443671136949659791857627226010035098852612213069214204854272836427588365797527794538661441361573181836079021805877412175112748661121398056635321680505355292019329748454479259316057070332238280254490552287576639690080707881611949247691158018001794309894711525856325342643089923287827794992084926668497750789503276908467046439184389025016240500408311290902165697666776569879462255449201177809111787492027856784703718172903162188234668111124625648802939312068623755564296648971771469530492824696339289200788594192341529340335313215747627710382101524238530569480191989271052357393538330627659238394768979059883780175761301305522399794653863125425195653958232334062672425830899029799482806994327768463189814609139756935053502517236809023679488482656391751359758770550788756585992036931527884277073770066578030956147851300792862035403067380446308825099212566272551180974275518899614535847811363792686024209348493666831209624533925869659270779270084167715189073986758629925338349829742980961042026978664994333331955745157945207244306171710727830612047951720246946450485546582689932764059058639763828398919372557754836270369611696955620166719746093821241205464856525025508003162808226743220671601795799544379665891217718614521408116744564469162411457220312835601619282996889207614645025211523307158101195013520371063972646283722523032879503045824715374047326336367148521305554893793294297012313921485280063507300755144161943684837661595625121475689893742024180086075430943745850118755525177763682035237675451816860808033327070110465014796964196991038235956006968134876260857772960129925750151436978973753510932832224887642070269737750799616824265440767783877348942492867612053295533495372057005174182352224479194508192541335470337428587020962928637816744194209739926566278265292268552016789843859022850154340483761829248050376251044199380584981941583918964519427120350335222532809208515810515150724293658085945105907995812856810361167403308812157441411617031717989067370430890792624532530415509741262477006077978705044813411496463050126188920922299327653209689236137201169263633177987387253698129377486421821108101264000055832424889275824886736439340371874688984686250937577707483065677968070761867317744124987659557347290106845300616918092469565275075376635263210560202645483142996943378542573411013214263698315015084564867325005042968385443715217327911785489870579966276893049898487801720765436161160380959906674553713739861684774374367004587858783235459349030927573997387971292188866152348143355282016554790557831748252576449172215239005913820309761390656933470317557178157038319401293232422333771269594011924940927236232408671596299749933483080679001849124705396631416787222334561143147247990653888664098740137204921657278004080442072889098543196626569412125027483620765997469259880848489516131224642006776710670245119973794002534180548547451703054059805719946975713334224018504114261397420319112162673626154826139332512261850675109136005823583212228137968174920735684235189687255253305100540212040677928028706382118866979127089648673550696472474527818213498328866632873837287357404622401939995754393722555287334709401183249850760809889300959821355484895455354659352568777887482175697489925990808632852274321254755964646025079863331711042315569964723094500218986525046689349719886477365076977963755801370769908879832939606372546436396108746474353446508177776491233707729400285447811266228156788735576264388607323625365682122027716997420720670110736351753333654190622155330475263094481189587273580403435173704723538349662854464812789069153833523052336099931085081664848259636089687689322245403352803952680526391514565549412214392688960908679580282490280647219554860885113727257849194055990601098072410450524431988783755980121459343288860893407402072822420461602985397897681168379366847235372973684161058930898863893507896102356229021509694671880225015990369896165932555649825876525143332956988142443492291890189408728299704537621621699489094246188482206267629095342620062465540441349530874401640157806081261952482181231986228569068046451247414580337201376530387489854474208546715658562975793974762955260058175490738955028726245549720192623426655731131462368569835864154204358856519879287449807581251232858184275479874268020152845379991646171689882774832000356695676019864456141630575017775368454218896792769647162658663780913068238003479691586935419718362610989172630701759322499117050042989170709208341327847561446251952611097808769559072510225706476170867443956636518747996178369277115115414249770930243729575797478154579300297433461313939108019821551895274319306560703900204099715296505139386672862513449474021481792630577760271676903985758623420967851021131293615874369540692422497225277655056659750460694431258829378182699643638262075613950136878010579655635598352773608756680530165249187078675278816638228191438044273009029342656282046289103319329151453871555357853177272941934675179912157416982761451995147367009401840258606490197449201631068027949599844893335913820883490460258460598238622523152196377892487693932057010466755038098572639990984000481513305800402164799313069400584473907986831098812175682186709525739219480925924156316219119495925811235901750855688517388642829051995772544752816974954707356443668331270653583480964987083448915744545839643183579659389398089460947821948908855222524969268443342359398465534201941483461474407954051680189821446048414626206176263173441852946033238146496268660992679059491104752928908493609672834897889346593436807146010812466442520108783649642727344717102646429073132059789723172817639980037331915492630403705355177338261102792096140327756506689160816920805248407076431549231562934190899075638348738238774951186997141155274263869618587396192922306386342938645333552758052144817864936671397543936N 78 88 117656160977043387098109857557099227299025734465841483704988966155941634000127080177045901229870310720823705555067862020396147534858717596439503973663849722824218135909892096N 81 13 56 1.3895041818898348 57.900551574472566 121.0 26 99 41 14 1.0 46] ...
That 14th value has 14017 digits in it. And this is one state of the RegisterMachine
, so this number gets used as an argument for later steps, and so on.
Clojure has an interesting behavior when it comes to numeric values, especially pertinent when you use the auto-promoting arithmetic functions I’m using here, and even more pertinent when you’re running random code on random numbers. If it can use a long
for an integer value, it will. So 91
is represented internally as a java.lang.Long
. But you’ll recall in the last episode I decided to use the auto-promoting arithmetic operations (and also clojure.math.numeric-tower
and its expt
function), not the basic type-sensitive arithmetic.
The reason I chose to use the auto-promoting functions is simple enough: I didn’t want to deal with errors like this one:
user=> (* 999999999999 9999999999999999) ArithmeticException integer overflow clojure.lang.Numbers.throwIntOverflow (Numbers.java:1501)
When you multiply the java.lang.Long
values 999999999999
and 9999999999999999
, the result is bigger than Long/MAX_VALUE
(9223372036854775807
), and so an exception is thrown. When you multiply these values with the auto-promoting function:
user=> (*' 999999999999 9999999999999999) 9999999999989999000000000001N user=> (class (*' 999999999999 9999999999999999)) clojure.lang.BigInt
The result is now an arbitrary-precision clojure.lang.BigInt
value.
So now you probably can see where can get 14017-digit numbers, huh?
Actually, there are a couple of other things that can make “ungainly” numbers—I don’t want to say “big” or “long” because that’s not mathematically accurate; I mean to refer to the number of digits. Dividing integers in Clojure produces rational
results, so for example (/ 9 13)
gives a result of 9/13
. And if you divide that by some other large number with a huge common divisor, or multiply by some other rational
, or even raise it to some large integer power, you’ll get loads of digits in the numerator and denominator very quickly. And then there’s raising integers to powers, of course. That’s also auto-promoting in the numeric-tower
library.
Thus, what’s happening here with the long slow running RegisterMachine
tests isn’t so much a “bug” as whatcha might call a consequence of using non-failing arithmetic operations in my design. If I remove the auto-promoting stuff, I’ll end up with Exceptions raised whenever integers overflow. If I don’t then somewhere along the way, random chance being what it is, some RegisterMachine
will arise that raises some big-ass number to a big-ass power, and we will be waiting while the fan spins.
Of course….
No, it’s too ridiculously simple.
Well, see here: The problem here is that integers are auto-promoting. There’s a strange quirk in the Clojure numerics code (one I’ve never quite understood, to be honest). The integers auto-promote to the arbitrary-precision BigInteger
type, but a java.lang.Float
will auto-promote to a java.lang.Double
, but that will not auto-promote to the arbitrary-precision BigDecimal
type. That latter is part of the language, and I can make a BigDecimal
, and all the arithmetic will work with them, but when I multiply java.lang.Double
values they don’t overflow, they produce Infinity
as an answer:
user=> (*' 9.0e199 9.0e199) Infinity
Infinity
is actually useful, because (at least within the float
classes of java.lang.Float
and java.lang.Double
) it pretty much acts like “real infinity”. Adding Infinity
to a number gives you Infinity
, and so on.
So you know what I should do? At least to make some headway here?
I’m gonna try very hard to always set the initial values of all RegisterMachine
registers to float
values, not integers ever. All of the functions will return float
results if the arguments are that type. So I think I can fix this test just by changing the initial settings to something like this, where instead of setting the :connectors
to 0
I set them to 0.0
:
(fact "I can trace steps with rm-trace" (let [rm (->RegisterMachine (into [] (take 11 (repeatedly #(rand 100.0)))) (into [] (repeat 30 0.0)) (random-program all-functions 11 30 100))] (count (rm-trace rm 50)) => 50 (distinct (map class (rm-trace rm 50))) => [clojure.lang.PersistentVector] (distinct (map count (rm-trace rm 50))) => [30] ))
I run that a few dozen times, and … yup. Fixes it!
What’s going on in there?
Now I can make a new RegisterMachine
, and run it for however I long I want, and with rm-trace
I can even get a huge long list of numeric vectors out that I can admire.
Maybe I should make a picture.
If I were working in Gorilla REPL I could just look at a plot inline. But I’m not. Lacking that, Clojure is… well, let’s just say there’s a lot of overhead before I can look at plots from my text editor. I’d rather work in a more suitable environment.
Frankly, if all I want to do is plot the numbers in :connectors
over time, a spreadsheet chart will do fine. I fart around with Clojure’s string-handling functions clojure.string/join
, produce a reasonable semblance of a CSV file by munging the results of rm-trace
, save that to a text file, and drop it into Google Spreadsheets just to see what’s going on. Not too painful a process, to be honest.
Here’s a plot of all 30 :connector
values, over 500 steps of some random RegisterMachine
running its 100-step :program
in arbitrary order:
And what’s happening inside that little blob of high-compressed junk near the x-axis:
Well, one thing’s for sure: Even with the trick of only using float
values to initialize these things, they’re perfectly capable of making huge numbers by applying arbitrary arithmetic at random.
Who knew?
Not done
I feel like I managed to run a random RegisterMachine
, but I didn’t manage yet to initialize it with the input values necessary for it to “solve” a training case. And I haven’t even begun to think about what sort of convention we’ll use to ask one for an “output”. Will it always be the last :connectors
entry? The first? The best? No idea.
Tomorrow.
Next time: Let’s evolve these suckers