10
\$\begingroup\$

I decided to learn a Lisp-ish language and I am discovering a very weird and new syntax. The obligatory first program, that also uses all the fundamentals, is the number guessing game.

  • Did I define too few or too many constants?
  • Is my code properly spaced and indented?
  • Is there a cleaner way to exit the loop than error?
  • Is it too imperative? Is it even possible to write the number guessing game functionally?
  • Is it OK to define null and set!?

#lang racket

(define NUM 12)
(define TRIES 6)
(define CORRECT "Correct! Well guessed!")
(define TOO_LOW "... Too Low ...")
(define TOO_HIGH "... Too high ...")

(define (num_message player_num)
  (if (= player_num NUM)
      CORRECT
       (if (> player_num NUM)
          TOO_HIGH
          TOO_LOW      
      )
    )
  )

(define (number-guessing-game)
  (define out null)
  (for ([i (range TRIES)])
    (set! out (num_message (read)))
    (print out)
    (when (equal? out CORRECT)
        (print "You won")
        (error "Exiting")
    )
  )
  (print "Too many attempts")
)

(number-guessing-game)
\$\endgroup\$
5
+200
\$\begingroup\$

Winning shouldn't be considered an error. If you want a way to exit from the for-loop, use #:break. But for and set! aren't really idiomatic in Lisp-like languages. Instead, it is more common to use recursion and let, which conform to the functional programming paradigm.

In num_message, you can use a cond instead of nested ifs.

I'm not happy about the use of a string comparison to detect the winning condition: the number-guessing-game function needs to know what congratulatory message to expect from num_message. You've worked around that problem by defining some string constants, which gives them more importance than one would expect.

It would be more elegant for num_message not to have NUM fixed, and for number-guessing-game not to have NUM and TRIES fixed.

The number-guessing-game function would be clearer if it were broken up, because it is actually doing two things: play one guess, and conditionally repeat.

The print calls should probably be displayln instead, to also write a trailing newline.

Conventionally, words in identifiers are separated-by-hyphens rather than underscores. Closing parentheses don't get their own line.

#lang racket

(define NUM 12)
(define TRIES 6)

(define (num-message delta)
  (cond [(< delta 0) "... Too low ..."]
        [(> delta 0) "... Too high ..."]
        [else        "Correct! Well guessed!"]))

(define (guess-number target)
  (let ([delta (- (read) target)])
       (displayln (num-message delta))
       (zero? delta)))

(define (number-guessing-game target tries)
  (cond [(zero? tries)         (displayln "Too many attempts")]
        [(guess-number target) (displayln "You won")]
        [else                  (number-guessing-game target (- tries 1))]))

(number-guessing-game NUM TRIES)
\$\endgroup\$
  • \$\begingroup\$ There's little justification for passing target around with the calls to number-guessing-game. NUM is already in lexical scope. \$\endgroup\$ – ben rudgers Jul 27 '15 at 13:15
  • \$\begingroup\$ @benrudgers This isn't a very interesting game. Presumably the next enhancement to this program would be to make the target number not hard-coded. \$\endgroup\$ – 200_success Jul 27 '15 at 14:01
  • \$\begingroup\$ Surely (define NUM (get-target)) is the next iteration since it modularizes the acquisition. \$\endgroup\$ – ben rudgers Jul 28 '15 at 4:21
1
\$\begingroup\$

The Good

  1. The program exemplifies a sound approach to naming. Debates can rage on what the best names are: different and perhaps better or worse names are possible.
  2. The program does an excellent job of treating constants as constants rather than variables.
  3. num-message is a step toward modularity.
  4. Whether by accident or intuition the idea of for points toward a very useful and idiomatic abstraction often found in scheme/racket/Lisp programs: streams. Treating the guesses as a stream to be processed is useful.

Areas for improvement

  1. Modularity: User interface is still mingled with control flow in num-message.
  2. Iteration: Recursion is the idiomatic method for repeating a process. List comprehensions are an available alternative for programming in the fuctional style.

Alternative Approach

The code demonstrates some different [maybe better, maybe worse] techniques.

Streams

The key abstraction is three streams:

  1. An input stream of guesses.
  2. An output stream of messages.
  3. An internal stream of clock ticks. The clock ticks up rather than down so that we can talk about the first guess and the first message or write timestamped guesses and messages to a log. It starts with 1 because that maps better to the idea of "first, second...".

Modularity

Declaring the constants as functions allows for redefiing their behavior without impacting the main control flow. Providing for variable argument arity allows for experimenting with the semantics of the main loop independently of the function implementation or dispatching based on the number of arguments.

  #lang racket

  #| Data Types |#

  (struct game-message (start win lose too-high too-low)) 

  #| User Interaction |#

  (define messages
    (game-message "Please make your first guess: "
                  "\nWinner Winner Chicken Dinner!\n"
                  "\nAll your guesses are belong to us :(\n"
                  "\nYour guess is too high. Please Guess again: "
                  "\nYour guess is too low. Please Guess again: "))

  (define (get-target . args)
    12)

  (define (get-guesses . args)
    6)

  (define (get-guess . args)
    (read))

  (define (inform-user message)
    (display message))

A Higher Order Function

The function make-game takes a target and a number of guesses as input and returns a function that iterates over the clock stream, taking a guess from the stream of guesses and outputing a message on the stream of messages.

  #| Constructor |#

  (define (make-game target guesses)

  (define (game clock)
    (let ((guess (get-guess)))
        (cond
         [(equal? guess target)
          (inform-user (game-message-win messages))]
         [(= clock guesses)
          (inform-user (game-message-lose messages))]
         [(< guess target)
          (inform-user (game-message-too-low messages))
          (game (add1 clock))]
         [(> guess target)
         (inform-user (game-message-too-high messages))
         (game (add1 clock))])))

  game)

Executable Submodule

A submodule named "main" has special status when called by racket from the command line: it will execute automatically.

Game execution begins by calling the function returned by make-game as a closure.

  #| Play game |#
  (module* main #f
    (game-message-start messages)
    ((make-game (get-target)(get-guesses)) 1))

Example Use:

Note that this is run from the operating system shell, not the Racket REPL.

  $ racket number-game.rkt
  "Please make your first guess: "
  5

  Your guess is too low. Please Guess again: 6

  Your guess is too low. Please Guess again: 9

  Your guess is too low. Please Guess again: 99

  Your guess is too high. Please Guess again: 55

  Your guess is too high. Please Guess again: 18

  All your guesses are belong to us :(
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.