Guides
- Purge this page if the LaTeX typesetting doesn't render.
Contents
- 1 The Return Statement
- 2 Higher-order functions
- 3 Environment diagrams
- 4 Sequences
- 5 Recursion
- 6 Data abstraction
- 7 Time complexity
- 8 Mutability
- 9 Mutable data-structures
- 10 Object-oriented programming
- 11 Iterables, iterators and generators
- 12 Python semantics and syntax
- 13 Scheme
- 14 Streams
- 15 Logic
- 16 Student guides
- 17 Composition
- 18 Debugging
- 19 Miscellaneous
The Return Statement
Return vs. Print
A lot of students were confused about the return statement (more specifically return vs. print). Return statements allow the programmer to return a value from a function. You can take the returned value and save it to a variable or do whatever you want with it. Think of it as a store. When you return something at Walmart, that item that you return will probably be purchased by someone else and they will use it however they want. (I know the analogy is not so great). Print statements on the other hand just print what you want to the screen and returns None. It doesn't allow you to actually use the value that you printed elsewhere.
Two different functions will be used to illustrate the difference.
The first function example will return a String:
def some_function(): return "I love John DeNero"
When I call the function, all it does it return the String "I love John DeNero". Now I can code this:
myLove = some_function()
Now, the variable "myLove" is set to the return value of some_function(), in this case it is "I love John DeNero". So myLove = "I love John DeNero".
The second function example will print a String:
def some_function(): print("I love John DeNero")
Now when I execute some_function(), it's going to print "I love John DeNero" onto the terminal and then return None (If a function "has no return value", it will always return None). So if I code this:
myLove = some_function()
It will print "I love John DeNero" onto the terminal/console and set myLove = None.
It is important to note that return statements will terminate the function. So if I wrote:
def some_function(): return 5 return 3
It will return 5 and end the function. It will never return 3. Also, you can only use return statements in functions.
Higher-order functions
Difference between currying and nested def
functions
Student Question
What's the difference between currying and nested def functions? Don't they both have multiple arguments/functions within?
Student Answer
According to our online reading, curry is to "use higher-order functions to convert a function that takes multiple arguments into a chain of functions that each take a single argument. More specifically, given a function f(x, y)
, we can define a function g
such that g(x)(y)
is equivalent to f(x, y)
."
I think nested def functions is a way to realize the aim of "currying". You can also nest def function with aim other than "currying". For example:
def sum_fact(n): def fact(n): i, product = 1, 1 while i <= n: product *= i i += 1 i, sum = 1, 0 while i<= n: sum += fact(i) return sum
In this function, fact only serves as a helper function to calculate factorial instead of currying, namely breaking down f(x,y)
to f(x)(y)
.
Environment diagrams
Environment diagram Rules
Source: Spring 2014 Piazza (131)
Environment Diagrams are very important in our understanding of how the computer interprets our code.
We will test you on this in every exam.
It will never go away.
Given that, master it as quickly as you can! :)
Below are the rules I follow when drawing environment diagrams. If you understand and faithfully follow these rules when drawing them, you'll never get them wrong.
One thing you haven't learned yet is nonlocal. You can skip that particular step for now (step 2 of Assignment).
Post here if you have any questions!
You can also take a look at this link for some examples of environment diagrams: http://albertwu.org/cs61a/notes/environments
For a different perspective on the rules, check out: http://markmiyashita.com/cs61a/sp14/environment_diagrams/rules_of_environment_diagrams/
A handout with detailed instructions on drawing environment diagrams is also available here (linked on the bottom of the course homepage): http://inst.eecs.berkeley.edu/~cs61a/sp14/pdfs/environment-diagrams.pdf
Environment Diagram Rules ========================= Creating a Function -------------------- 1. Draw the func <name>(<arg1>, <arg2>, ...) 2. The parent of the function is wherever the function was defined (the frame we're currently in, since we're creating the function). 3. If we used def, make a binding of the name to the value in the current frame. Calling User Defined Functions ------------------------------ 1. Evaluate the operator and operands. 2. Create a new frame; the parent is whatever the operator s parent is. Now this is the current frame. 3. Bind the formal parameters to the argument values (the evaluated operands). 4. Evaluate the body of the operator in the context of this new frame. 5. After evaluating the body, go back to the frame that called the function. Assignment ---------- 1. Evaluate the expression to the right of the assignment operator (=). 2. If nonlocal, find the frame that has the variable you re looking for, starting in the parent frame and ending just before the global frame (via Lookup rules). Otherwise, use the current frame. Note: If there are multiple frames that have the same variable, pick the frame closest to the current frame. 3. Bind the variable name to the value of the expression in the identified frame. Be sure you override the variable name if it had a previous binding. Lookup ------ 1. Start at the current frame. Is the variable in this frame? If yes, that's the answer. 2. If it isn't, go to the parent frame and repeat 1. 3. If you run out of frames (reach the Global frame and it's not there), complain. Tips ---- 1. You can only bind names to values. No expressions (like 3+4) allowed on environment diagrams! 2. Frames and Functions both have parents.
Sequences
Reversing tuples
Source: Spring 2014 Piazza (639)
Student Question
Why does [::-1] tuple work while the tuple [0:3:-1] doesn't?
I thought the -1 after the second semicolon meant that the interpreter is going to read the indexes "backwards".
Student Answer
The syntax of slicing is tup[start:end:step]:
- start from index start and end just before index end, incrementing the index by step each time
- if no step is provided, step = 1
- if step is positive, default values if not provided: start = 0, end = len(tup)
- if step is negative, default values if not provided: start = -1, end = one position before the start of the string
>>> (1, 2, 3)[::-1] # start at index -1, end one position before the start of the string (3, 2, 1) >>> (1, 2, 3)[0:3:-1] # start at 0 and go to 3, but step is negative, so this doesn't make sense and an empty tuple is returned ()
This is a helpful visualization from http://en.wikibooks.org/wiki/Python_Programming/Strings#Indexing_and_Slicing:
To understand slices, it's easiest not to count the elements themselves. It is a bit like counting not on your fingers, but in the spaces between them. The list is indexed like this:
Element: 1 2 3 4 Index: 0 1 2 3 4 -4 -3 -2 -1
More info about slicing at http://stackoverflow.com/a/13005464/2460890.
Slicing with negative step
Source: Spring 2014 Piazza (702)
Student Question
if the third example returns an empty tuple because you can't take negative steps from 0 to 4, shouldn't the second example also return an empty tuple?
Can someone explain why each example returns the respective answers?
Thanks
>>> x= (1,2,3,4) >>> x[0::-1] (1,) >>> x[::-1] (4, 3, 2, 1) >>> x[0:4:-1] () >>> x[1::-1] (2, 1)
Instructor Answer
(For reference, the notation is x[start:end:step])
Python does something a very strange when the step is negative: if you omit the arguments to start and end, Python will fill them with what makes sense for a negative step. In the simple case of x[::-1], Python fills in the start with len(x)-1 and the end with -(len(x)+1). The end term is strange, but remember that the end term isn't included. We therefore can't use 0, but we can't use -1 either, since that clearly refers to the last element of the tuple. We need to fully wrap the negative index around, to refer to the element "before" the 0th index. This way, Python will start at the end of the tuple and proceed to the beginning of the tuple.
That's why x[0:4:-1] doesn't make sense: how can we start at 0 and end at 4, if we're proceeding backwards?
And that's why x[0::-1] makes sense (albeit, in a strange way): Python is proceeding from the 0 index to the beginning of the list. It includes the start index, which is why you see a 1 pop up.
Let me know if that was confusing!
Recursion
Recursion visualizer
Source: Python Recursion Visualization with rcviz
Data abstraction
Time complexity
Andrew Huang's guide to order of growth and function runtime
Source: Guide to Order of Growth and Function Runtime (Retrieved June 16th, 2014)
Introduction
Confused by $O$, $\Omega$, and $\Theta$?
Want to figure out the runtime of that tricky function?
Read this.
NOTE THAT THIS GUIDE STARTS WITH BIG O, WHICH IS DIFFERENT FROM THETA. IF YOU UNDERSTAND BIG O, THETA IS EASY (IN FACT, IT DEFINES THETA IN TERMS OF BIG O BELOW).
First some math.
Formal definition of O(Big O):
Let $f(n)$ and $g(n)$ be functions from positive integers to positive reals. We say $f \in O(g)$ (“f grows no faster than g”) if there is a constant $0 < c < \inf$ <such that $f(n) \leq c \cdot g(n)$.
(Paraphrased from Dasgupta, Papadimitriou, & Vazirani)
(You'll see this again in CS 170)
What the heck does that mean?
Let’s look at math functions for a second (just a second).
Say $f(n)=5n$ and $g(n)=n^{2}$
What does that look like on a graph?
http://www.wolframalpha.com/input/?i=plot+5n+and+n%5E2+from+0+to+10
There’s a section where $n$ dominates $n^{2}$, from 0 to 5, but we don’t really care, because after that point, $n^{2}$ is larger, all the way to infinity! By the definition, we could scale $n^{2}$ by 5 and we would span that initial gap.
Thus we can say $5n \in O(n^{2})$ or $f \in O(g)$.
Can we say the converse? That is, is $n^{2} \in O(5n)$?
Not at all! From the graph we see that $n^{2}$ grows too quickly for $n$ to catch up, no matter what constant we scale $n$ by.
So what if $f(n)=n+1000$ and $g(n)=n^{2}$?
It turns out $n+1000 \in O(n^{2})$ still, because according to the definition, as long as we can multiply $n^{2}$ by some $c$, such that the gap of 1000 is spanned, we’re good. In the case, $c=1001$.
What about and $\Omega$ and $\Theta$?
If you digested all of the above, the rest isn’t scary! (Note, $a \equiv b$ means $a$ is equivalent to $b$)
$f \in \Omega(g) \equiv g \in O(f)$ (You'll see this again briefly in CS 170)
$f \in \Theta(g) (f \in O(g) and g \in O(f))$
This means that if $f$ is Theta of $g$, then there exist some $c_{1}$ and $c_{2}$ such that
$c_{1}g > f$ and
$c_{2}g < f$
for all positive integers.
What does that mean for Python functions?
Given a function $f$, we want to find out how fast that function runs. One way of doing this is to take out a stopwatch, and clock the amount of time it takes for $f$ to run on some input. However, there are tons of problems with that (different computers => different speeds; only one fixed input? Maybe $f$ is really fast for that input but slow for everything else; next year, all the measurements need to be redone on new computers; etc.) Instead, we'll count the steps that a function needs to perform as a function of its input. For example, here are some of the functions that take one step regardless of their input:
mul
add
sub
print
return
...
So for example, (3 + 3 * 8) % 3
would be 3 steps--one for the multiply, one of the add, and one for the mod.
Let's take a simple example:
def square(x): return x * x
square
is a function that for any input, always takes two steps, one of the multiplication, and one for returning. Using the notation, we can say square ∈ Θ(1).
Functions with iteration (for loops, recursion, etc.), usually multiply the steps by some factor. For example, consider factorial:
def factorial(n): if n == 0: return 1 else: return n * factorial(n-1)
factorial ∈ Θ(n). Why? Well given some input n
, we do n
recursive calls. At each recursive call, we carry out 4 steps, one for if n == 0
, one for subtraction, one for multiply, one for return. Plus, we have the base case, which is another 2 steps, one for if and one for return. So factorial(n)
takes $4n+2$ steps => ∈ Θ(n).
As mentioned, we care about how the running time (how long the function takes to run) of the function changes, as we increase the size of the argument. So if we imagine a graph, then the x-axis represents the size of our input, and the y-axis represents how long the function took to run for each x. As the size of the input increases, the function’s runtime does something on the graph. So when we say something like “$O(n^{2})$ where $n$ is the length of the list”, we are saying as we double the size of the list, the function is expected to run at most four times as long. NOTE ALSO THAT I SAID WHAT $n$ IS! ALWAYS GIVE YOUR UNITS.
This means that when we compare two functions A and B, A may be overall slower than B as we increase the size of their arguments. However, it’s possible at some specific arguments, the A may run faster (like the $f(n)=5n$ and $g(n)=n^{2}$ example above.)
This also means we do not care about the time taken of any particular input! This implies that all those constant-time base cases all those functions don’t really matter, because they don’t scale. That is, only one specific input causes the base case to be reached, and if we increased the size of the argument, $O(1)$ doesn't necessarily hold.
Brief “What runs faster than what”
Sorted from fastest to slowest. This is by no means comprehensive.
- $\Theta(1)$
- $\Theta(\log(n))$
- $\Theta(n)$
- $\Theta(n \log(n))$
- $\Theta(n^{2})$
- $\Theta(n^{3})$
- $\Theta(2^{n})$
- (Anything past this point is kind of ridiculous)
- $\Theta(n!)$
- $\Theta(n^{n})$
So we know about the math and the motivation, now how do we actually assign runtimes to real Python functions?
What you must understand, is that there is no one method for finding the runtime. You MUST look at a function holistically or you won’t get the right answer. What does this mean? In order to get the correct runtime, you first must understand what the function is doing! You cannot pattern-match your way to becoming good at this.
This cannot be stressed enough: UNITS MATTER, if you say O((n)), you must tell us what $n$ is.
General tips
- UNDERSTAND WHAT THE FUNCTION IS DOING!!!
- Try some sample input. That is, pretend you’re the interpreter and execute the code with some small inputs. What is the function doing with the input? Having concrete examples lets you do tip 1 better. You can also graph how the runtime increases as the argument size increases.
- If applicable, draw a picture of the tree of function calls. This shows you the "growth" of the function or how the function is getting "bigger", which will help you do tip 1 better.
- If applicable, draw a picture of how the input is being modified through the function calls. For example, if your input is a list and your function recursively does something to that list, draw out a list, then draw out parts of the list underneath it that are called during the recursion. Helps with tip 1.
- See tip 1.
Anyways, let's examine some common runtimes (keep scrolling). Remember, this is in no way a comprehensive list, NOR IS IT TRYING TO TEACH YOU HOW TO FIND THEM. This post is just to give you a starting point into orders of growth by showing you some examples and basic details about each runtime.
Constant $\Theta(1)$
What it looks like:
http://www.wolframalpha.com/input/?i=plot+5
Example:
def add(x, y): return x + y
$add \in \Theta(1)$, where 1 is.. well a constant...
Approach:
The key behind constant time functions is that regardless of the size of the input, they always run the same number of instructions.
Don’t fall for this Trap:
def bar(n): if n % 7 == 0: return "Bzzst" else: return bar(n -1)
$\mathtt{bar} \in \Theta(1)$. Why?
Logarithmic $\Theta(\log(n))$
What it looks like:
http://www.wolframalpha.com/input/?i=plot+4log3n+from+0+to+10
Example:
def binary_search(sorted_L, n): """ sorted_L is a list of numbers sorted from smallest to largest """ if sorted_L == []: return False mid_num = sorted_L[len(sorted_L) // 2] if n == mid_num: return True elif n < mid_num: return binary_search(sorted_L[:mid_num], n) else: return binary_search(sorted_L[mid_num:], n)
$\mathtt{binary\_search} \in \Theta(log(n))$, where $n$ is the number of elements in sorted_L
.
Approach:
Logarithmic functions scale down the size of the problem by some constant every iteration (either with a recursive loop, a for loop, or a while loop). Also, logarithmic functions do not branch out--they generally do not make more than one call to themselves per recursion.
Linear $\Theta(n)$
What it looks like:
http://www.wolframalpha.com/input/?i=plot+8n+from+0+to+10
Examples:
def sum_list(L): sum = 0 for e in L: sum += e return sum
$\mathtt{sum\_list} \in \Theta(n)$, where $n$ is the number of elements in $L$.
</pre>def countdown(n):
if n > 0: print(n) countdown(n - 1) else: print("Blast off!")</pre>
$\mathtt{countdown} \in \Theta(n)$, where $n$ is n.
Approach:
Linear functions usually act on sequences or other collections of data. In that case, the function will go through the elements once or twice or k times, where $k<<n$. If the function acts on a number, the number usually gets smaller by a constant each iteration.
Don't fall for this trap:
def two_for_loops(n): for a in range(n): if n == 4: for y in range(n): print("Admiral Ackbar") else: print("It's a trap!")
$\mathtt{two\_for\_loops} \in \Theta(n)$, where $n$ is n. Why?
Loglinear/Linearithmic $\Theta(n \log(n))$
What it looks like:
http://www.wolframalpha.com/input/?i=plot+nlog%28n%29+from+0+to+10
Example:
def merge(s1, s2): if len(s1) == 0: return s2 elif len(s2) == 0: return s1 elif s1[0] < s2[0]: return [s1[0]] + merge(s1[1:], s2) else: return [s2[0]] + merge(s1, s2[1:]) def mergesort(lst): if len(lst) <= 1: return lst else: middle = len(lst) // 2 return merge(mergesort(lst[:middle]), \ mergesort(lst[middle:]))
$\mathtt{mergesort} \in \Theta(n \log(n))$, where $n$ is the number of elements in lst
.
Approach: These functions tend to make two recursive calls, each making the problem smaller by a half. There's a neat way to see this. For example in mergesort, start with an entire line, which represents mergesort called on the initial list. From there, the list gets split in half by the two recursive calls to mergesort in the code, so draw the another line right below the first, of the same length, but with a small gap in the middle to represent the split. Repeat until you're tired. At the end, you get a rectangle that's nwide and (n)tall!
--------------- ------- ------- --- --- --- --- - - - - - - - -
The total area is the runtime, $\Theta(n \log(n))$
Don’t fall for this trap:
Don’t confuse functions that have an average running time of n(n)(like quicksort) with functions that are in (n(n))
Polynomial $\Theta(n^{2})$,$\Theta(n^{3})$, etc.
What it looks like:
http://www.wolframalpha.com/input/?i=plot+n%5E2%2B3+from+0+to+10
Example:
def print_a_grid(n): for _ in range(n): for _ in range(n): print("+", end="") print("")
$\mathtt{print\_a\_grid} \in \Theta(n^{2})$, where $n$ is n.
Approach:
Polynomial functions will examine each element of an input many, many times, as opposed to linear functions, which examine some constant number of times.
Don’t fall into this trap:
Don’t get polynomial confused with exponential (below).
Exponential
What it looks like:
http://www.wolframalpha.com/input/?i=plot+2%5En+from+0+to+10
Example:
(define (strange-add x) (if (zero? x) 1 (+ (strange-add (- x 1)) (strange-add (- x 1)) ))) def strange_add(x): if x == 0: return 1 else: return strange_add(x - 1) + strange_add(x - 1)
$\mathtt{strange\_add} \in \Theta(2^{n})$, where $n$ is x.
Approach:
Exponential functions tend to branch out as you get deeper and deeper into their call tree, and each call only makes the work smaller by a little bit. For example, (strange-add 8)
calls (strange-add 7)
and (strange-add 7)
. Those two calls each make two calls, (strange-add 6)
, (strange-add 6)
, (strange-add 6)
, and (strange-add 6)
respectively, and so on.
Mutability
Michelle Chang's guide to immutability and mutability
Source: What You Should Know about Immutability vs Mutability
Mutable data-structures
Object-oriented programming
Inheritance and class vs instance attributes
Source: Spring 2014 Piazza (1413)
Custom Writing Service Student Question
I'm confused on how Classes and Inheritance work.
If there's a Parent class and a Child class, when coding in the Child class, when do you write Parent.attribute
, when do you write Child.attribute
, and when do you write self.attribute
?
Also, I'm also confused as to when to put self
into the parentheses as well.
Instructor Answer
Parent.attribute
and Child.attribute
would both be ways of accessing aclass variable. These are variables that can be accessed without creating new instances of the that class.
self.attribute
would be used in methods to access an instance variable (an attribute specific to an instance).
So for example, Insect.watersafe
is False
, but Bee.watersafe
is True
. These are class attributes because you don't have to create an Insect object or a Bee object in order to say Insect.watersafe
or Bee.watersafe
.
However it wouldn't make any sense to say Bee.armor
, since armor is an instance variable. You have to first create a new Bee before you could ask it for it's armor. If you created a second Bee after that, the second Bee would also have its own armor.
There's a lot of vocab (in bold) that might trip you up. Try reading Discussion 6 and posting a followup if you're still unsure!
Iterables, iterators and generators
Python semantics and syntax
While vs. If
Source: Fall 2013 Piazza (397)
Student Question
I'm a bit confused, what is the exact function of the "while" function, and how is this different from the "if" function?
Instructor Answer
A while loop will keep evaluating the body of the while loop when its conditional expression is true, whereas an if statement will evaluate its body only once after evaluating its conditional expression.
i.e. Consider the following two functions! f()
will return 10
, while g()
will return 1
.
def f(): i = 0 while i < 10: i += 1 return i
vs
def g(): i = 0 if i < 10: i += 1 return i
@property
Source: Spring 2014 Piazza (3015)
Student Question
Is it possible to call @property
on methods that have arguments?
Student Answer
@property
is used to define "setters" and "getters". It essentially gives you control of what happens when a variable is assigned or retrieved.
class A: def __init__(self): self._x = 5 @property def x(self): return self._x @x.setter def x(self, value): print("Hello World") self._x = value >>> a = A() >>> a.x = 52 "Hello World"
As such, there isn't really room for arguments, since it is treated similar to a variable rather than a function. It is retrieved as "a.x" and set as "a.x = value".
If you would like to pass arguments when assigning something, it would probably be best to just have them use that method. e.g.
def set_x(self, value1, value2, value3): self._x = value1 * value2 * value3
Of course, you won't get the syntactic sugar of "a.x = ", but it gets the job done.
The cool thing about properties is that you can also place restrictions on a variable. For example, if you leave out the "x.setter" method in the example above, then assigning a.x = 5 would throw an error since it isn't defined (you can still assign a._x though). It would only be available for reading Custom Writing. Sometimes that is a good sign to let any users know to use some method to set it (e.g. set_x) or to not set it at all.
Scheme
Scheme semantics and syntax
Difference between eq?
, eqv?
, equal?
and =
What is the difference between eq?, eqv?, equal?, and = in Scheme?
Let's start with the =
equivalence predicate. The =
predicate is used to check whether two numbers are equal. If you supply it anything else but a number then it will raise an error:
(= 2 3) => #f (= 2.5 2.5) => #t (= '() '()) => error
The eq?
predicate is used to check whether its two parameters respresent the same object in memory. For example:
(define x '(2 3))
(define y '(2 3))
(eq? x y) => #f
(define y x)
(eq? x y) => #t
Note however that there's only one empty list '()
in memory (actually the empty list doesn't exist in memory, but a pointer to the memory location 0
is considered as the empty list). Hence when comparing empty lists eq?
will always return #t
(because they represent the same object in memory):
(define x '()) (define y '()) (eq? x y) => #t
Now depending upon the implementation eq?
may or may not return #t
for primitive values such as numbers, strings, etc. For example:
eq? 2 2) => depends upon the implementation (eq? "a" "a") => depends upon the implementation
This is where the eqv?
predicate comes into picture. The eqv?
is exactly the same as the eq?
predicate, except that it will always return #t
for same primitive values. For example:
(eqv? 2 2) => #t (eqv? "a" "a") => #t
Hence eqv?
is a superset of eq?
and for most cases you should use eqv?
instead of eq?
.
Finally we come to the equal?
predicate. The equal?
predicate is exactly the same as the eqv?
predicate, except that it can also be used to test whether two lists, vectors, etc. have corresponding elements which satisfy the eqv?
predicate. For example:
(define x '(2 3)) (define y '(2 3)) (equal? x y) => #t (eqv? x y) => #f
In general:
- Use the
=
predicate when you wish to test whether two numbers are equivalent. - Use the
eqv?
predicate when you wish to test whether two non-numeric values are equivalent. - Use the
equal?
predicate when you wish to test whether two lists, vectors, etc. are equivalent. - Don't use the
eq?
predicate unless you know exactly what you're doing.
Scheme lists
Using cons
Source: Spring 2014 Piazza (3004)
Student Question
What's the difference between the following in Scheme?
(cons 1 2) (cons 1 . 2)
(cons 1 (cons 2 (cons 3 nil))) (cons 1 . (cons 2 . (cons 3 . 4)))
Why does putting a dot before "(cons" cause it to be a malformed list? But when you put in (cons 1 2) it returns (1 . 2)? Is the dot something that only the interpreter returns, and that the user can't use in defining a list?
Student Answer
I struggled with this a bit as well. It helps to know the difference between a list containing a "." and one that doesn't.
First off, scheme lists love to be recursive, kind of like rlists. That is, if you did cdr on the list, you would keep getting a list until you finally get an empty list/nil.
STk> (define a '(1 2 3)) a STk> a (1 2 3) STk> (cdr a) (2 3) STk> (cddr a) (3) STk> (cdddr a) ()
Note how each call of cdr returns a list. Even (3) is a list. It is just a list containing one member. So what happens with something like (1 2 . 3)?
STk> (define a '(1 2 . 3)) a STk> a (1 2 . 3) STk> (cdr a) (2 . 3) STk> (cddr a) 3
Notice how the last cdr
returns a simple 3. Running cdddr
would throw an error, because the list stops at 3. A list containing a "." is known as an improper list.
As for the ".", you won't be using it unless you use it in combination with a quote, otherwise it will always return a malformed list and throw an error. For example:
STk> (define a (1 2 . 3)) *** Error: eval: malformed list: (1 2 . 3) Current eval stack: __________________ 0 (1 2 . 3) 1 (define a (1 2 . 3)) STk> (define a '(1 2 . 3)) a
Basically, the "." is seen in output, but not input. The only exception is the quote. Think of scheme as having two stages. First, it interprets your commands to construct the lists etc. Next, it will simplify the expression. Think of the quote as skipping straight to the second stage.
Finally, the only way to get the standard lists is to end the list with nil or to use the "list" function (or to use a quote). If the list doesn't end with nil, then it will become an improper list. That is why you can't do things like (1 . 2 . 3 . 4)
to make (1 2 3 4)
. In order to form a "proper" list, each element must be represented by a list. You can do (1 . (2 . (3 . (4))))
because you are treating each element like a list. When in doubt, just test some output:
STk> (cons 1 2) (1 . 2) ; doesn't end in an empty list/nil STk> (cons 1 (cons 2 '())) (1 2) STk> (cons 1 (cons 2 (3))) ; ERROR STk> (cons 1 (cons 2 (list 3))) (1 2 3) ; lists are formed with an nil at the end, so this works STk> '(1 . (2 . (3 . 4))) (1 2 3 . 4) STk> '(1 . (2 . (3 . (4))) (1 2 3 4) STk> '(1 (2 . 3) 4) (1 (2 . 3) 4) ; still works, but this has 3 elements: (1), (2 . 3), and (4) STk> (cons 1 (cons (cons 2 3) (cons 4 nil))) (1 (2 . 3) 4) ; equivalent to above, without quote
append
vs cons
vs list
Source: Spring 2014 Piazza (2067)
This post isn't meant to be comprehensive. Ask questions in lab or as a followup here if you're confused. One of its major flaws is that it doesn't cover box and pointers. LEARN BOX AND POINTERS.
Here is a beautiful web based Scheme interpreter that will draw box and pointer diagrams for you. Run through the examples below with this thing: http://xuanji.appspot.com/js-scheme-stk/index.html
In order to understand these three procedures, you first have to understand a little about Pairs and Lists.
Pairs are data structures that have two slots. You can put different stuff in these slots, like numbers or words or sentences or booleans--pretty much anything. You make a pair using cons.
STk> (cons 'foo 'bar) (foo . bar) STk> (cons 1 'ring) (1 . ring) STk> (cons (+ 1 2 3) (member? 3 '(the 3 stooges))) (6 . #t)
In order to get stuff from a pair that you have made, you use car
and cdr
. car
gets the thing in the first slot. cdr
gets the thing in the second slot.
STk> (define foo (cons 'x 'y)) foo STk> foo (x . y) STk> (car foo) x STk> (cdr foo) y
That was straightforward. Now for the trippy part: You can put pairs inside of pairs:
STk> (define foo (cons (cons 3 4) 5)) foo STk> foo ((3 . 4) . 5) STk> (car foo) (3 . 4) STk> (car (car foo)) 3 STk> (caar foo) ; functionally equivalent as above. 3 STk> (cdr foo) 5 STk> (cdr (car foo)) 4 STk> (cdar foo) ; functionally equivalent as above. 4
There's a certain style of pair nesting that is especially useful—Lists.
Each list has these properties:
- Every list is a pair or the empty list (denoted by '() or nil).
- The car of a nonempty list is some item.
- The cdr of a nonempty list must be another list.
STk> (cons 1 (cons 2 (cons 3 '()))) ; list of numbers (1 2 3) STk> (define stooges (cons 'larry (cons 'curly (cons 'moe nil)))) stooges STk> stooges (larry curly moe) STk> (car stooges) larry STk> (cdr stooges) ; Calling cdr on a non-empty list gives you another list! (curly moe) STk> (cadr stooges) curly STk> (cdar stooges) ; Why does this break? *** Error: cdar: bad list: (moe larry curly) Current eval stack: __________________ 0 (cdar stooges) STk> (define not-a-list (cons 'foo (cons 'bar 'baz))) ; This is not a list. not-a-list STk> not-a-list ; What property does this break?
Notice how Scheme knew that we were making lists. Before we had parens
and periods which organized our items. Scheme now recognizes that we're making a list and does away with the periods and some of the parens
.
If you stare a bit at the list rules above, you can notice we used a recursive definition to define lists. Recursion... on data!
Let's talk about list
. list
takes a bunch of stuff and makes a list out of them. The stuff can be anything. Words, numbers, pairs, other lists. list
doesn't care. [picture of a honey badger]
STk> (list 'foo 'bar' 'baz) ; Lists takes anything and makes a list out of it. (foo bar baz) STk> (list 'foo ((lambda (x) (+ x 4)) 8) #f (cons 1 (cons 3 4)) (cons 1 (cons 2 nil)) (list 1 2 3)) ; ANYTHING (foo 12 #f (1 3 . 4) (1 2) (1 2 3)) STk> (list 'x 'y 'z) (x y z) STk> '(x y z) ; Sometimes you can get away with using quote to make literal lists. Yes, sentences are secretly lists. (x y z)
Now we can talk about append
:
STk> (append '(a b c) '(d e f) '(g h i)) ; Append takes in lists and appends them together. (a b c d e f g h i) STk> (append 'foo '(1 2 3)) ; foo is not a list. Stuff will break. *** Error: append: argument is not a list: foo Current eval stack: __________________ 0 (append (quote foo) (quote (1 2 3)))
You know that cons
makes a pair. You also know that you can make a list out of pairs. You can abuse cons
for your own maniacal purposes.
STk> (cons 'joe stooges) ; Put stuff at the beginning of a list! (joe larry curly moe)
The following only applies to the STk interpreter.
STk> (append '(1 2 3) 'foo) ; Wait... what? (1 2 3 . foo) STk> (append '(1 2 3) (cons 4 5)) ; The plot thickens! (1 2 3 4 . 5) STk> (append stooges 'shemp) ; You should really figure out why this works. (larry curly moe . shemp)
To summarize:
- append takes in lists and outputs a big list.
- cons takes in things and makes a pair out of them. However, we know that lists are made of pairs, so we can throw together a list if we use cons a certain way
- list takes in things and makes a list out of those things, regardless of what they are.
Tail recursion
Mark Miyashita's guide on tail recursion
Source: Tail Recursion and Tail Optimized Calls
First off, I think this is an excellent article to read about tail recursion and tail calls in Python: here
Basically, you can write tail recursive functions in any language. Tail recursion, in one sentence, is where you return the answer in the final frame instead of following the frames back up to the original frame. For example, we have factorial which is normally not tail recursive:
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)
because it needs to keep track of the n *
at each level of recursion.
The following implementation of factorial
, is tail recursive because at the end of the last frame, it can return the answer, instead of going back up through all the frames to multiply and compute the answer:
def factorial(n): def helper(n, total): if n == 1: return total return helper(n - 1, total * n) return helper(n, 1)
You can’t have tail optimized calls in Python – at least, not like the code that we defined above. You can define your own sort of tail optimized way of evaluating the functions by using lambdas, and I believe the article linked at the top of this post goes into detail about how to implement this if you are interested. In Scheme, the language detects when you have something like the helper function in the example above where your return statement consists of only the recursive call. In the first example, we have the n *
and the recursive call which means it cannot be tail optimized because it needs to keep track of all of the frames that it creates. In a tail optimized call, Scheme will get rid of the frames that are no longer necessary.
tl;dr – Tail recursion can be done in any language where the basic idea is that you return the answer in the final frame of recursion. Tail optimized calls are a Scheme (and some other languages, not including Python) feature where it will get rid of the frames above, if certain conditions are met – such as where the return statement is only the recursive call and nothing else. The cases in which Scheme uses a tail optimized call are located on the lecture slides located here.
Tail recursion in Python
Source: http://kylem.net/programming/tailcall.html (Retrieved June 16th, 2014)
In this page, we’re going to look at tail call recursion and see how to force Python to let us eliminate tail calls by using a trampoline. We will go through two iterations of the design: first to get it to work, and second to try to make the syntax seem reasonable. I would not consider this a useful technique in itself, but I do think it’s a good example which shows off some of the power of decorators.
The first thing we should be clear about is the definition of a tail call. The “call” part means that we are considering function calls, and the “tail” part means that, of those, we are considering calls which are the last thing a function does before it returns. In the following example, the recursive call to f is a tail call (the use of the variable ret
is immaterial because it just connects the result of the call to f
to the return statement), and the call to g
is not a tail call because the operation of adding one is done after g
returns (so it’s not in “tail position”).
def f(n) : if n > 0 : n -= 1 ret = f(n) return ret else : ret = g(n) return ret + 1
1. Why tail calls matter
Recursive tail calls can be replaced by jumps. This is called “tail call eliminination,” and is a transformation that can help limit the maximum stack depth used by a recursive function, with the benefit of reducing memory traffic by not having to allocate stack frames. Sometimes, recursive function which wouldn’t ordinarily be able to run due to stack overflow are transformed into function which can.
Because of the benefits, some compilers (like gcc
) perform tail call elimination[1], replacing recursive tail calls with jumps (and, depending on the language and circumstances, tail calls to other functions can sometimes be replaced with stack massaging and a jump). In the following example, we will eliminate the tail calls in a piece of code which does a binary search. It has two recursive tail calls.
def binary_search(x, lst, low=None, high=None) : if low == None : low = 0 if high == None : high = len(lst)-1 mid = low + (high - low) // 2 if low > high : return None elif lst[mid] == x : return mid elif lst[mid] > x : return binary_search(x, lst, low, mid-1) else : return binary_search(x, lst, mid+1, high)
Supposing Python had a goto
statement, we could replace the tail calls with a jump to the beginning of the function, modifying the arguments at the call sites appropriately:
def binary_search(x, lst, low=None, high=None) : start: if low == None : low = 0 if high == None : high = len(lst)-1 mid = low + (high - low) // 2 if low > high : return None elif lst[mid] == x : return mid elif lst[mid] > x : (x, lst, low, high) = (x, lst, low, mid-1) goto start else : (x, lst, low, high) = (x, lst, mid+1, high) goto start
which, one can observe, can be written in actual Python as
def binary_search(x, lst, low=None, high=None) : if low == None : low = 0 if high == None : high = len(lst)-1 while True : mid = low + (high - low) // 2 if low > high : return None elif lst[mid] == x : return mid elif lst[mid] > x : high = mid - 1 else : low = mid + 1
I haven’t tested the speed difference between this iterative version and the original recursive version, but I would expect it to be quite a bit faster because of there being much, much less memory traffic.
Unfortunately, the transformation makes it harder to prove the binary search is correct in the resulting code. With the original recursive algorithm, it is almost trivial by induction.
Programming languages like Scheme depend on tail calls being eliminated for control flow, and it’s also necessary for continuation passing style.[2]
2. A first attempt
Our running example is going to be the factorial function (a classic), written with an accumulator argument so that its recursive call is a tail call:
def fact(n, r=1) : if n <= 1 : return r else : return fact(n-1, n*r)
If n
is too large, then this recursive function will overflow the stack, despite the fact that Python can deal with really big integers. On my machine, it can compute fact(999)
, but fact(1000)
results in a sad RuntimeError: Maximum recursion depth exceeded
.
One solution is to modify fact to return objects which represent tail calls and then to build a trampoline underneath fact which executes these tail calls after fact returns. This way, the stack depth will only contain two stack frames: one for the trampoline and another for each call to fact.
First, we define a tail call object which reifies the concept of a tail call:
class TailCall(object) : def __init__(self, call, *args, **kwargs) : self.call = call self.args = args self.kwargs = kwargs def handle(self) : return self.call(*self.args, **self.kwargs)
This is basically just the thunk lambda : call(*args, **kwargs)
, but we don’t use a thunk because we would like to be able to differentiate between a tail call and returning a function as a value.
The next ingredient is a function which wraps a trampoline around an arbitrary function:
def t(f) : def _f(*args, **kwargs) : ret = f(*args, **kwargs) while type(ret) is TailCall : ret = ret.handle() return ret return _f
Then, we modify fact to be
def fact(n, r=1) : if n <= 1 : return r else : return TailCall(fact, n-1, n*r)
Now, instead of calling fact(n)
, we must instead invoke t(fact)(n)
(otherwise we’d just get a TailCall object).
This isn’t that bad: we can get tail calls of arbitrary depth, and it’s Pythonic in the sense that the user must explicitly label the tail calls, limiting the amount of unexpected magic. But, can we eliminate the need to wrap t around the initial call? I myself find it unclean to have to write that t
because it makes calling fact different from calling a normal function (which is how it was before the transformation).
3. A second attempt
The basic idea is that we will redefine fact to roughly be t(fact)
. It’s tempting to just use t
as a decorator:
@t def fact(n, r=1) : if n <= 1 : return r else : return TailCall(fact, n-1, n*r)
(which, if you aren’t familiar with decorator syntax, is equivalent to writing fact = t(fact)
right after the function definition). However, there is a problem with this in that the fact in the returned tail call is bound to t(fact)
, so the trampoline will recursively call the trampoline, completely defeating the purpose of our work. In fact, the situation is now worse than before: on my machine, fact(333)
causes a RuntimeError
!
For this solution, the first ingredient is the following class, which defines the trampoline as before, but wraps it in a new type so we can distinguish a trampolined function from a plain old function:
class TailCaller(object) : def __init__(self, f) : self.f = f def __call__(self, *args, **kwargs) : ret = self.f(*args, **kwargs) while type(ret) is TailCall : ret = ret.handle() return ret
and then we modify TailCall
to be aware of TailCallers
:
class TailCall(object) : def __init__(self, call, *args, **kwargs) : self.call = call self.args = args self.kwargs = kwargs def handle(self) : if type(self.call) is TailCaller : return self.call.f(*self.args, **self.kwargs) else : return self.call(*self.args, **self.kwargs)
Since classes are function-like and return their constructed object, we can just decorate our factorial function with TailCaller
:
@TailCaller def fact(n, r=1) : if n <= 1 : return r else : return TailCall(fact, n-1, n*r)
And then we can call fact directly with large numbers!
Also, unlike in the first attempt, we can now have mutually recursive functions which all perform tail calls. The first-called TailCall
object will handle all the trampolining.
If we wanted, we could also define the following function to make the argument lists for tail calls be more consistent with those for normal function calls:[3]
def tailcall(f) : def _f(*args, **kwargs) : return TailCall(f, *args, **kwargs) return _f
and then fact could be rewritten as
@TailCaller def fact(n, r=1) : if n <= 1 : return r else : return tailcall(fact)(n-1, n*r)
One would hope that marking the tail calls manually could just be done away with, but I can’t think of any way to detect whether a call is a tail call without inspecting the source code. Perhaps an idea for further work is to convince Guido von Rossum that Python should support tail recursion (which is quite unlikely to happen).
[1] This is compiler-writer speak. For some reason, “elimination” is what you do when you replace a computation with something equivalent. In this case, it’s true that the call is being eliminated, but in its place there’s a jump. The same is true for “common subexpression elimination” (known as CSE), which takes, for instance,
a = b + c d = (b + c) + e and replaces it with a = b + c d = a + e
Sure, the b+c
is eliminated from the second statement, but it’s not really gone...
The optimization known as “dead code elimination” actually eliminates something, but that’s because dead code has no effect, and so it can be removed (that is, be replaced with nothing).
[2] In Scheme, all loops are written as recursive functions since tail calls are the pure way of redefining variables (this is the same technique Haskell uses). For instance, to print the numbers from 1 to 100, you’d write
(let next ((n 1)) (if (<= n 100) (begin (display n) (newline) (next (+ n 1)))))
where next is bound to be a one-argument function which takes one argument, n
, and which has the body of the let
statement as its body. If that 100
were some arbitrarily large number, the tail call to next had better be handled as a jump, otherwise the stack would overflow! And there’s no other reasonable way to write such a loop!
Continuation passing style is commonly used to handle exceptions and backtracking. You write functions of the form
(define (f cont) (let ((cont2 (lambda ... (cont ...) ...))) (g cont2)))
along with functions which take multiple such f’s and combines them into another function which also takes a single cont argument. I’ll probably talk about this more in another page, but for now notice how the call to g is in the tail position.
[3] This is basically a curried[4] version of TailCall
.
[4] That is, Schönfinkelized.
Miscellaneous
Useful Scheme Procedures
Source: Spring 2014 Piazza (2226)
Here is a short list of Scheme procedures that you might use in writing your programs:
; define - defines a variable or a procedure (define my-variable 4) (define (square x) (* x x)) ; if - conditional branching akin to if ... else (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))) )) ; conditional branching akin to if ... elif ... elif ... else (define (deep-map f lst) (cond ((null? lst) lst) ((list? (car lst)) (cons (deep-map f (car lst)) (deep-map f (cdr lst)))) (else (cons (f (car lst)) (deep-map f (cdr lst)))) )) ; and - outputs the the rightmost value if all of the arguments evaluate to #t. Outputs #f otherwise. STk> (and 0 1 2 3) 3 ; or - outputs the the first value that evaluates to #t. Outputs #f otherwise. STk> (or 0 1 2 3) 0 ; equal - tests if symbols are the same STk> (equal? 'foo 'bar) #f STk> (equal? 'foo 'foo) #t STk> (list? 'foo) #f STk> (list? '(1 2 3)) #t STk> (list? '()) #t STk> (null? '(1 2 3)) #f STk> (null? ()) #t ; member? - tests if a symbol is in a list EDIT: NOT BUILT IN (BUT SUPER USEFUL SEE FOLLOWUP) STk> (member? 'quick '(the quick brown fox jumped over the lazy dog)) #t ; number? - checks if input is a number STk> (number? 42) #t STk> (number? #t) #f ; remainder - computes the remainder of the first number divided by the second STk> (remainder 100 21) 16
Streams
Logic
Logic Guide (PDF)
Quick Guide to Logic Programming
Source: Spring 2014 Piazza (2524) Note: Someone should convert this from scmlog to Logic notation
Here's something I wrote a long time ago. The logic interpreter scmlog still exists and you should be able to access it using your cs61a-xx account. Post a followup if you have any questions. Hope this helps!
Introduction: logic programming is a completely different way to think about telling computers to do stuff. Instead of telling the computer what to compute, you give the computer facts and ask it questions. The computer does its own thinking given the facts and the question, and then returns an answer.
Table of Contents - Introduction - What is Logic Programming - - Giving Facts and Asking Questions - - More Complicated Facts and Questions - How to write Logic Programs - - Common Pitfalls - More Resources
What is Logic Programming?
Logic Programming is a way to ask the computer questions and get answers without telling it explicitly how to reach the conclusion. Kind of like this: http://youtu.be/tpKx7Oi0oeM
Anyways, we have a logic programming interpreter called Scmlog. It interprets a simpler version of the logic programming language, Prolog. The first few things to understand about it is that SCMLOG IS NOT SCHEME. It just happens to look like it. Scmlog is its own language and so it has its own rules. You can't program in Scmlog like you would in Scheme or Python, so it might be good to forget what you know about those languages for a second.
When you fire up scmlog (type scmlog into a terminal on the school computers), you get this prompt:
star [501] ~ # scmlog scmlog (Prolog in Scheme), v. 0.2 Type 'help' for help; 'quit' to exit. ?-
You can interact with this prompt in two ways:
- Give facts - "Let's tell the computer some things it should know!"
- Ask questions - "Let's ask the computer questions about the things we told it about!"
Giving Facts and Asking Questions
A basic fact takes this form: (fact (assertion))
, Where each assertion is simply a relation between things.
For example: (fact (likes potstickers brian))
relates three ideas, liking something, potstickers, and some guy name Brian. "Brian likes potstickers."
Note that we put the relation first, and then the parties the relation acts upon. In this example, "likes" is the relation, it hooks up "brian" and "potstickers". Here's some more:
?- (fact (likes potstickers brian)) ?- (fact (likes potstickers andrew)) ?- (fact (likes the_beatles brian)) ?- (fact (likes the_beatles andrew)) ?- (fact (likes led_zeppelin andrew)) ?- (fact (dislikes led_zeppelin brian))
Now that we've given the computer a bunch of facts, how do we ask questions about them? Just replace "fact" with a "?", and replace any part of the relation (besides the relation itself) with a variable prefixed by an underscore. This is called "querying":
?- (? (likes _what brian)) _what : potstickers More? _what : the_beatles More? ?- (? (likes potstickers _who)) _who : brian More? _who : andrew More? ?- (? (dislikes _what brian)) _what : led_zeppelin More? ?- (? (likes led_zeppelin andrew)) Yes. ?- (? (dislikes the_beatles _who)) No.
Notice:
- We can query any part of the assertion, besides the relation itself (can't replace "likes" with a variable).
- All possible answers to the question show up.
- If Scmlog couldn't find a fact that matched your query, it'll say "No."
- Asking a question without any variables essentially asks if that fact exists. To which Scmlog will answer "Yes." or "No."
More Complicated Facts and Questions Now this isn't the whole picture. We also have the ability to make more powerful assertions via variables, hypotheses, and conclusions:
(fact (ancestor _x _y) (parent _x _y))
"X is an ancestor of Y if X is a parent of Y"
Here _x and _y are variables like usual. However, we have two parts to this fact, the conclusion ("X is an ancestor of Y") and the hypothesis ("X is a parent of Y"). We can have more than one hypotheses, and they can be any kind of query. Here's ancestor in action:
?- (fact (parent george paul)) ?- (fact (parent martin george)) ?- (fact (parent martin martin_jr)) ?- (fact (parent martin donald)) ?- (fact (parent george ann)) ?- (fact (ancestor _X _Y) (parent _X _Y)) ?- (fact (ancestor _X _Y) (parent _X _Z) (ancestor _Z _Y)) ?- (? (ancestor paul george)) No. ?- (? (ancestor george paul)) Yes. ?- (? (ancestor george george)) No. ?- (? (ancestor martin paul)) Yes.
Now there's one more thing you need to know about Scmlog. Scmlog knows about pairs and lists:
?- (fact (lst (1 2 3))) ?- (? (lst _x)) _x : (1 2 3) More? ?- (? (lst (1 . _x))) _x : (2 3) More? ?- (? (lst (1 2 . _x))) _x : (3) More? ?- (? (lst (_x . _y))) _x : 1 _y : (2 3) ?- (fact (my_pair (2 . 3))) ?- (? (my_pair _y)) _y : (2 . 3) ?- (? (my_pair (2 . _x))) _x : 3
How to write Logic Programs
The trick behind writing logic programs is to forget everything you know about programming. You instead want to focus on the relation you're trying to establish. Take append for example:
?- (fact (append () _b _b)) ?- (fact (append (_x . _rest) _b (_x . _z)) (append _rest _b _z)) ?- (? (append (1 2 3) (3 2 1) _answer)) _answer : (1 2 3 3 2 1) ?- (append (1 2) 3 (1 2 3)) Huh? ?- (? (append (1 2) 3 (1 2 3)) ) No. ?- (? (append (1 2) (3) (1 2 3)) ) Yes. ?- (? (append (1 2) (3) (1 2 3))) Yes. ?- (? (append _x (3 4 9) (1 0 3 2 3 4 9))) _x : (1 0 3 2) More? ?- (? (append _x _y (2 1))) _x : () _y : (2 1) More? _x : (2) _y : (1) More? _x : (2 1) _y : ()
What is append? Well the first fact establishes that if we append an empty list to something, the result is that something. The next fact is the meat of the code. You can almost think of this as a recursive relation. Any append relation between some three lists a b c must also fulfill another append relation between the rest of a, b, and the rest of c, respectively. Think about why that will always be true for all inputs. If the example above doesn't make sense to you, see the following:
Common Pitfalls
- One of the biggest issues people have with writing logic programs is that they don't realize that the variables don't work like they do in Scheme or Python. All Scmlog ever does is pattern match:
?- (fact (x (0 1 8))) ?- (fact (x (1 8 4))) ?- (fact (x (9 4 4))) ?- (fact (x (3 0 8))) ?- (? (x _y)) _y : (0 1 8) More? _y : (1 8 4) More? _y : (9 4 4) More? _y : (3 0 8) ?- (? (x (0 1 . _z))) _z : (8)
- SCMLOG IS NOT SCHEME. SCMLOG IS NOT PYTHON.
More Resources:
- http://www-inst.eecs.berkeley.edu/~cs61a/sp12/lectures/prolog/ReadMe
- http://www-inst.eecs.berkeley.edu/~cs61a/sp12/lectures/prolog/
- http://www-inst.eecs.berkeley.edu/~cs61a/sp12/discussion/week14/
- http://www-inst.eecs.berkeley.edu/~cs61a/fa13/slides/31-Logic_6pp.pdf
Logic Mathematics
Source: Spring 2014 Piazza (3050)
Student Question
Can someone explain the intuition behind the implementation of the increments and the addition facts? The following is from Mark's website
(fact (increment 0 1)) (fact (increment 1 2)) (fact (increment 2 3)) (fact (increment 3 4)) (fact (increment 4 5)) (fact (increment 5 6)) (fact (increment 6 7)) (fact (increment 7 8)) (fact (increment 8 9)) (fact (increment 9 10)) (fact (increment 10 11)) (fact (increment 11 12)) (fact (increment 12 13)) (fact (add 1 ?x ?x+1) (increment ?x ?x+1)) (fact (add ?x+1 ?y ?z+1) (increment ?x ?x+1) (increment ?z ?z+1) (add ?x ?y ?z)) (query (add 2 4 6)) ; expect Success!
Student Answer
First, we state a bunch of facts that denote relations between a number and the number that follows it (e.g. 1 and 2, 2 and 3, and so on). This is the increment fact.
Then, we state a "base" fact, which is:
(fact (add 1 ?x ?x+1) (increment ?x ?x+1))
In English, this fact states that some value ?x
added to 1 will give us some value ?x+1
(x+1
is a valid variable name in Logic) if and only if the fact (increment ?x ?x+1
) is true. As an example, (add 1 2 3)
is true if and only if (increment 2 3
) is true. This is true because of the increment facts we stated before.
Now, to deal with additions that aren't just the sum of two numbers in which one number is a 1, we need:
(fact (add ?x+1 ?y ?z+1) (increment ?x ?x+1) (increment ?z ?z+1) (add ?x ?y ?z))
I will try to give an intuition as to what is happening here.
We state our fact: that two numbers, ?x+1
and ?y
, will add up to some number ?z+1
if and only if the following 3 hypotheses are true:
- (
increment ?x ?x+1
) - "There exists some number?x
that is 1 less than?x+1
, and" - (
increment ?z ?z+1
) - "There exists some number?z
that is 1 less than?z+1
, and" - (
add ?x ?y ?z
) - "The numbers?x
and?y
will add up to?z
."
Consider the example of:
(query (add 2 4 6))
Here's an idea of what's happening when Logic tries to match the query with the facts you've stated.
- ?x+1 = 2, ?y = 4, ?z+1 = 6
- It finds a match for
?x = 1
, since (increment ?x ?x+1
) gives?x = 1
because?x+1 = 2
- It finds a match for
?z = 5
, since (increment ?z ?z+1
) gives?z = 5
because?z+1 = 6
- It then checks for a match for (
add ?x ?y ?z
) which in this case is (add 1 4 5
). This goes to our first "base" fact for add. (add 1 4 5
) is a success because (increment 4 5
) is a true fact [refer again to the "base fact" to see why this is the case]. This is also where we would get a "Failed.", if it turns out that (increment ?x ?x+1
) wasn't actually true! - Hence, all 3 of our hypotheses are true, and so (
query (add 2 4 6)
) is a success!
In this example, we only have to recurse once to get to our "base" fact. In other examples, where ?x+1
is not 2, but some number greater, such as 5, we will have to recurse 4 whole times to get to 1, at which point our "base" fact is reached.
This recursion is similar to this idea in mathematical equations:
x + y = z
is the same as
(x - 1) + y = (z - 1)
is the same as
(x - 2) + y = (z - 2)</pre? and so on... In Logic, we stop when we find that the first term (x) is 1, and then we use our increment facts to determine if the original statement is true, because all of these equations are equivalent. == Python syntax and semantics == === <code>print</code> vs <code>return</code> === ---- ==== Andrew's tips ==== [https://piazza.com/class/hoxc5uu6sud761?cid=779 Source: Spring 2014 Piazza (779)] Remember the differences between return and print. * <code>return</code> can only be used in a <code>def</code> statement. It returns a value from a function. Once Python evaluates a <code>return</code> statement, it immediately exits the function. * <code>print</code> is a function that displays its argument on the screen. It always returns <code>None</code>. Examples: <pre>def foo1(x): return x def foo2(x): print(x) >>> foo2(1) # In foo2, we print 1 ourselves using the print function 1 >>> foo1(1) # HERE, THE PYTHON INTERPRETER PRINTS THE RETURN VALUE OF FOO1. CANNOT STRESS HOW IMPORTANT TO UNDERSTAND THIS 1 >>> foo1(1) + 1 2 >>> foo2(1) + 1 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
Function decorators
How function decorators work
Source: Spring 2014 Piazza (638)
Student Question
I'm having difficulties understanding what exactly a function decorator is. Can someone elaborate and potentially provide me with an example other than the one in the readings?
Instructor Answer
So imagine you wanted your functions to print their arguments before they executed them. Here's one way to do this.
def loud(fn): def new_fn(*args): print(args) return fn(*args) return new_fn
Here's a function loud that takes in a function and returns a new function that when called, prints out its arguments, and then does what the old function does.
For example:
def sq(x): return x * x >>> sq(4) 16 >>> sq = loud(sq) # replace the old square with our loud one. >>> sq(4) (4,) 16
A function decorator does the same thing as the above. Assuming loud is defined, we can do this:
@loud def sq(x): return x * x >>> sq(4) (4,)
Student guides
How to learn computer science
Source: Spring 2014 Piazza (241)
If you've never programmed before, or if you've never taken a class quite like 61A before, things right now might be scary. Everything is strange and new and there quite a lot to take in all at once. So if you're having a hard time so far, here are a few articles that might help.
Note: these articles are pretty long, so feel free to read them in multiple sittings.
At the beginning, everything seems a bit scary in CS. Michelle Bu, a Berkeley alum and a crazy good hacker, shares one of her experiences when she was a wee n00b in 21 Nested Callbacks.
Start here! "A Beginner's Guide to Computer Science" Written by Berkeley's own James Maa. James is known for his killer walkthroughs (check out his Productivity guide). This article gives you some background on learning CS and then provides a practical guide on how to learn effectively.
How do we learn? Mark Eichenlaub explains in this Introduction to Learning Theory. This is quite possibly the best introduction to Learning Theory.
Sometimes, you're stuck and you end up really, really frustrated. Marcus Geduld explains Why do we get frustrated when learning something?
Quick guide on getting unstuck
Source: Quick Guide on Getting Unstuck (Retrieved June 16th, 2014)
A major frustration you might encounter in 61A is when you stare at a homework problem and have no idea where to start. Or you write some code and it doesn't pass the doctests, but now what? You work at it for a while, but next thing you know, you've been stuck for hours on the the same problem and have little to show about it.
So here's a checklist of things you can do when you're stuck. Experienced programmers do these things almost naturally (because of how much practice they've had being stuck), and so while they get stuck just as much as your or I, they always know what to do next.
- Do I understand what the problem is asking?
- If not, which part of the problem is confusing me?
- Identify the exact sentences/phrases/words/etc.
- Check the given examples. Do they make sense to me?
- Can I come up with my own examples? A good indicator that you understand the question is that you can come up with some nontrivial examples of how the function works.
- If not, which part of the problem is confusing me?
- What concepts should I use here?
- Do I understand the concepts? Can I explain the concept in English to one of my friends such that they get it?
- If not, go back and relearn the specific concepts that are unclear (through discussion, lab, lecture, etc.) Don't read the entire book in order to solve one problem..
- How do I apply the concept to the given problem?
- Do I understand the concepts? Can I explain the concept in English to one of my friends such that they get it?
- Write your code and test it.
- Use doctests, BUT ALSO LOAD IT INTERACTIVELY (python3 -i ...)
- Saying "my function works because the doctests pass" is a lot like saying "this airplane will fly because it has wings."
- If your code breaks, ask yourself:
- Does it error? Is it a....
- Syntax error? If so, find the syntax bug and fix it.
- Logic error? Is it something weird that you don't understand? (E.g. cannot add integer and tuple)
- Why did it do that? Why didn't it do what I expected? Trace through the code by hand with an example (sample values) you came up with in step 0. Add calls to
print
in order to figure out how your function is handling the arguments.
- Does it error? Is it a....
- Use doctests, BUT ALSO LOAD IT INTERACTIVELY (python3 -i ...)
- Am I missing a trick?
- Oftentimes you've never seen this type of problem before. This is expected on homework (and this is why homework can take a long time) because if you see it on the homework, then you will be familiar with it on the exam and when you program for fun and profit.
- The key here is just to learn the trick however you need to.
- Stare at it yourself
- Stare at it with others (peers in the class)
- Ask on PIazza what the approach is.
- Stare at it with the TAs/lab Assistants
- Once you figure it out, remember the trick so that you can use it next time.
- At any point you identify what you're stuck on, you can begin to resolve it.
- Use the tips above. Try things out on the interpreter. Review the lecture/discussion/labs/etc. Do whatever helps you get a better understanding of the problem.
- Once you have something specific that you're stuck on, you can ask other people in the class.
- Don't be afraid to ask. Everyone gets stuck and feels stupid sometimes. However, you get to choose how you react to it.
- At the same time, it really helps to work with people who are on about the same level in the course.
- Look on Piazza. Ask questions if yours hasn't come up yet. Be that awesome guy/girl who helps answer questions.
- You can ask the TA if all else fails. We are here to help you learn!
Here is an old algorithm for studying for tests (the final in this case), salvaged from the sands of time:
For each topic on the final, find problems on them and do them. If you can solve them on your own, move on. Else if you are stuck, look at the solution and figure out if you are missing a trick or if you do not understand the concepts. If the problem is that you are stuck on some random trick, just learn the trick. Stare at the solutions, ask Piazza, your TA, etc. Questions you should ask at this stage: What is the problem asking me to do? How was I suppose to follow the instructions to solve the problem? What part of the problem do I not understand? What is the fastest way to clear up that misunderstanding? Then if you think you are still stuck conceptually, review and learn the concept, however you learn best. Suggestions for picking up concepts quickly (~1-2 hours): Discussion notes typically have a very concise recap of the thing they are going over. There are guides for particularly tricky things on Piazza, like Logic, Pairs and Lists in Scheme, etc. Find them and go over them. Ask a TA: "what is the best way to learn X?" If these do not work and you are still shaky after an hour or two, it might be worth watching a lecture or reading the notes.
Composition
General style guidelines from 61A website
Source: Spring 2014 Piazza (149)
Student Question
Are we required to add any comments to our code to say what a function does, etc.? And does clarity of code count for this project, in which case should we write comments at the end of not-so-clear statements? Thanks.
Student Answer
Docstrings of each function are already provided. If you add a helper function, you should write a docstring for it.
The style guide on the course website advises: "Your actual code should be self-documenting -- try to make it as obvious as possible what you are doing without resorting to comments. Only use comments if something is not obvious or needs to be explicitly emphasized"
Instructor Answer
You should always aim to make your code "self-documenting," meaning it is clear what your code is doing without the aid of comments. You should try to keep the number of comments to a minimum, but if there are lines which you think are unclear/ambiguous, feel free to add a comment.
All projects in this class contain a 3 point component that is judged solely on your code "composition" -- i.e. whether your code is clear, concise, and easy to read.
Simplifying code
Source: Spring 2014 Piazza (1116)
Hi everyone, here's some tips about certain functions in Python that can greatly simplify your code for the Trends project.
Sorting keys
You should be familiar with the max and min functions in python, which can take in many arguments and return the maximum value.
>>> max(1,3,2) 3
These functions can also take in lists:
>>> min([1,5,1,6]) 1
(In fact they can take in any iterable and return the maximum/minimum value)
These functions work because Python knows how to compare the elements in the list (they are all integers). But what if the elements in the list are not integers? Fortunately, there is a way for you to tell Python how to turn each element of the list into a number that it can understand.
Lets start with an example. Lets say you have a list of strings, and want to find the shortest string in the list. Here's what you can do:
>>> min(['hihi', 'bye', 'a', 'zebra'], key=len) 'a'
Notice the new keyword argument key we are passing into the min function. key is a function that min applies to each element of the list. In this case, the key is the len function, which returns the length of each string. Applying the key function to each element will return a cooresponding integer, which Python can easily use to find the minimum element.
You can also use keys in the sorted
function too, which returns a sorted list of its inputs, based on the key function passed in.
>>> sorted(['hihi', 'bye', 'a', 'zebra'], key=len) ['a', 'bye', 'hihi', 'zebra']
We can have more complex key functions. Here we sort a list of people by their age, which is the second element in the tuple. A key function, once defined, works for sorted, min and max:
>>> names = [('Alice', 19, 'F'), ('Bob', 5, 'M'), ('Charlie', 12, 'M')] >>> get_age = lambda name: name[1] >>> sorted(names, key=get_age) [('Bob', 5, 'M'), ('Charlie', 12, 'M'), ('Alice', 19, 'F')] >>> max(names, key=get_age) ('Alice', 19, 'F')
Dictionary default values
Suppose we have a dictionary mapping names to counts:
>>> d = {'apples': 1, 'pears': 9000}
If we want to add a new pear to the dictionary, we can use:
>>> d['pears'] = d['pears'] + 1 >>> d {'apples': 1, 'pears': 9001}
However we cannot use the same code to add a new item that is not already in the dictionary.
>>> d['oranges'] = d['oranges'] + 1 Traceback (most recent call last): ... KeyError: 'oranges'
To solve this problem, we have to use dict.setdefault(key, default)
. If key
is in dict
, it will return dict[key]
. If not, it will insert key
with a value of default
and return default
. Now we can write:
>>> d['oranges'] = d.setdefault('oranges', 0) + 1 >>> d {'oranges': 1, 'apples': 1, 'pears': 9001} >>> d['oranges'] = d.setdefault('oranges', 0) + 1 >>> d {'oranges': 2, 'apples': 1, 'pears': 9001}
There's actually a even better way of doing this. If you are curious to find out, look up collections.defaultdict
.
For loops
If you are iterating through a list and want to get both the item and the index the item is at, the built-in function enumerate is helpful here.
>>> a = ["apple", "pear", "orange"] >>> for index, fruit in enumerate(a): ... print(index, fruit) ... 0 apple 1 pear 2 orange
You can iterate through each key-value pair in a dictionary with dictionary.items. This is useful if you want to access both the key and the value at the same time.
>>> prices = {"apple": 3, "pear": 5, "orange": 20} >>> for fruit, price in prices.items(): ... print(fruit, price) ... apple 3 pear 5 orange 20
Hope this helps for the project!
Programming style in scheme
Source: Spring 2014 Piazza (2068)
Since Scheme has no rules on whitespace and indentation, you could technically write all your Scheme in one line like this:
(define (fib n) (if (< n 1) n (+ (fib (- n 1)) (fib (- n 2)))))
But that would be terrible and everyone who had to read your code would hate you for it. Here is a more reasonable version:
(define (fib n) (if (< n 1) ; Arguments to if are flush n ; Each argument gets a new line (+ (fib (- n 1)) ; Sometimes it makes to insert a newline (fib (- n 2))))) ; so that you can see arguments side by side
Remember that code is primarily for humans to read and incidentally for computers to run.
Here are some more examples:
(define (deep-map f lst) (cond ((null? lst) lst) ((list? (car lst)) (cons (deep-map f (car lst)) (deep-map f (cdr lst)))) (else (cons (f (car lst)) (deep-map f (cdr lst)))) )) STk> (deep-map (lambda (x) (* x x)) (list 1 2 (list (list 3) 4))) (1 4 ((9) 16))
(define (reverse lst) (define (helper lst result) (if (null? lst) result (helper (cdr lst) (cons (car lst) result)) )) (helper lst ()) ) STk> (reverse (list 1 2 3)) (3 2 1)
ucb.py's trace
method
Source: Spring 2014 Piazza (3000)
Student Question
How does the trace in ucb.py actually work?
Student Answer
It's actually very similar to the printed
function that was defined in the Hog spec!
def printed(fn): def print_and_return(*args): result = fn(*args) print('Result:', result) return result return print_and_return
The main idea is still the same in trace
-- we want to figure out all the arguments and save the result of calling the function with those arguments (so we can print it before returning it). One (sort of) major improvement is **kwds
. Just like how *args
collected all the "positional arguments", **kwds
captures all the "keyword arguments" (the ones of the form param='some_val'
). This can be a bit confusing, but a couple of examples might help!
>>> def add_three(a, b, c): ... return a + b + c >>> add_three(1, 2, 3) # all arguments are positional (normal) >>> add_three(1, 2, c=3) # a, b are positional arguments, c is a keyword argument >>> def fn(*args, **kwargs): ... print(args) ... print(kwargs) >>> fn(1, 2, 3) (1, 2, 3) {} >>> fn(1, k=2) (1,) {'k' : 2} >>> fn(a=1, b=2, c=3) () {'a':1, 'b':2, 'c':3}
Since there are only two types of arguments, having both *args
and **kwds
covers all our bases. If we passed printed
a keyword argument, it could cause an error!
Everything else in trace
just makes the output prettier and more helpful. trace
uses the _PREFIX
global variable to keep track of how far to indent the next print statement. It catches exceptions and prints them out, before re-raising that exception. It also uses some Python black magic to figure out the name of the function so we can print some_fn
instead of <function some_fn at 0x...>
.
If there's a particular aspect of trace
that you're confused about, feel free to post a followup!
Debugging
Miscellaneous
Andrew Huang's tips
Source: Spring 2014 Piazza (779)
Order of evaluation matters. The rules for evaluating call expressions are
- Evaluate the operator
- Evaluate the operands
- Call the operator on the operands (and draw a new frame...)
For example:
def baz(): print("this was first") def bar(x): print(x) return lambda x: x * x return bar # baz is a function that when called, returns a function named bar >>> baz() # the operator is baz, there are no operands this was first <function bar at 0x2797e20> >>> baz()("this was second") # the operator is baz(), the operand is "this was second" this was first this was second <function <lambda> at 0x2120e20> >>> baz()("this was second")(3) # the operator is baz()("this was second"), the operand is 3 this was first this was second 9 >>> def bar(x): ... print(x) ... return 3 ... >>> baz()("this was second")(bar("this was third")) # the operator is baz()("this was second"), the operand is bar("this was third") this was first this was second this was third 9
In order to solve any problem, you must first understand what the problem is asking. Often times it helps to try to explain it concisely in English. It also helps to come up with small examples. For example:
def mouse(n): if n >= 10: squeak = n // 100 n = frog(squeak) + n % 10 return n def frog(croak): if croak == 0: return 1 else: return 10 * mouse(croak+1) mouse(21023508479)
So the goal is to figure out what mouse(21023508479)
evaluates to.
One way is to just step-by-step evaluate this, as an interpreter would.
Another way, is to understand what the functions are doing.
Looking at mouse
, we see that it takes in a number and outputs that same number if it is smaller than 10. otherwise, it'll return something weird. In order to understand that weird thing, we have to understand what frog
is doing. frog
takes in a number and if that number is 0
, return 1
. Otherwise, return ten times mouse(croak+1)
. Well, this is still confusing. Let's try a small example.
>>> mouse(357) 47 >>> mouse(123) 23 >>> mouse(1234) 44 >>> mouse(12345) 245
There is a pattern. We notice that the resulting number is composed of every other digit of the original, plus one (except for the last one.)
So 21023508479
is [2+1][0+1][3+1][0+1][4+1][9] = 314159
. Can you see how the code reflects that?
However in this particular example, the pattern is definitely tricky to find here, so it might make more sense to brute force it.
Remember for recursion, you always need to find three things:
- One or more base cases
- One or more was to reduce the problem
- A way to solve the problem given solutions to smaller problems
For example, the discussion notes, we asked you to write count_stairs. This function takes in n, the number of steps, and returns all the ways you can climb up them if at each step, you can take either one or two steps.
- Base cases: if we consider n to be the number of steps left to climb, then it makes sense that if there is 1 step left, then there is exactly one way. If there are two steps left, then there are exactly 2 ways (1 step, 1 step, or two steps). Why do we need two base cases here?
- We can make the problem smaller by reducing the n. At each step, we can take one step (resulting in count_stairs(n-1)) or two steps (count_stairs(n-2)).
- Assuming we get the solutions to the two recursive calls, we should add them together to get all the ways we can climb the stairs.
Thus we end up with
def count_stairs(n): if n <= 2: return n else: return count_stairs(n-1) + count_stars(n-2)
Notice that at each stair step, we either take one step or two steps. This is a common pattern in tree recursion. Look through Discussion 3 for more info.
Y combinators (in Scheme)
Source: Spring 2014 Piazza (2450)
Student Question
Can someone explain this to me?
scm> (((lambda (f) (lambda (x) (f f x))) (lambda (f k) (if (zero? k) 1 (* k (f f (- k 1)))))) 5)
I've edited the code as follows:
( ( (lambda (f) (lambda (x) (f f x)) ) (lambda (f k) (if (zero? k) 1 (* k (f f (- k 1))) ) ) ) 5 )
My understanding is that the second lambda function is passed as the first f
in the first lambda function and the 5
is passed in as x
. But does that mean f f x
becomes the second lambda function with itself and x
passed as the arguments to (f k)
?
Student Answer
You're on the right track. The first lambda function is a higher order function that takes in a function, and then returns a function that takes one argument. It's actually the third lambda that is then passed into the first lambda (currying!) and then 5 is then passed into the resulting function.
In case you're curious, this is the Python equivalent:
>>> (lambda f: (lambda x: f(f, x))) (lambda f, k: 1 if k == 0 else (k * f(f, k - 1)))(5)
Which is then equivalent to:
>>> def func1(f): def func2(x): return f(f, x) return func2 >>> def func3(f, k): if k == 0: return 1 else: return k * f(f, k - 1) >>> func1(func3)(5) 120
By the way, this is just a fancy way of recursively calculating the factorial using only lambda functions. If you're still curious as to how this works, you could try this in Python tutor. Except I would recommend calculating 3! instead of 5, because it's a lot of frames.
Instructor Answer
Maybe it will look a little nicer in Python:
(lambda f: lambda x: f(f, x))(lambda g, k: 1 if k == 0 else (k * g(g, k-1)))(5)
Or maybe not.
So the idea is, you define a lambda function that takes a function f
, and that returns a lambda function that takes an argument x
and returns f(f, x)
. Then, you call this lambda function you just defined on another lambda function (let's call this func
) that takes a function g and another argument k, and is basically the factorial function. This first call returns the inner lambda of the first part, and when that's called with 5
you're essentially calling func(func, 5)
. The chain of recursive calls then works as follows:
func(func, 5) -> 5 * func(func, 4) -> 5 * 4 * func(func, 3) -> ... -> 120
In functional programming theory, this is known as a Y Combinator, and it is how you achieve recursion with just lambda functions. If you're wondering why we need func
to take in a function as the first parameter, see what would happen if you took that part out!
The role of the textbook in CS61A
Source: Summer 2014 Piazza (96)
There are hundreds of students that go through 61A each year, and many have been successful for different reasons, with some reading the course textbook Composing Programs very closely and others having not touched the reading. We will not give reading quizzes (e.g. no "what did the textbook say..." problems) on the assigned reading. Exam problems primarily involve applying the ideas, not the specific wording of the ideas themselves from the textbook.
Personally, I believe that the textbook is as important, if not more so, compared to all the other activities that we do. Especially important are the definitions and the rules from the first chapter. Then, when I got more comfortable with the material, I found that I needed to refer to the textbook less as I went on, focusing more on the lecture slides and discussion problems. Regardless, the book is >2 years old and words are permanent while speech is temporary, so the explanations in the book will clearly be more polished.
With that said, each lecturer has a different way of presenting the material, so some aspects that Andrew and Rohin (lecturers of Summer 2014) will cover are going to be different from those of the book.
If you would like to know more about our (Summer 2014 TAs) experiences of taking 61A, please feel free to come to our office hours to discuss this more!