McCarthy's Ambiguous Operator
Back in 1961, John McCarthy (the inventor of LISP) described an
interesting mathematical operator called amb. Essentially,
amb hates to be called with no arguments, and can look
into the future to keep that from happening. Here’s how it might look in
Ruby.
# amb will (appear to) choose values
# for x and y that prevent future
# trouble.
x = amb 1, 2, 3
y = amb 4, 5, 6
# Ooops! If x*y isn't 8, amb would
# get angry. You wouldn't like
# amb when it's angry.
amb if x*y != 8
# Sure enough, x is 2 and y is 4.
puts x, y
Of course, amb can’t actually see the future. However, it
can rewind into the past whenever it sees trouble, and try a
different coice.
So, how could we implement this function? As it turns out, we need continuations. Here’s a basic implementation in Ruby.
# A list of places we can "rewind" to
# if we encounter amb with no
# arguments.
$backtrack_points = []
# Rewind to our most recent backtrack
# point.
def backtrack
if $backtrack_points.empty?
raise "Can't backtrack"
else
$backtrack_points.pop.call
end
end
# Recursive implementation of the
# amb operator.
def amb *choices
# Fail if we have no arguments.
backtrack if choices.empty?
callcc {|cc|
# cc contains the "current
# continuation". When called,
# it will make the program
# rewind to the end of this block.
$backtrack_points.push cc
# Return our first argument.
return choices[0]
}
# We only get here if we backtrack
# using the stored value of cc,
# above. We call amb recursively
# with the arguments we didn't use.
amb *choices[1...choices.length]
end
# Backtracking beyond a call to cut
# is strictly forbidden.
def cut
$backtrack_points = []
end
If you’d like a fun, non-technical overview of continuations, see the explanation at RubyGarden.
Want to contact me about this article? Or if you're looking for something else to read, here's a list of popular posts.
# define variables m = 1 n = amb(0, 2, 3, 4, 5, 6, 7, 8, 9) o = amb(0, 2, 3, 4, 5, 6, 7, 8, 9) r = amb(0, 2, 3, 4, 5, 6, 7, 8, 9) s = amb(2, 3, 4, 5, 6, 7, 8, 9) y = amb(0, 2, 3, 4, 5, 6, 7, 8, 9) e = amb(0, 2, 3, 4, 5, 6, 7, 8, 9) d = amb(0, 2, 3, 4, 5, 6, 7, 8, 9) # reduce the problem space amb unless(d+e == y || d+e == y+10) amb unless(s+m+1 >= 10) # The actual problem send = s*1000 + e*100 + n*10 + d more = m*1000 + o*100 + n*10 + e money = m*10000 + o*1000 + n*100 + e*10 + y amb unless(send+more == money) # p "m=#{m}, n=#{n}, o=#{o}, r=#{r}, s=#{s}, y=#{y}, e=#{e}, d=#{d}," # Make sure all variables are different a = [m, n, o, r, s, y, e, d] t = Array.new(10, 0) a.each { |v| amb unless (t[v]+=1)<2 } # stop cut p "Final solution: m=#{m}, n=#{n}, o=#{o}, r=#{r}, s=#{s}, y=#{y}, e=#{e}, d=#{d},"Amb is pretty cool! Ben