Due by 11:59pm on Tuesday, 11/19
Submission. See the online submission instructions. We have provided a hw9 starter file for the questions below.
Readings. Section 3.4 of Composing Programs.
The Brackulator language shares an evaluator with the Calculator language, but uses a more concise syntax. Instead of using operator names or symbols, Brackulator indicates operations using different kinds of brackets.
[]: add
(): subtract
<>: multiply
{}: divide
Operand expressions are separated by spaces within these brackets. The following Brackulator expressions are followed by their Calculator equivalents.
<1 2 3> (* 1 2 3)
(5) (- 5)
[2{4 8}] (+ (/ 4 8))
<[2{12 6}](3 4 5)> (* (+ 2 (/ 12 6)) (- 3 4 5))
By solving the following problems, you will implement a parser, brack_read, that returns an expression for the calc_eval evaluator implemented in the Calculator example from lecture.
All of your solutions should be defined in terms of the following dictionaries of bracket types, which configure the parser to supply the correct operator for each bracket:
# A dictionary from pairs of matching brackets to the operators they indicate. BRACKETS = {('[', ']'): '+', ('(', ')'): '-', ('<', '>'): '*', ('{', '}'): '/'} # A dictionary with left-bracket keys and corresponding right-bracket values. LEFT_RIGHT = {left:right for left, right in BRACKETS.keys()} # The set of all left and right brackets. ALL_BRACKETS = set(b for bs in BRACKETS for b in bs)
Q1. Complete tokenize, which splits a Brackulator expression into tokens, and raise a ValueError if any token is not a number or a known bracket. Hint: You can first surround each bracket with spaces using line.replace, then split on spaces. Afterward, check each token to ensure that it is legal. The provided coerce_to_number function may prove useful:
def tokenize(line): """Convert a string into a list of tokens. >>> tokenize('2.3') [2.3] >>> tokenize('(2 3)') ['(', 2, 3, ')'] >>> tokenize('<2 3)') ['<', 2, 3, ')'] >>> tokenize('<[2{12.5 6.0}](3 -4 5)>') ['<', '[', 2, '{', 12.5, 6.0, '}', ']', '(', 3, -4, 5, ')', '>'] >>> tokenize('2.3.4') Traceback (most recent call last): ... ValueError: invalid token 2.3.4 >>> tokenize('?') Traceback (most recent call last): ... ValueError: invalid token ? >>> tokenize('hello') Traceback (most recent call last): ... ValueError: invalid token hello >>> tokenize('<(GO BEARS)>') Traceback (most recent call last): ... ValueError: invalid token GO """ # Surround all brackets by spaces so that they are separated by split. for b in ALL_BRACKETS: line = line.replace(b, ' ' + b + ' ') # Convert numerals to numbers and raise ValueErrors for invalid tokens. tokens = [] for t in line.split(): "*** YOUR CODE HERE ***" return tokens def coerce_to_number(token): """Coerce a string to a number or return None. >>> coerce_to_number('-2.3') -2.3 >>> print(coerce_to_number('(')) None """ try: return int(token) except (TypeError, ValueError): try: return float(token) except (TypeError, ValueError): return None
Q2. Implement brack_read, which returns an expression tree for the first valid Brackulator expression in a list of tokens. The expression tree should contain Calculator operators that correspond to the bracket types. Raise a SyntaxError for any malformed expression. The Pair class and nil object from lecture appear at the bottom of this file. This function is similar to scheme_read from Calculator's scheme_reader.py.
Hint: Introduce another function read_tail that reads the elements in a combination until reaching a closing bracket. In brack_read make sure that the closing bracket of an expression matches the opening bracket. The LEFT_RIGHT dictionary defined above gives you the matching right bracket for each type of left bracket. The BRACKETS dictionary gives you the corresponding operator (e.g. '+' for '[' and ']').
Once you complete this problem, you can place your homework file in the same directory as scalc.py (and its supporting files), then run read_eval_print_loop to interact with the Brackulator language:
def brack_read(tokens): """Return an expression tree for the first well-formed Brackulator expression in tokens. Tokens in that expression are removed from tokens as a side effect. >>> brack_read(tokenize('100')) 100 >>> brack_read(tokenize('([])')) Pair('-', Pair(Pair('+', nil), nil)) >>> print(brack_read(tokenize('<[2{12 6}](3 4 5)>'))) (* (+ 2 (/ 12 6)) (- 3 4 5)) >>> brack_read(tokenize('(1)(1)')) # More than one expression is ok Pair('-', Pair(1, nil)) >>> brack_read(tokenize('[])')) # Junk after a valid expression is ok Pair('+', nil) >>> brack_read(tokenize('([]')) # Missing right bracket Traceback (most recent call last): ... SyntaxError: unexpected end of line >>> brack_read(tokenize('[)]')) # Extra right bracket Traceback (most recent call last): ... SyntaxError: unexpected ) >>> brack_read(tokenize('([)]')) # Improper nesting Traceback (most recent call last): ... SyntaxError: unexpected ) >>> brack_read(tokenize('')) # No expression Traceback (most recent call last): ... SyntaxError: unexpected end of line """ if not tokens: raise SyntaxError('unexpected end of line') token = tokens.pop(0) n = coerce_to_number(token) if n != None: return n elif token in LEFT_RIGHT: "*** YOUR CODE HERE ***"
Q3. (Optional; Extra for experts) The Python Challenge is a website designed to teach people the many features of the Python Library. Each page of the site is a puzzle that can be solved simply in Python. The solution to each puzzle gives the URL of the next.
There is a function stub below to include your solution to puzzle 4 (the one with the picture of a wood carving). You will have to complete puzzles 0, 1, 2, and 3 to reach 4.
http://www.pythonchallenge.com/pc/def/0.html
Some hints:
Puzzle 1. Try str.maketrans to make a dictionary and str.translate to generate a new string. Letters are listed in the string module.
>>> 'Borkozoy'.translate(str.maketrans('oz', 'el')) 'Berkeley' >>> import string >>> string.ascii_lowercase 'abcdefghijklmnopqrstuvwxyz'
Puzzles 2 & 3. To view the source code of a web page in a browser, use
Uppercase letters are also in the string module.
>>> string.ascii_uppercase 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
Puzzle 4. Here's an example of fetching the source of a web page in Python. The address below links to an archive of the first WWW site.
>>> base = 'http://www.w3.org/History/19921103-hypertext/hypertext' >>> addr = base + '/WWW/TheProject.html' >>> from urllib.request import urlopen >>> contents = urlopen(addr).read().decode() >>> contents.split('\n')[1] '<TITLE>The World Wide Web project</TITLE>'
As you work on this puzzle, make sure to print the result of each step.
The comments on the puzzle page say: "urllib may help. DON'T TRY ALL NOTHINGS, since it will never end. 400 times is more than enough."
You can include your solution to puzzle 4 below:
from urllib.request import urlopen def puzzle_4(): """Return the soluton to puzzle 4.""" "*** YOUR CODE HERE ***"
Support code for Brackulator (from the Calculator example):
################################### # Support classes for Brackulator # ################################### class Pair(object): """A pair has two instance attributes: first and second. For a Pair to be a well-formed list, second is either a well-formed list or nil. Some methods only apply to well-formed lists. >>> s = Pair(1, Pair(2, nil)) >>> s Pair(1, Pair(2, nil)) >>> print(s) (1 2) >>> len(s) 2 >>> s[1] 2 >>> print(s.map(lambda x: x+4)) (5 6) """ def __init__(self, first, second): self.first = first self.second = second def __repr__(self): return "Pair({0}, {1})".format(repr(self.first), repr(self.second)) def __str__(self): s = "(" + str(self.first) second = self.second while isinstance(second, Pair): s += " " + str(second.first) second = second.second if second is not nil: s += " . " + str(second) return s + ")" def __len__(self): n, second = 1, self.second while isinstance(second, Pair): n += 1 second = second.second if second is not nil: raise TypeError("length attempted on improper list") return n def __getitem__(self, k): if k < 0: raise IndexError("negative index into list") y = self for _ in range(k): if y.second is nil: raise IndexError("list index out of bounds") elif not isinstance(y.second, Pair): raise TypeError("ill-formed list") y = y.second return y.first def map(self, fn): """Return a Scheme list after mapping Python function FN to SELF.""" mapped = fn(self.first) if self.second is nil or isinstance(self.second, Pair): return Pair(mapped, self.second.map(fn)) else: raise TypeError("ill-formed list") class nil(object): """The empty list""" def __repr__(self): return "nil" def __str__(self): return "()" def __len__(self): return 0 def __getitem__(self, k): if k < 0: raise IndexError("negative index into list") raise IndexError("list index out of bounds") def map(self, fn): return self nil = nil() # Assignment hides the nil class; there is only one instance
To use the following function, you will need to place your homework solution in the same directory as the files from the Calculator Example:
def read_eval_print_loop(): """Run a read-eval-print loop for the Brackulator language.""" global Pair, nil from scheme_reader import Pair, nil from scalc import calc_eval while True: try: src = tokenize(input('> ')) while len(src) > 0: expression = brack_read(src) print(calc_eval(expression)) except (SyntaxError, ValueError, TypeError, ZeroDivisionError) as err: print(type(err).__name__ + ':', err) except (KeyboardInterrupt, EOFError): # <Control>-D, etc. return