Partial application is not Schönfinkeling

The wages of pedantry

Faking Continuation Based Web Serving Using Exceptions

| Comments

A friend has pointed out to me that the command line and web interface in my last post do not need to interact with the main game through an iterator. He proposed that the web interface could pause the execution of the game by using exceptions. I played with the idea, and discovered that he was right. The upshot is that continuation-based web serving can be faked in any language which has exceptions. (Lexical closures are also helpful, but can also be faked using objects.) The approach given below relies on neither CPS nor monads, and so has the added advantage of being fairly idiomatic in most mainstream languages.

As before, our game is the children’s “Guess a number between 1 and 100” game:

(game.py) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import random

rgen = random.Random()

class RandomNumberGame(object):
    def __init__(self, interface):
        self.interface = interface

    def start_game(self):
        name = self.interface.prompt_string(
                "Greetings. What is your name? ")
        self.interface.display("Hello, %s." % (name,))

        self.__play_game()

    def __play_game(self):
        self.interface.display("I am thinking of a number between 1 and 100.")
        my_number = self.interface.get(lambda: rgen.randint(1,100))
        num_guesses = 0
        while True:
            user_number = self.interface.prompt_int("Guess: ")
            num_guesses += 1
            if my_number == user_number:
                break
            elif my_number < user_number:
                self.interface.display("Try lower.")
            else:
                self.interface.display("Try higher.")

        self.interface.display("Correct in %s guesses." % num_guesses)
        play_again = self.interface.prompt_yes_no("Play again? ")

        if play_again:
            self.__play_game()
        else:
            self.interface.display("Thank you for playing.")

I like this version much better because the ugly wart of before:

The trouble with calling a generator from a generator
1
2
3
4
5
6
7
8
# The coroutine equivalent of self.__play_game()
iter = self.__play_game()
try:
    y = yield iter.next()
    while True:
        y = yield iter.send(y)
except StopIteration:
    pass

has been simplified to:

Regular method invocation
1
self.__play_game()

The interface between the game and the user interface is the same as before, with one addition:

Interface
1
2
3
4
5
6
display(text)       # displays the given text to the user and returns None
prompt_string(text) # displays the given text to the user and returns a string input by the user
prompt_int(text)    # display the given text to the user and returns an int input by the user
prompt_yes_no(text) # display the given text to the user and returns True for yes and False for no

get(callback)       # invokes the callback and returns what it returns

The new method “get” is made use of in the game when generating the answer for the game:

Get usage
1
my_number = self.interface.get(lambda: rgen.randint(1,100))

Retrieving the random number through self.interface.get ensures that the game will not be constantly changing its answer while a user is playing through the web interface.

As before, the command line interface is very simple:

(cli.py) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python

class CommandLineInterface:
    def __init__(self, routine):
        self.routine = routine(self)

    def run(self):
        self.routine.start_game()

    def get(self, f):
        return f()

    def display(self, prompt):
        print prompt

    def prompt_string(self, prompt):
        return raw_input(prompt)

    def prompt_int(self, prompt):
        return int(raw_input(prompt))

    def prompt_yes_no(self, prompt):
        response = raw_input(prompt)
        return response.startswith('y') or response.startswith('Y')

if __name__=="__main__":
    from game import RandomNumberGame
    cmd = CommandLineInterface(RandomNumberGame)
    cmd.run()

The web interface works by raising a StopWebInterface exception when execution of the game needs to be paused so that the user can input some data into a form. Our abstraction is thus slightly leaky, in that a game which at some point generically caught all types of exceptions might interfere with the behavior of the web interface. The yield lambda solution did not have this problem.

(web.py) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python

import cgi
from wsgiref.simple_server import make_server

# Everytime we generate an html page, we create a hidden input element
# with this name. It lets us know which saved history must be resumed
# in order to continue the program.
HISTORY_FORM_NAME = 'WebInterface_history_index'

class History(object):
    """A record of past values returned by the routine. Useful for playback.
       self.get_pending is a function which takes a dictionary like object
           corresponding to the POST variables, and returns the new value
           to be added to the history.
       self.old_values is all of the results of functions yielded by the routine.
       """
    def __init__(self, get_pending, old_values):
        self.get_pending = get_pending
        self.old_values = old_values

class StopWebInterface(Exception):
    def __init__(self, get_pending):
        self.get_pending = get_pending

class WebInterface(object):
    """WebInterface wraps around a routine class to allow for the routine to
       be executed through a browser. It works by remembering the results
       of functions that were yielded by the routine. In order to pick up
       where it left off, the routine is re-run with the remembered values,
       a new value is parsed from the POST variables, and the routine keeps
       running until it reaches another prompt."""
    def __init__(self, routine_class):
        self.routine_class = routine_class
        self.histories = [History(None, [])]

    def respond(self, environ, start_response):
        responder = WebInterface.Response(self, environ, start_response)
        return responder.respond()

    class Response:
        """The Response class is instantiated for every HTTP request.
           It grabs the appropriate history according to the value in the POST
           variable 'WebInterface_history_index'. Using that history, it
           re-runs the routine with the contained old_values, parses the POST
           variables for any new values using get_pending, and continues the
           routine until it needs to make another request. For simplicity,
           it modifies web_interface.histories directly. A real
           implementation would need to protect this variable using locks
           (or find a mutation-free solution), since it is possible for
           the wsgi server to make concurrent calls to WebInterface.respond.
           """
        def __init__(self, web_interface, environ, start_response):
            self.web_interface = web_interface
            self.environ = environ
            self.start_response = start_response

        def respond(self):
            routine = self.web_interface.routine_class(self)
            self.form = cgi.FieldStorage(fp=self.environ['wsgi.input'],environ=self.environ)
            history_index = int(self.form.getvalue(HISTORY_FORM_NAME, default="0"))
            if len(self.web_interface.histories) <= history_index:
                self.start_response('412 Cannot read the future', [('Content-type', 'text/html')])
                return ["That history has not yet been written."]

            # Copy the history in order to create a new history.
            history_orig = self.web_interface.histories[history_index]
            self.history = History(history_orig.get_pending, history_orig.old_values[:])
            self.start_response('200 OK', [('Content-type', 'text/html')])
            self.output = [ '<form method="POST">\n' ]

            self.paused = False
            self.iter = self.iterate_old_values()
            try:
                routine.start_game()
            except StopWebInterface, inst:
                self.history.get_pending = inst.get_pending

            self.web_interface.histories.append(self.history)
            self.output += '<input type="hidden" name="%s" value="%s">\n' % (HISTORY_FORM_NAME, len(self.web_interface.histories) - 1)
            self.output += '</form>\n'
            return self.output

        def iterate_old_values(self):
            for val in self.history.old_values:
                yield val
            if self.history.get_pending != None:
                val = self.history.get_pending(self)
                self.history.old_values.append(val)
                yield val

        def get(self, f):
            try:
                return self.iter.next()
            except StopIteration:
                val = f()
                self.history.old_values.append(val)
                return val

        def display(self, str):
            try:
                return self.iter.next()
            except StopIteration:
                self.output += str + "<br/>\n"
                self.history.old_values.append(None)

        def prompt_string(self, prompt):
            return self.prompt_type(prompt, str)

        def prompt_int(self, prompt):
            return self.prompt_type(prompt, int)

        def prompt_yes_no(self, prompt):
            return self.prompt_and_pause(
                    prompt,
                    [("submit", "btn_yes", "Yes"), ("submit", "btn_no", "No")],
                    lambda form: form.has_key('btn_yes'))

        def prompt_type(self, prompt, type_parse):
            return self.prompt_and_pause(
                    prompt,
                    [("text", "prompt", ""), ("submit", "btn_submit", "Enter")],
                    lambda form: type_parse(form["prompt"].value))

        def prompt_and_pause(self, prompt, inputs, parse_form):
            try:
                val = self.iter.next()
                return val
            except StopIteration:
                self.output += prompt
                for input in inputs:
                    self.output += '<input type="%s" name="%s" value="%s">\n' % input
                def read_from_form(responder):
                    return parse_form(responder.form)
                raise StopWebInterface(read_from_form)

if __name__=="__main__":
    from game import RandomNumberGame
    interface = WebInterface(RandomNumberGame)
    httpd = make_server('', 8000, interface.respond)
    httpd.serve_forever()

Comments