Search code examples
functional-programmingschemelispracketn-queens

How to remove mutability from this function in scheme (N-queens)


I'm arduously struggling my way through the N-queens problem in SICP (the book; I spent a few days on it -- last question here: Solving Eight-queens in scheme). Here is what I have for the helper functions:

#lang sicp

; the SICP language in Racket already defines this:
; (define nil '()

; boilerplate: filter function and range functions
(define (filter func lst)
  (cond 
    ((null? lst)
       nil)
    (else
      (if (func (car lst))
        (cons (car lst) (filter func (cdr lst)))
        (filter func (cdr lst))))))

(define (range a b)
  (if (> a b)
    nil
    (cons a (range (+ 1 a) b))))
; Selectors/handlers to avoid confusion on the (col, row) notation:
; representing it a position as (col, row), using 1-based indexing
(define (make-position col row) (cons col (list row)))
(define (col p) (car p))
(define (row p) (cadr p))

; adding a new position to a board
(define (add-new-position existing-positions p)
  (append existing-positions
     (list (make-position (col p) (row p)))))
; The 'safe' function
(define (any? l proc)
  (cond ((null? l) #f)
        ((proc (car l)) #t)
        (else (any? (cdr l) proc))))

(define (none? l proc) (not (any? l proc)))

(define (safe? existing-positions p)
  (let ((bool (lambda (x) x))  (r (row p))  (c (col p)))
   (and
    ; is the row safe? i.e., no other queen occupies that row?
    (none? (map (lambda (p) (= (row p) r))  existing-positions)
           bool)

    ; safe from the diagonal going up
    (none? (map (lambda (p) (= r (+ (row p) (- c (col p)))))
                existing-positions)
           bool)
    
    ; safe from the diagonal going down
    (none? (map (lambda (p) (= r (- (row p) (- c (col p)))))
                existing-positions)
           bool))))

And now, with that boilerplate, the actual/monstrous first working version I have of the queens problem:

(define (positions-for-col col size)
    (map (lambda (ri) (make-position col ri)) 
         (range 1 size)))

(define (queens board-size)
  
(define possible-positions '())
(define safe-positions '())
(define all-new-position-lists '())
(define all-positions-list '())

; existing-positions is a LIST of pairs
(define (queen-cols col existing-positions)
  (if (> col board-size)
    (begin
      (set! all-positions-list 
            (append all-positions-list (list existing-positions))))

    (begin
      ; for the column, generate all possible positions, 
      ;   for example (3 1) (3 2) (3 3) ...
      (set! possible-positions (positions-for-col col board-size))
      ; (display "Possible positions: ") (display possible-positions) (newline)

      ; filter out the positions that are not safe from existing queens
      (set! safe-positions 
            (filter (lambda (pos) (safe? existing-positions pos)) 
                    possible-positions))
      ; (display "Safe positions: ") (display safe-positions) (newline)

      (if (null? safe-positions)
        ; bail if we don't have any safe positions
        '()
        ; otherwise, build a list of positions for each safe possibility 
        ;     and recursively call the function for the next column
        (begin
          (set! all-new-position-lists 
                (map  (lambda (pos) 
                          (add-new-position existing-positions pos)) 
                      safe-positions))
          ; (display "All positions lists: ") (display all-new-position-lists) (newline)
          
          ; call itself for the next column
          (map (lambda (positions-list) (queen-cols (+ 1 col) 
                    positions-list))
               all-new-position-lists))))))

    (queen-cols 1 '())

    all-positions-list)
(queens 5)
(((1 1) (2 3) (3 5) (4 2) (5 4))
 ((1 1) (2 4) (3 2) (4 5) (5 3))
 ((1 2) (2 4) (3 1) (4 3) (5 5))
 ((1 2) (2 5) (3 3) (4 1) (5 4))
 ((1 3) (2 1) (3 4) (4 2) (5 5))

To be honest, I think I did all the set!s so that I could more easily debug things (is that common?) How could I remove the various set!s to make this a proper functional-procedure?


As an update, the most 'terse' I was able to get it is as follows, though it still appends to a list to build the positions:

(define (queens board-size)
  (define all-positions-list '())
  (define (queen-cols col existing-positions)
    (if (> col board-size)
      (begin
        (set! all-positions-list 
              (append all-positions-list 
                      (list existing-positions))))
      (map (lambda (positions-list)
               (queen-cols (+ 1 col) positions-list))
           (map (lambda (pos) 
                    (add-new-position existing-positions pos))
                (filter (lambda (pos) 
                            (safe? existing-positions pos)) 
                        (positions-for-col col board-size))))))
  (queen-cols 1 nil)
  all-positions-list)

Finally, I think here is the best I can do, making utilization of a 'flatmap' function that helps deal with nested lists:

; flatmap to help with reduction
(define (reduce function sequence initializer)
  (let ((elem (if (null? sequence) nil (car sequence)))
        (rest (if (null? sequence) nil (cdr sequence))))
    (if (null? sequence)
        initializer
        (function elem 
                  (reduce function rest initializer)))))

(define (flatmap proc seq) 
   (reduce append  (map proc seq)  nil))
; actual
(define (queens board-size)
  (define (queen-cols col existing-positions)
    (if (> col board-size)
        (list existing-positions)
        (flatmap 
           (lambda (positions-list)  
              (queen-cols (+ 1 col) positions-list))
           (map 
              (lambda (pos) 
                 (add-new-position existing-positions 
                                   pos))
              (filter 
                 (lambda (pos) 
                    (safe? existing-positions pos))
                 (positions-for-col col board-size))))))
  (queen-cols 1 nil))

Are there any advantages of this function over the one using set! or is it more a matter of preference (I find the set! one easier to read and debug).


Solution

  • There are many ways to tackle this problem. I'll attempt to write a short and concise solution using Racket-specific procedures, explaining each step of the way. A solution using only the Scheme procedures explained in SICP is also possible, but it'll be more verbose and I'd argue, more difficult to understand.

    My aim is to write a functional-programming style solution reusing as many built-in procedures as possible, and avoiding mutation at all costs - this is the style that SICP encourages you to learn. I'll deviate from the template solution in SICP if I think we can get a clearer solution by reusing existing Racket procedures (it follows then, that this code must be executed using the #lang racket language), but I've provided another answer that fits exactly exercise 2.42 in the book, implemented in standard Scheme and compatible with #lang sicp.

    First things first. Let's agree on how are we going to represent the board - this is a key point, the way we represent our data will have a big influence on how easy (or hard) is to implement our solution. I'll use a simple representation, with only the minimum necessary information.

    Let's say a "board" is a list of row indexes. My origin of coordinates is the position (0, 0), on the top-left corner of the board. For the purpose of this exercise we only need to keep track of the row a queen is in, the column is implicitly represented by its index in the list and there can only be one queen per column. Using my representation, the list '(2 0 3 1) encodes the following board, notice how the queens' position is uniquely represented by its row number and its index:

       0 1 2 3
    0  . Q . .
    1  . . . Q
    2  Q . . .
    3  . . Q .
    

    Next, let's see how are we going to check if a new queen added at the end of the board is "safe" with respect to the previously existing queens. For this, we need to check if there are any other queens in the same row, or if there are queens in the diagonal lines starting from the new queen's position. We don't need to check for queens in the same column, we're trying to set a single new queen and there aren't any others in this row. Let's split this task in multiple procedures.

    ; main procedure for checking if a queen in the given
    ; column is "safe" in the board; there are no more
    ; queens to the "right" or in the same column
    (define (safe? col board)
      ; we're only interested in the queen's row for the given column
      (let ([row (list-ref board (sub1 col))])
        ; the queen must be safe on the row and on the diagonals
        (and (safe-row? row board)
             (safe-diagonals? row board))))
    
    ; check if there are any other queens in the same row,
    ; do this by counting how many times `row` appears in `board`
    (define (safe-row? row board)
      ; only the queen we want to add can be in this row
      ; `curry` is a shorthand for writing a lambda that
      ; compares `row` to each element in `board`
      (= (count (curry equal? row) board) 1))
    
    ; check if there are any other queens in either the "upper"
    ; or the "lower" diagonals starting from the current queen's
    ; position and going to the "left" of it
    (define (safe-diagonals? row board)
      ; we want to traverse the row list from right-to-left so we
      ; reverse it, and remove the current queen from it; upper and
      ; lower positions are calculated starting from the current queen
      (let loop ([lst   (rest (reverse board))]
                 [upper (sub1 row)]
                 [lower (add1 row)])
        ; the queen is safe after checking all the list
        (or (null? lst)
            ; the queen is not safe if we find another queen in
            ; the same row, either on the upper or lower diagonal
            (and (not (= (first lst) upper))
                 (not (= (first lst) lower))
                 ; check the next position, updating upper and lower
                 (loop (rest lst) (sub1 upper) (add1 lower))))))
    

    Some optimizations could be done, for example stopping early if there's more than one queen in the same row or stopping when the diagonals' rows fall outside of the board, but they'll make the code harder to understand and I'll leave them as an exercise for the reader.

    In the book they suggest we use an adjoin-position procedure that receives both row and column parameters; with my representation we only need the row so I'm renaming it to add-queen, it simply adds a new queen at the end of a board:

    ; add a new queen's row to the end of the board
    (define (add-queen queen-row board)
      (append board (list queen-row)))
    

    Now for the fun part. With all of the above procedures in place, we need to try out different combinations of queens and filter out those that are not safe. We'll use higher-order procedures and recursion for implementing this backtracking solution, there's no need to use set! at all as long as we're in the right mindset.

    This will be easier to understand if you read if from the "inside out", try to grok what the inner parts do before going to the outer parts, and always remember that we're unwinding our way in a recursive process: the first case that will get executed is when we have an empty board, the next case is when we have a board with only one queen in position and so on, until we finally have a full board.

    ; main procedure: returns a list of all safe boards of the given
    ; size using our previously defined board representation
    (define (queens board-size)
      ; we need two values to perform our computation:
      ; `queen-col`: current row of the queen we're attempting to set
      ; `board-size`: the full size of the board we're trying to fill
      ; I implemented this with a named let instead of the book's
      ; `queen-cols` nested procedure
      (let loop ([queen-col board-size])
        ; if there are no more columns to try exit the recursion
        (if (zero? queen-col)
            ; base case: return a list with an empty list as its only
            ; element; remember that the output is a list of lists
            ; the book's `empty-board` is just the empty list '()
            (list '())
            ; we'll generate queen combinations below, but only the
            ; safe ones will survive for the next recursive call
            (filter (λ (board) (safe? queen-col board))
                    ; append-map will flatten the results as we go, we want
                    ; a list of lists, not a list of lists of lists of...
                    ; this is equivalent to the book's flatmap implementation
                    (append-map
                     (λ (previous-boards)
                       (map (λ (new-queen-row)
                              ; add a new queen row to each one of
                              ; the previous valid boards we found
                              (add-queen new-queen-row previous-boards))
                            ; generate all possible queen row values for this
                            ; board size, this is similar to the book's
                            ; `enumerate-interval` but starting from zero
                            (range board-size)))
                     ; advance the recursion, try a smaller column
                     ; position, as the recursion unwinds this will
                     ; return only previous valid boards
                     (loop (sub1 queen-col)))))))
    

    And that's all there is to it! I'll provide a couple of printing procedures (useful for testing) which should be self-explanatory; they take my compact board representation and print it in a more readable way. Queens are represented by 'o and empty spaces by 'x:

    (define (print-board board)
      (for-each (λ (row) (printf "~a~n" row))
                (map (λ (row)
                       (map (λ (col) (if (= row col) 'o 'x))
                            board))
                     (range (length board)))))
    
    (define (print-all-boards boards)
      (for-each (λ (board) (print-board board) (newline))
                boards))
    

    We can verify that things work and that the number of solutions for the 8-queens problem is as expected:

    (length (queens 8))
    => 92
    
    (print-all-boards (queens 4))
    
    (x x o x)
    (o x x x)
    (x x x o)
    (x o x x)
    
    (x o x x)
    (x x x o)
    (o x x x)
    (x x o x)