The Good
- 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.
- The program does an excellent job of treating constants as constants rather than variables.
num-message
is a step toward modularity.
- 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
- Modularity: User interface is still mingled with control flow in
num-message
.
- 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:
- An input stream of guesses.
- An output stream of messages.
- 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 :(