3

I'm trying to review the slides of class. The code is supposed to print "early work" once then followed by "later work" twice(you can set the repeat number of the later work). But I wonder why this code doesn't work, and how can I modify the code? Since now the code will generate infinite loop of "later work" rather than 2(which is supposed to be)

require 'continuation'
def work
  p "early work"
  here = callcc {|here| here}
  p "later work"
  return here
end

def rework(k)
  entry = work
  k.times do |i|
    entry.call(entry)
  end
end

rework(2)
2ez4rtz
  • 61
  • 4
  • What do you mean by *"doesn't work"*? And your title mentions an infinite loop, how are you seeing that manifest? – UnholySheep Nov 20 '16 at 20:33
  • yeah, executing the code will output "later work" infinitely. Sorry for the ambiguity. – 2ez4rtz Nov 20 '16 at 20:39
  • Changing `entry.call(entry)` to just `entry.call()` should give you the desired behavior (although it still stops with a runtime error) – UnholySheep Nov 20 '16 at 20:49
  • Nevermind my previous comment, it doesn't actually fix it. On the other hand the Ruby interpreter does give a warning that `callcc` is deprecated and shouldn't be used anymore – UnholySheep Nov 20 '16 at 20:52
  • yah, I tried it only prints twice. Thanks anyway – 2ez4rtz Nov 20 '16 at 20:55

1 Answers1

2

The code doesn't work because the loop counter in k.times is stuck. Each call to entry.call(entry) rewinds the program to when callcc returns. So callcc returns again, the later work happens again, work returns again, and k.times starts again. When k.times starts, it resets its loop counter to zero. The infinite loop is because the loop counter is always zero.

To fix the program, we must continue the loop, not restart it. The best fix is to use a fiber, but first, I try to use a continuation. Here's the version that works on my machine:

require 'continuation'
def work
  p "early work"
  here = callcc {|here| here}
  p "later work"
  return here
end

class Integer
  def my_times
    i = 0
    while i < self
      yield i
      i += 1
    end
  end
end

def rework(k)
  entry = nil
  k.my_times do |i|
    if i == 0
      entry = work
    else
      entry.call(entry)
    end
  end
end

rework(2)

I fix the control flow by calling work inside the loop. When work returns again, I don't reset the loop counter.

I also define my own Integer#my_times and don't use Ruby's Integer#times. If I change the code from k.my_times back to k.times, the loop counter gets stuck again. This exposes a problem with continuation objects in Ruby.

When a continuation rewinds a program, it might rewind or preserve the values of local variables. My program assumes that entry.call preserves the loop counter. Matz's Ruby Implementation preserves the loop counter in Integer#my_times, but rewinds the loop counter in Integer#times. This is the only reason why my program can't use Integer#times.

MRI seems to rewind locals in C code (like Integer#times) but preserve locals in Ruby code (like Integer#my_times). This makes a mess of loop counters and other locals. Ruby does not fix this mess, but warns against callcc. Ruby says, warning: callcc is obsolete; use Fiber instead.

Here's the program using a fiber:

def work
  p "early work"
  here = Fiber.new do
    while true
      p "later work"
      Fiber.yield
    end
  end
  here.resume
  return here
end

def rework(k)
  entry = nil
  k.times do |i|
    if i == 0
      entry = work
    else
      entry.resume
    end
  end
end

rework(2)
George Koehler
  • 1,560
  • 17
  • 23