Eliminating Loops: A Number Guessing Game in Python

Interact with this notebook on Binder here.

Just for fun, let us take a quick look at how we could take out all loops from any Python program. Most of the time this is a bad idea, both for readability and performance, but it is worth looking at how simple it is to do in a systematic fashion as background to contemplate those cases where it is actually a good idea.

Excerpt From: David Mertz. Functional Programming in Python

In [2]:
from IPython.display import display, Image
In [3]:
display(
    Image(
        url="https://minio.apps.selfip.com/mymedia/screenshots/functional_programming_in_python.png"
    )
)

Basic REPL

In [4]:
def identity(item):
    print(f"output: {item}")
    return item


echo = lambda: identity(input("Type something:\r")) == "quit" or echo()
echo()
Type something:
Hello,
output: Hello,
Type something:
world.
output: world.
Type something:
quit
output: quit
Out[4]:
True
In [5]:
import random

EXIT_WORD = "quit"
EXIT_WORDS = {EXIT_WORD, "q", "bye", "exit"}
ANSWER = random.randrange(1, 11)
EXIT_WORDS.update([str(ANSWER)])


def get_verification(guess):
    guess_int = int(guess)
    if guess_int == ANSWER:
        return EXIT_WORD
    return "higher" if guess_int < ANSWER else "lower"


def identity(item):
    verification = hint = get_verification(item)
    if verification != EXIT_WORD:
        print(f'your guess: "{item}" should be {hint}')

    return verification


echo = lambda: identity(input("Type something:\r")) in EXIT_WORDS or echo()
echo()
Type something:
1
your guess: "1" should be higher
Type something:
5
your guess: "5" should be lower
Type something:
3
your guess: "3" should be lower
Type something:
2
Out[5]:
True

Make the game more complex. It now has state.

In [6]:
import random

EXIT_WORDS = {"quit", "q", "bye", "exit"}
START, STOP = 1, 11
END = STOP - 1
ANSWER = random.randrange(START, STOP)
EXIT_WORDS.update([str(ANSWER)])

GUESSES = 3
counter = 0


def count():
    global counter
    counter += 1


def get_message(guess):
    if guess in EXIT_WORDS:
        return guess
    guess_int = int(guess)
    if guess_int == ANSWER:
        return f'The answer "{guess_int}" is correct.'
    hint = "higher" if guess_int < ANSWER else "lower"
    return f'Guess {hint} than "{guess}".'


def identity(item):
    print(get_message(item))
    count()
    return item


echo = (
    lambda: any(
        (
            identity(input(f"Type a number between {START} and {END}:\r"))
            in EXIT_WORDS,
            counter > GUESSES,
        )
    )
    or echo()
)
echo()
Type a number between 1 and 10:
1
Guess higher than "1".
Type a number between 1 and 10:
5
Guess higher than "5".
Type a number between 1 and 10:
7
Guess higher than "7".
Type a number between 1 and 10:
10
Guess lower than "10".
Out[6]:
True

Refactor to handle malformed input.

Add a try, except where the guess is cast to an int.

In [7]:
import random

EXIT_WORDS = {"quit", "q", "bye", "exit"}
START, STOP = 1, 11
END = STOP - 1
ANSWER = random.randrange(START, STOP)
EXIT_WORDS.update([str(ANSWER)])

MAX_GUESSES = 3
counter = 0


def count():
    global counter
    counter += 1


def get_message(guess):
    if guess in EXIT_WORDS:
        return guess
    try:
        guess_int = int(guess)
    except ValueError:
        return f'"guess" is not valid input.'
    if guess_int == ANSWER:
        return f'The answer "{guess_int}" is correct.'
    hint = "higher" if guess_int < ANSWER else "lower"
    return f'Guess {hint} than "{guess}".'


def identity(item):
    print(get_message(item))
    count()
    return item


echo = (
    lambda: any(
        (
            identity(input(f"Type a number between {START} and {END}:\r"))
            in EXIT_WORDS,
            counter >= MAX_GUESSES,
        )
    )
    or echo()
)
echo()
if counter >= GUESSES:
    print(f'Maximum guesses of "{MAX_GUESSES}" exceeded.')
f"The answer is {ANSWER}"
Type a number between 1 and 10:
cat
"guess" is not valid input.
Type a number between 1 and 10:
1
Guess higher than "1".
Type a number between 1 and 10:
10
Guess lower than "10".
Maximum guesses of "3" exceeded.
Out[7]:
'The answer is 7'

Refactor to remove the lambda.

Add a try, except where the guess is cast to an int.

In [8]:
import random

EXIT_WORDS = {"quit", "q", "bye", "exit"}
START, STOP = 1, 11
END = STOP - 1
ANSWER = random.randrange(START, STOP)
EXIT_WORDS.update([str(ANSWER)])

MAX_GUESSES = 3
counter = 0


def count():
    global counter
    counter += 1


def get_message(guess):
    if guess in EXIT_WORDS:
        return guess
    try:
        guess_int = int(guess)
    except ValueError:
        return f'"guess" is not valid input.'
    if guess_int == ANSWER:
        return f'The answer "{guess_int}" is correct.'
    hint = "higher" if guess_int < ANSWER else "lower"
    return f'Guess {hint} than "{guess}".'


def identity(item):
    print(get_message(item))
    count()
    return item


def echo():
    return (
        any(
            (
                identity(input(f"Type a number between {START} and {END}:\r"))
                in EXIT_WORDS,
                counter >= MAX_GUESSES,
            )
        )
        or echo()
    )


echo()
if counter >= GUESSES:
    print(f'Maximum guesses of "{MAX_GUESSES}" exceeded.')
f"The answer is {ANSWER}"
Type a number between 1 and 10:
1
Guess higher than "1".
Type a number between 1 and 10:
10
Guess lower than "10".
Type a number between 1 and 10:
5
5
Maximum guesses of "3" exceeded.
Out[8]:
'The answer is 5'