From an obfuscated Sudoku to Fibers and coroutines
TRICK is the Transcendental Ruby Imbroglio Contest for RubyKaigi, a contest for interesting & weird (transcendental, imbroglio) Ruby programs. I participated in the 2015 edition and won the 4th prize. In this article I explain my submission and detail interesting facts about it.
Not so obfuscated
This is my submission to TRICK 2015. It is actually now part of the ruby/ruby repository, like other winning entries of TRICK.
So what is this? Looks like a Sudoku puzzle in the middle. Running it shows:
Which are the 2 solutions to this specific Sudoku puzzle.
Running after changing the puzzle also solves the modified puzzle.
Giving an empty puzzle (all _
), the program will print every possible completed sudoku puzzle.
However, that might take a while.
So around the puzzle, in 302 characters, must be the code of the Sudoku solver. Let’s find out!
Mixing code and data
First we would like to separate the code (the solver) and the data (the puzzle). The first line shows how this is done. Here it is expanded:
We redefine the []
operator on Strings.
The code is in the String, and the data is the arguments of []
.
The data is stored in $*
, a global variable pointing to ARGV
.
So we push rows of digits to ARGV
. Why on earth would we do that?
It’s a code golf technique (minimizing the number of characters in the code) to use an array, without having to declare it (A=[]
takes 4 characters).
The []
operator returns b
, which is a method call to String#b
, which returns the current String as binary.
This is another code golf technique to return self
for Strings but in only 1 character!
Next we have:
_
is just a variable assigned to 0 to mark the holes in the puzzle input.
More interesting, the variable z
contains the concatenated code.
And $*
will be a two-dimensional array of the puzzle input.
The expanded code
Now we can print the code just before it is eval
‘d, add spaces and indent it nicely:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
C = Fiber
s = $*
a = *0..8
l = [
C.new { exit },
*a.product(a).select { |r,c|
s[r][c] == 0
}.map { |r,c|
C.new {
loop {
(1..9).map { |n|
C.yield(s[r][c] = n) if a.none? { |k|
s[r][k] == n || s[k][c] == n || s[r-r%3+k%3][c-c%3+k/3] == n
}
}
s[r][c] = 0
C.yield
}
}
},
C.new {
loop {
puts s.map { |r| r * ' ' } << ''
C.yield
}
}
]
c = l[i=1]
loop {
c = l[i += c.resume ? 1 : -1]
}
That’s still quite hard to read. This is the time to introduce some fun facts about this solver:
- The program implements backtracking and keeps state in a very elegant way.
- The main loop of a program is a dance between cells. On one end is the solutions, on the other the program ends.
- The program only uses infinite loops and no
break
. - The whole program never goes deeper than 9 stack frames, but yet can backtrack up to 81 levels!
This is interesting because a regular backtracking solver would recurse at least until its search depth, i.e., the number of empty cells in the input.
There is a big hint up there in line 1: Fiber
.
The solver uses Fibers (Ruby’s coroutines) to solve the Sudoku puzzle,
one Fiber per empty cell.
Let’s dig further in small steps.
So C
is just a short name for Fiber
, s
is our Sudoku input and a
is the same as (0..8).to_a
, an Array of integers from 0 to 8 included.
The next chunk is the list of cells, commented inline:
Note the r * ' '
which is the same as r.join(' ')
to format a row.
On the same line, << ''
adds an element to the array passed to puts
so we get an empty line between solutions.
Let’s see how we walk through that list of Fibers:
c.resume
returns the value given to Fiber.yield
.
When Fiber.yield
is called without argument, resume
returns nil
.
So we use Fiber.yield
without argument to mean failure and any true value for success.
Further optimizations
Going back to this code a couple of years later, I noticed there are a few ways to make this even better:
- There is no need to save the original value of the cell for empty cells, it’s always 0 (already applied in the expanded code to simplify).
- Using
$*
as an empty Array is a neat trick but since we dos = $*
we might as well declareS=[]
and always useS
, it’s shorter and clearer. - The Fibers could directly
resume
the next empty cell’s Fiber andFiber.yield
would be enough to backtrack, so there is neither a need for a main loop nor for a list of Fibers.
So I could not resist, here is a 2018-refactored version:
Conclusion
So here it is, a backtracking Sudoku solver never going deeper than 9 stack frames, using only infinite loops and no break
,
with no state other than a single copy of the puzzle!
Fibers – Ruby’s coroutines – enable us to avoid keeping explicit state (such as the current digit being tried for an empty cell) and transparently switch between different tasks, even in the middle of a method or block. That sounds like concurrency and that’s exactly what Fibers are. Due to the lack of preemption, they switch on demand and deterministically, which makes it easy to reason about. Concurrency in this context enables us to structure the program in a new and different way. Fibers are a very powerful tool and an interesting alternative to solve many problems.
What do you think? Do you find this solution elegant?
Maybe this article made you want to use Fibers more? I got the original idea from an inspiring paper on coroutines: Revisiting Coroutines by Ana Lúcia De Moura and Roberto Ierusalimschy.
If you liked this post or got some ideas from it, consider submitting to TRICK 2018! The deadline for submissions is March 31, 2018. The judge panel is very impressive and even includes Matz!
Bonus
Here is an even shorter and cleaner version of the solver, based on @Maumagnaguagno
’s comment: