The Tower of Hanoi, three ways
recursion, currying with combinators, and every move at once

Table of Contents

Move a stack of disks from one peg to another, one disk at a time, never a larger disk on a smaller. The puzzle is the classic example of a problem that is easy to say recursively, and it makes a good test of a language: the same moves can be computed by a recursion, by curried functions stitched together with combinators, or, the array way, all at once from the binary digits of the move number. This document writes all three in X_eTaL, checks that they agree, and watches the disks move. The program is demos/classics/hanoi.xtl.

A move is a row of two pegs, from and to; the pegs are 1, 2 and 3, and every solution here moves the disks from peg 1 to peg 3.

1. Recursion, the pegs as one vector

To move n disks: move n - 1 out of the way onto the spare peg, move the largest, move the n - 1 back on top of it. The three pegs travel together as one vector, from, to and spare, and each half of the recursion reorders them with s_elect: 1 3 2 s_elect pegs makes the spare the target, 3 2 1 s_elect pegs moves from the spare.

ᵘh̲anoi ← { n pegs →
  n = 0 ? 0 2 r̲eshape 0
  first ← (n − 1) ᵘh̲anoi 1 3 2 s̲elect pegs
  last ← (n − 1) ᵘh̲anoi 3 2 1 s̲elect pegs
  first c̲at (1 2 r̲eshape 2 t̲ake pegs) c̲at last
}
3 ᵘh̲anoi 1 3 2

⍝ typed:
⍝   u:h_anoi := { n pegs ->
⍝     n = 0 ? 0 2 r_eshape 0
⍝     first := (n - 1) u:h_anoi 1 3 2 s_elect pegs
⍝     last := (n - 1) u:h_anoi 3 2 1 s_elect pegs
⍝     first c_at (1 2 r_eshape 2 t_ake pegs) c_at last
⍝   }
⍝   3 u:h_anoi 1 3 2
1 3
1 2
3 2
1 3
2 1
2 3
1 3

How to read it: each row is one move, from peg, to peg. 1 3 means take the top disk off peg 1 and put it on peg 3. Three disks take seven moves:

move from to disk moved
1 1 3 1 (the smallest)
2 1 2 2
3 3 2 1
4 1 3 3 (the largest)
5 2 1 1
6 2 3 2
7 1 3 1

The reorderings are data: 1 3 2 and 3 2 1 are permutations, which compose by indexing and can be printed or drawn. Two moves for each disk added, plus one: 2^n - 1 moves.

'{ t̲ally ⍵ ᵘh̲anoi 1 3 2 } e̲ach r̲ange 10

⍝ typed:
⍝   '{ t_ally _r u:h_anoi 1 3 2 } e_ach r_ange 10
1 3 7 15 31 63 127 255 511 1023

2. Curried, with the combinators

The textbook writes h(n, from, to, via), four arguments. In X_eTaL every function is curried: a function of four parameters applied to one value is a function of three. But a call takes one value on each side, and two values side by side are an error, so the third and fourth arguments are applied to the function value with (expr)_:

ᵘm̲v ← { from to → 1 2 r̲eshape from c̲at to }
ᵘh̲ ← { n from to via →
  n = 0 ? 0 2 r̲eshape 0
  a ← (((n − 1) ᵘh̲ from)_ via)_ to
  b ← (((n − 1) ᵘh̲ via)_ to)_ from
  a c̲at (from ᵘm̲v to) c̲at b
}
((3 ᵘh̲ 1)_ 3)_ 2

⍝ typed:
⍝   u:m_v := { from to -> 1 2 r_eshape from c_at to }
⍝   u:h_ := { n from to via ->
⍝     n = 0 ? 0 2 r_eshape 0
⍝     a := (((n - 1) u:h_ from)_ via)_ to
⍝     b := (((n - 1) u:h_ via)_ to)_ from
⍝     a c_at (from u:m_v to) c_at b
⍝   }
⍝   ((3 u:h_ 1)_ 3)_ 2
1 3
1 2
3 2
1 3
2 1
2 3
1 3

Naming the partial applications reads better. m_ove is "move n - 1 disks from here", still waiting for to and via; the first half swaps those two, and swapping arguments is Smullyan's C, the cardinal, from the Combinators library: to 'f c:C_ via is via f to.

ᶜ⁼u̲se< "Combinators"
ᵘh̲c ← { n from to via →
  n = 0 ? 0 2 r̲eshape 0
  m̲ove ← (n − 1) ᵘh̲c from
  b̲ack ← (n − 1) ᵘh̲c via
  (to 'm̲ove ᶜC̲ via) c̲at (from ᵘm̲v to) c̲at to b̲ack from
}
(((6 ᵘh̲c 1)_ 3)_ 2) m̲atch 6 ᵘh̲anoi 1 3 2

⍝ typed:
⍝   "c:" u_se< "Combinators"
⍝   u:h_c := { n from to via ->
⍝     n = 0 ? 0 2 r_eshape 0
⍝     m_ove := (n - 1) u:h_c from
⍝     b_ack := (n - 1) u:h_c via
⍝     (to 'm_ove c:C_ via) c_at (from u:m_v to) c_at to b_ack from
⍝   }
⍝   (((6 u:h_c 1)_ 3)_ 2) m_atch 6 u:h_anoi 1 3 2
1

It is the same recursion, said more functionally; it is no more an array program than the first, and X_eTaL's one-value-a-side calls make the four-argument call clumsy. That is why the first version packs the pegs into one vector.

3. Every move at once

The recursion hides a pattern. Number the moves k = 1 to 2^n - 1.

  • Move k moves disk 1 + (the number of trailing zero bits of k): the smallest disk every other move, the next every fourth, and so on.
  • Each disk always cycles round the pegs in the same direction: for disks d with n - d even it goes 1, 3, 2, 1, …; the others go 1, 2, 3, 1, ….
  • Before move k, disk d has moved k d_iv 2^d times, so its from and to pegs are that count, and that count plus one, taken round its cycle.

So every move can be computed from k alone, for all k at once. The trailing zeros are counted as how many of 2, 4, 8, … divide k, a table of remainders reduced along its second axis:

k ← r̲ange 7
0 = k 'm̲od t̲able 2 ^ r̲ange 3
'+ r̲/₂ 0 = k 'm̲od t̲able 2 ^ r̲ange 3

⍝ typed:
⍝   k := r_ange 7
⍝   0 = k 'm_od t_able 2 ^ r_ange 3
⍝   '+ r_/_2 0 = k 'm_od t_able 2 ^ r_ange 3
0 0 0
1 0 0
0 0 0
1 1 0
0 0 0
1 0 0
0 0 0
0 1 0 2 0 1 0

How to read it: the block of 0s and 1s has one row for each move k (1 to 7, top to bottom) and one column for each of 2, 4 and 8; a 1 says that number divides k (move 4: 2 and 4 divide it, 8 does not, so 1 1 0). The last line adds up each row: how many of them divide k, one number per move. One more than that is the disk the move moves: 1 2 1 3 1 2 1, the smallest disk every other move and the largest once, in the middle.

The rest is arithmetic on whole vectors. The from and to pegs are stacked as two rows and transposed with o_\, so each move is a row.

ᵘm̲oves ← { n →
  k ← r̲ange (2 ^ n) − 1
  d ← 1 + '+ r̲/₂ 0 = k 'm̲od t̲able 2 ^ r̲ange n
  m ← k d̲iv 2 ^ d
  s ← 1 + 0 = (n − d) m̲od 2
  from ← 1 + (s × m) m̲od 3
  to ← 1 + (s × m + 1) m̲od 3
  o̲\ (2 c̲at t̲ally from) r̲eshape from c̲at to
}
ᵘm̲oves 3

⍝ typed:
⍝   u:m_oves := { n ->
⍝     k := r_ange (2 ^ n) - 1
⍝     d := 1 + '+ r_/_2 0 = k 'm_od t_able 2 ^ r_ange n
⍝     m := k d_iv 2 ^ d
⍝     s := 1 + 0 = (n - d) m_od 2
⍝     from := 1 + (s * m) m_od 3
⍝     to := 1 + (s * m + 1) m_od 3
⍝     o_\ (2 c_at t_ally from) r_eshape from c_at to
⍝   }
⍝   u:m_oves 3
1 3
1 2
3 2
1 3
2 1
2 3
1 3

The same seven rows as the recursion: from peg, to peg, one row per move. The same moves as the recursion, for 1 to 10 disks:

'{ (ᵘm̲oves ⍵) m̲atch ⍵ ᵘh̲anoi 1 3 2 } e̲ach r̲ange 10

⍝ typed:
⍝   '{ (u:m_oves _r) m_atch _r u:h_anoi 1 3 2 } e_ach r_ange 10
1 1 1 1 1 1 1 1 1 1

Watching it

The state is the peg each disk is on, smallest first. A move takes the smallest disk on its first peg (where i_ndexOf first finds that peg) to its second. Each state is drawn as bars, a disk of size s as wide as 2s - 1 and centered on its peg, by a table of sizes against offsets from the center; every state stacked is an animation. Here the moves come from the array way:

ᵘm̲ove ← { p m →
  from ← 1 s̲elect m
  disk ← p i̲ndexOf from
  p + ((2 s̲elect m) − from) × disk = r̲ange t̲ally p
}
ᵘs̲lots ← { p →
  n ← t̲ally p
  (r̲ange n) '{ r j → r s̲elect (0 − n) t̲ake 0 c̲at w̲here p = j } t̲able 1 2 3
}
ᵘp̲icture ← { p →
  n ← t̲ally p
  w ← 1 + 2 × n
  bars ← (ᵘs̲lots p) '{ s x → s > a̲bs x } t̲able (o̲ffsets w) − n
  (n c̲at 3 × w) r̲eshape r̲avel bars
}
ᵘp̲lane ← { m → (1 c̲at s̲hape m) r̲eshape m }
ᵘp̲lay ← { p moves →
  frame ← ᵘp̲lane ᵘp̲icture p
  0 = t̲ally moves ? frame
  frame c̲at (p ᵘm̲ove f̲irst moves) ᵘp̲lay 1 d̲rop moves
}

⍝ typed:
⍝   u:m_ove := { p m ->
⍝     from := 1 s_elect m
⍝     disk := p i_ndexOf from
⍝     p + ((2 s_elect m) - from) * disk = r_ange t_ally p
⍝   }
⍝   u:s_lots := { p ->
⍝     n := t_ally p
⍝     (r_ange n) '{ r j -> r s_elect (0 - n) t_ake 0 c_at w_here p = j } t_able 1 2 3
⍝   }
⍝   u:p_icture := { p ->
⍝     n := t_ally p
⍝     w := 1 + 2 * n
⍝     bars := (u:s_lots p) '{ s x -> s > a_bs x } t_able (o_ffsets w) - n
⍝     (n c_at 3 * w) r_eshape r_avel bars
⍝   }
⍝   u:p_lane := { m -> (1 c_at s_hape m) r_eshape m }
⍝   u:p_lay := { p moves ->
⍝     frame := u:p_lane u:p_icture p
⍝     0 = t_ally moves ? frame
⍝     frame c_at (p u:m_ove f_irst moves) u:p_lay 1 d_rop moves
⍝   }
watched ← ⎕S̲HOW ⎕G̲RID (4 r̲eshape 1) ᵘp̲lay ᵘm̲oves 4

⍝ typed:
⍝   watched := []S_HOW []G_RID (4 r_eshape 1) u:p_lay u:m_oves 4

literate-hanoi.svg

It is the same picture, to the byte, as the one drawn from the recursion's moves in the classics walkthrough: both documents are rerun and compared against the committed file.

Which is better?

Version Says For Against
Recursion, pegs as a vector move n - 1 aside, the largest, n - 1 back the textbook idea; the peg reorderings are data still scalar thinking: n deep, the list built by repeated joins
Curried, with combinators h(n, from, to, via), C swapping two reads like the mathematics; shows currying and C the same recursion; four-argument calls need (expr)_
Every move at once move k from the bits of k no recursion, every move independent, arithmetic on vectors why it works is not obvious without the recursion

The first explains the puzzle; the third is the array program. Seeing them agree is the point.

Literate documents · Live demo · Repository