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
andset!
?
#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)
2 Answers 2
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 if
s.
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)
-
\$\begingroup\$ There's little justification for passing
target
around with the calls tonumber-guessing-game
.NUM
is already in lexical scope. \$\endgroup\$ben rudgers– ben rudgers2015年07月27日 13:15:11 +00:00Commented Jul 27, 2015 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– 200_success2015年07月27日 14:01:10 +00:00Commented Jul 27, 2015 at 14:01
-
\$\begingroup\$ Surely
(define NUM (get-target))
is the next iteration since it modularizes the acquisition. \$\endgroup\$ben rudgers– ben rudgers2015年07月28日 04:21:40 +00:00Commented Jul 28, 2015 at 4:21
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 :(
Explore related questions
See similar questions with these tags.