Draft

A few more evolved FizzBuzz Push programs

Draft of 2018.02.20

May include: genetic programming&c.

I’ve said in earlier episodes that I’ve been successfully evolving algorithms that “do” FizzBuzz, using Lee Spector’s Clojush. I wanted to record and walk through a few more of the programs that arose, just to explore the “design patterns” that Clojush tends to uncover in the Push language it implements.

I’m hoping to use one of the other off-the-shelf genetic programming projects out there (possibly PonyGE2, which uses Grammatical Evolution) to do the same thing. We’ll see how that works another time, but I am curious to see what the Python code it “discovers” will be like, in comparison to Push code.

How I’m doing this

I’m starting from the instructions posted at the Clojush repository’s wiki, but of course things could be made a lot easier.

So here’s what I’ve set up in my REPL, which is honestly weird because why wouldn’t this already be a thing included in the Clojush distribution?

(use 'clojush.pushstate)
(use 'clojush.interpreter)
(use 'clojush.args)

(defn trace-push
  [program args-vector steps]
  (let [start-state
          (reduce (fn [p i] (push-item i :input p))
                  (make-push-state)
                  args-vector)]
    (reset-globals {:evalpush-limit steps})
    (run-push program start-state true)
    ))

This function trace-push does all the work of setting up a new Push interpreter thing, pushes all the items in the collection args-vector onto the :input stack (in that order), and runs the program with the specified :evalpush-limit. This way, I can type a single line to trace a given program with a particular input, and it will poop out all this state information for me to edit later:

clojush.core=> (trace-push '(2 3 integer_mult 4 5 integer_sub) [17] 100)

State after 0 steps:
:exec = ((2 3 integer_mult 4 5 integer_sub))
:code = nil
:integer = nil
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 1 steps (last step: (...)):
:exec = (2 3 integer_mult 4 5 integer_sub)
:code = nil
:integer = nil
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 2 steps (last step: 2):
:exec = (3 integer_mult 4 5 integer_sub)
:code = nil
:integer = (2)
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 3 steps (last step: 3):
:exec = (integer_mult 4 5 integer_sub)
:code = nil
:integer = (3 2)
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 4 steps (last step: integer_mult):
:exec = (4 5 integer_sub)
:code = nil
:integer = (6)
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 5 steps (last step: 4):
:exec = (5 integer_sub)
:code = nil
:integer = (4 6)
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 6 steps (last step: 5):
:exec = (integer_sub)
:code = nil
:integer = (5 4 6)
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil

State after 7 steps (last step: integer_sub):
:exec = ()
:code = nil
:integer = (-1 6)
:float = nil
:boolean = nil
:char = nil
:string = nil
:zip = nil
:vector_integer = nil
:vector_float = nil
:vector_boolean = nil
:vector_string = nil
:input = (17)
:output = nil
:auxiliary = nil
:tag = nil
:return = nil
:environment = nil
:genome = nil
#clojush.pushstate.PushState{:exec (), :code nil, :integer (-1 6), :float nil, :boolean nil, :char nil, :string nil, :zip nil, :vector_integer nil, :vector_float nil, :vector_boolean nil, :vector_string nil, :input (17), :output nil, :auxiliary nil, :tag nil, :return nil, :environment nil, :genome nil, :termination :normal}
clojush.core=>

As I mentioned in earlier Push traces, I’m editing out the superfluous information by hand, and also adding annotations in each step to help explain what’s going on. So don’t worry, I hope it won’t be this messy or opaque for the real programs.

Revisiting “The Finger Counter”

This is actually a program I’ve already discussed, which I started to call “the Finger Counter” when I started to take a look into its dynamics. I’m not certain I really know how it works, at this writing, just that it seems to have a remarkably high computational complexity. So I want to go back and walk through it for all four input conditions: argument is divisible by 15, divisible by 3, divisible by 5 and not divisible by 3 or 5.

;; "fingers"
(in1
  exec_do*while
  (exec_do*count
    (exec_do*count
      (in1
        "buzz"
        exec_yankdup)
      integer_div
      exec_when
      "fizz"
      string_take
      integer_yankdup
      boolean_frominteger
      string_take
      string_dup
      integer_stackdepth
      exec_yankdup
      exec_y
      integer_sub
      string_take
      exec_do*times
      (integer_lte
        exec_while
        ("fizz"
          "buzz"
          string_concat
          exec_flush)))))

Let me start this time with an input of 14:

And you know what? I have no damned idea how this works. I mean, I see it looping and stuff, but I can’t follow this example at all.

Let me try with an argument that is divisible by 3 (so how about 3 itself?):

It’s a real head-scratcher. But then, finally, I see where there’s a divergence in the two runs, at the 16th step. There is a call to 'exec_yankdup, and that is using a value that changes depending on the input argument (the value is actually, at this point, \((i-1)\) for some argument \(i\)).

The 'x_yankdup combinator instruction, in Push languages, pops an :integer argument. It looks at the :x stack (for some :x, so :exec_yankdup looks at :exec), and it makes a duplicate of the item in that stack it finds at the indicated position (in zero-based notation, because Clojure). If the integer is 0 or less, it will return the top item; up to the length of the stack, it will pick that indexed item; for larger numbers, it will pick the last item.

So this is where we see our branching logic in “Fingers”. For an input of 15, we push a copy of the item on the :exec stack indexed by 14, which is the code block '(integer_lte exec_while ("fizz" "buzz" string_concat exec_flush)). For an input of 3, we push a copy of the item found in the 2nd position, which is a 'string_take instruction. These two new different-sized :exec items change the length of the :exec stack in each case, so from here on the dynamics diverge in time somewhat as well.

What happens further along, it seems, is that with input 14, at step 21 we end up with :string = ("fizz" "buzz"), and the very next instruction is that string_take we didn’t duplicate earlier, producing the stack :string = ("" "buzz"), which persists through the remainder of the trace.

On the other hand, with input 3, the duplicated string_take instruction has erased the one string on the stack, but it has also consumed our one :integer. So now we enter a series of instructions that need :integer arguments, but lack them and therefore fail. Finally, in the 19th step, we obtain the stack :string = ("fizz" ""). Later, in the 24th step, we actually duplicate the top "fizz", but for no apparent reason.

To be honest, it’s not clear how this could work for an argument that is a multiple of 3. That is, we duplicated a particular item onto the :exec stack with input 3, and that depended on the input. If I try 6 what happens?

That’s odd. I haven’t finished annotating this one, because it appears to the same things along the way as the case with input 3. So what’s different, and how does that…?

Here’s what’s happens with an input of 33:

Notice that this one doesn’t enter the infinite loop until step 43, and when it does, its :string stack is :string = ("fizz" "fizz" "" "buzz" "buzz"). That’s quite a bit different from the earlier 3-divisible cases, where we entered the infinite loop at step 26, with a stack :string = ("fizz" "fizz" ""), in both cases.

This is so confusing.

I try a few different 3-divisible inputs, and look to see (1) when the infinite loop starts, and (2) what the :string stack looks like.

input    first exec_y     string stack
-----    ------------     ------------
   3          27          ["fizz" "fizz" ""]
   6          27          ["fizz" "fizz" ""]
   9          28        * ["fizz" "fizz" "buzz"]
  12          27          ["fizz" "fizz" ""]
  18          35          ["fizz" "fizz" "" "buzz"]
  21          35          ["fizz" "fizz" "" "buzz"]
  24          36        * ["fizz" "fizz" "buzz" "buzz"]
  27          35          ["fizz" "fizz" "" "buzz"]
  33          43          ["fizz" "fizz" "" "buzz" "buzz"]
  36          43          ["fizz" "fizz" "" "buzz" "buzz"]
  39          44        * ["fizz" "fizz" "buzz" "buzz" "buzz"]
  42          43          ["fizz" "fizz" "" "buzz" "buzz"]
  48          51          ["fizz" "fizz" "" "buzz" "buzz" "buzz"]
  51          51          ["fizz" "fizz" "" "buzz" "buzz" "buzz"]
  54          52        * ["fizz" "fizz" "buzz" "buzz" "buzz" "buzz"]
  57          51          ["fizz" "fizz" "" "buzz" "buzz" "buzz"]
  63          59          ["fizz" "fizz" "" "buzz" "buzz" "buzz" "buzz"]
  69          60        * ["fizz" "fizz" "buzz" "buzz" "buzz" "buzz" "buzz"]
 111          83          ["fizz" "fizz" "" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"]
 222         139          ["fizz" "fizz" "" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz"]
 333         203          ["fizz" "fizz" "" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz"]
 336         203          ["fizz" "fizz" "" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz"]
 339         225        * ["fizz" "fizz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz"]
 342         203          ["fizz" "fizz" "" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz" "buzz" "buzz" "buzz" "buzz"
                           "buzz" "buzz" "buzz"]

My head hurts.

OK, so it seems as if there are actually two different branches handling “divisible by 3 not 5”. In one, which applies when the argument is \(i \in \{3,6,12,18,21,27,33,36,42,\dots\}\), there ends up being an empty string between the two copies of "fizz", and a monotonically-increasing series of "buzz". In the other, which applies when the argument is \(i \in \{9,24,39,54,69,\dots\}\), the stacks don’t contain an empty string.

Those numbers don’t seem familiar. But looking up [9,24,39,54,69] in the OEIS produces, “Numbers that are the sum of 9 nonzero 4th powers.”

What the actual fuck?

You’ll notice that at the end of the table above, I considered inputs of 333, 336, 339 and 342. That’s because I’d looked at 333 (one of those numbers you type into a program when you want “a bunch not a lot”), and once I saw the OEIS series I tested to see if the next element larger than 333 would share that not-deleted-central-“buzz” feature. And yes, it does, and the adjacent divisible-by-3 values do not.

Seriously: what?

I will have to come back to to this another day. More infrastructure is called for, if I’m to actually understand what’s happening here. For example, as I watch the traces scroll by on the terminal window, I notice that the :exec stack grows and then shrinks again. Obviously something is happening that makes copies of something (possibly the way the Push loops unroll?), and then as those get executed we consume items until we’re down to the 'exec_y milestone I’ve arbitrarily picked.

But… what?

Just what is it that you’re doing, Fingers?