I've been reading and working through the exercises in the excellent Programming Elixir when I came across a problem that seemed easy enough: given a number contained within a range, write a binary search to guess the number. I thought "sure, I've solved this problem [years] ago, this won't be bad at all." Haha, funny. I've definitely gotten rusty on this class of problem and after about 20 minutes trying to solve this functionally in Elixir I turned to Ruby to work through the challenge in more familiar territory.
My first solution used a loop (and was based on the Javascript in this post which has an excellent write up of applying this algorithm to dichotomic search):
# guess.rb
class Guess < Struct.new(:number, :range)
def initialize(number, range = 1..1000)
super(number, range)
end
def start
guess
end
private
def guess
min = range.min
max = range.max
g = 0
while min < max do
g = min + (((max - min - 1) / 2) + 1).ceil
puts "Is it #{g}?"
if number < g
max = g - 1
else
min = g
end
end
puts "It is #{g}"
end
end
That works, so the next step is to make the code recursive so I can translate it easier to Elixir. Here's the recursive solution:
# guess.rb
class Guess < Struct.new(:number, :range)
def initialize(number, range = 1..1000)
super(number, range)
end
def start
guess range.max / 2, range
end
private
def guess(g, current_range)
if g == number
puts "It is #{g}"
exit
end
g = current_range.min + (((current_range.max - current_range.min - 1) / 2) + 1).ceil
puts "Is it #{g}?"
if number < g
guess(g, current_range.min..(g-1))
else
guess(g, g..current_range.max)
end
end
end
Looking at the recursive solution its easy to see how to translate this into Elixir. First, the convergence (base solution) in Elixir is our entry guard clause, while the calculation line (current_range.min + (((current_range.max - current_range.min - 1) / 2) + 1).ceil becomes an input into a helper function. The if statement's branches become two helper functions. The solution ends up looking like this:
# chop.ex
defmodule Chop do
def guess(n), do: guess(n, 1..1000)
def guess(n, b..e) when !(n in b..e), do: IO.puts "Unguessable"
def guess(n, b..b) when b == n, do: IO.puts "It is #{n}"
def guess(n, b..e) do
g = b + div(e - b - 1, 2) + 1
IO.puts "Is it #{g}?"
_guess(n, g, b..e)
end
def _guess(n, g, b.._) when n < g, do: guess(n, b..g-1)
def _guess(n, g, _..e), do: guess(n, g..e)
end