No class today. Have a great vacation!
Due to problems with the school network, we were unable to access the CodeWorld site today. However, we can still work together on ideas. So we went on to talk about data types, and then answer some questions about the Rube Goldberg project.
Detailed notes are coming soon.
For these two classes, we did a sequence of practice simulations, to build the skills to create various simulations from the selection above.
For each simulation, we ask the standard set of questions:
initial
function.step
function.picture
function.Once all four questions have been answered, the program can be written. There are always several ways to write the same program! So don't worry that there is a single right answer to these questions. The first question, in particular, has plenty of correct answers, each of which leads to a different program. For an example, let's compare two possible ways to solve challenge #4 (from the link in in-class resources above).
Here's a program using state with speeds:
main = simulationOf(initial, step, picture)
initial(rs) = (180, -45, -7, 0)
step(state, dt) = hit(swing(fly(state, dt), dt))
swing((ang, angv, x, xv), dt) = (ang + angv * dt, angv, x, xv)
fly((ang, angv, x, xv), dt) = (ang, angv, x + xv * dt, xv)
hit(ang, angv, x, xv)
| ang < 90 = (90, 0, x, 5)
| otherwise = (ang, angv, x, xv)
picture(ang, ang, x, xv) = ...
Everything is straight-forward until the hit function, which needs to check if the bat has swung far enough to hit the ball, and if so, stop the bat, and start the ball. Otherwise, nothing changes.
Here is the same function using only the angle and position in the state:
main = simulationOf(initial, step, picture)
initial(rs) = (180, -7)
step((ang, x), dt) = swing(fly(state, dt), dt)
| ang < 90 = (ang - 45 * dt, x)
| otherwise = (ang, x + 5 * dt)
picture(ang, x) = ...
Which program do you like better? There are advantages either way:
A similar approach can be used to solve the remaining puzzles, as well.
In today's class, we continued our Newton's Cradle simulations, extending them from one ball to two!
As a reminder, we recreated the program from the previous week:
main = simulationOf(initial, step, picture)
initial = (0, 5)
step(state, dt) = bounce(motion(state, dt))
bounce(x, v)
| x > 9 = ( 9, -v)
| x < -9 = (-9, -v)
| otherwise = ( x, v)
motion((x, v), dt) = (x + v * dt, v)
picture(x, v) = translated(solidCircle(1), x, 0)
When adding another ball, we determined that we will need a second position and a second velocity in the state. The resulting program looked like this:
main = simulationOf(initial, step, picture)
initial = (0, 5, 5, -3)
step(state, dt) = bounce(motion(state, dt))
bounce(x1, v1, x2, v2)
| x1 > 9 = ( 9, -v1, x2, v2)
| x1 < -9 = (-9, -v1, x2, v2)
| x2 > 9 = (x1, v1, 9, -v2)
| x2 < -9 = (x1, v1, -9, -v2)
| otherwise = (x1, v1, x2, v2)
motion((x1, v1, x2, v2), dt) = (x1 + v1 * dt, v1, x2 + v2 * dt, v2)
picture(x1, v1, x2, v2) = translated(solidCircle(1), x1, 0)
& translated(solidCircle(1), x2, 0)
All we did was take everything that worked on one ball, and make it work on both balls. The specific changes were:
Running this program is slightly disappointing, though, because the two balls pass through each other! We need one more case in bounce: for the balls bouncing off each other. This case is a little more tricky, so we slowed down to work out the parts in detail. There are several steps.
First, we needed to write an inequality to express that the two balls have collided. We drew a diagram of overlapping balls, and determined that the condition we need is that the distance between x1 and x2 is less than the sum of the radius of each ball. In our case, that means that x2 - x1 < 2.
Next, we needed to know what to do with the x coordinates to fix the collision. There are actually a few good answers here:
This is one of those decisions that makes a small difference, but as long as dt isn't very large, any choice works out in the end. In some sense, the most fair solution is the last one. But we chose the first, because it's easier.
Finally, we need to know the resulting velocities. The equations for this, in general, can be complicated (see here, example!), but we ran some experiments in class, instead. There are animations showing the results of our experiments about a page down in that link. We found a pattern, that when two balls collide, they just swap their velocities!
Putting it all together, we wrote this code, which is identical to the previous, but with a new line in bold:
main = simulationOf(initial, step, picture)
initial = (0, 5, 5, -3)
step(state, dt) = bounce(motion(state, dt))
bounce(x1, v1, x2, v2)
| x1 > 9 = ( 9, -v1, x2, v2)
| x1 < -9 = (-9, -v1, x2, v2)
| x2 > 9 = (x1, v1, 9, -v2)
| x2 < -9 = (x1, v1, -9, -v2)
| x2 - x1 < 2 = (x2 - 2, v2, x2, v1)
| otherwise = (x1, v1, x2, v2)
motion((x1, v1, x2, v2), dt) = (x1 + v1 * dt, v1, x2 + v2 * dt, v2)
picture(x1, v1, x2, v2) = translated(solidCircle(1), x1, 0)
& translated(solidCircle(1), x2, 0)
With this, we have succeeded in creating a two-ball Newton's cradle. However, it made the program significantly longer, and we had to spend a lot of time in class finding and fixing mistakes in the program. One can't be blamed for being a little afraid to keep on like this all the way to five balls!
Fortunately, the task of expanding to five balls (or, in fact, any number of balls!) is made much easier by the computer's ability to do repetitive calculations for us. In preparation for doing this, we need to package our balls into a list. Pattern matching makes this easy to do, just by rewriting the program above with a little extra punctuation.
main = simulationOf(initial, step, picture)
initial = [(0, 5), (5, -3)]
step(state, dt) = bounce(motion(state, dt))
bounce([(x1, v1), (x2, v2)])
| x1 > 9 = [( 9, -v1), (x2, v2)]
| x1 < -9 = [(-9, -v1), (x2, v2)]
| x2 > 9 = [(x1, v1), ( 9, -v2)]
| x2 < -9 = [(x1, v1), (-9, -v2)]
| x2 - x1 < 2 = [(x2 - 2, v2), (x2, v1)]
| otherwise = [(x1, v1), (x2, v2)]
motion([(x1, v1), (x2, v2)], dt) = [(x1 + v1 * dt, v1), (x2 + v2 * dt, v2)]
picture([(x1, v1), (x2, v2)]) = translated(solidCircle(1), x1, 0)
& translated(solidCircle(1), x2, 0)
We stopped here for the week. Next week, we'll use lists to easily extend this to five balls.
To start this class, we reviewed the bouncing ball simulation from the previous class, and investigated the meaning of each piece in more detail. This investigation is captured by the notes from last week's slides.
To consider different kinds of state that can occur in simulations, we then looked at this simulation, and tried to guess what it would do:
main = simulationOf(initial, step, picture)
initial = (-10, 0)
step((x, t), dt) | t < 3 = (x, t + dt)
| otherwise = (x + 1, 0)
picture(x, t) = translated(circle(1), x, 0)
Initially, it seemed that the ball might move up, and occasionally jump to the right. However, we eventually noticed that the second number in the ordered pair here is not a y coordinate. Ordered pairs can be points on the coordinate plane; but in simulations, they can also contain other numbers. What determines their meaning is how they are used in the program, not the fact that they are stored in an ordered pair.
In the end, we were able to work out the behavior with a table, keeping track of the state and the resulting frame, at each time step as the simulation went on. The second number turns out to be a timer - like a stopwatch - keeping track of how long it has been since the ball last moved. The step function then describes the behavior:
The resulting behavior was a ball that did not move smoothly, but jumped one unit to the right every 3 seconds.
Finally, we looked at writing a new simulation: the Newton's Cradle.
To start this simulation, we set out to model a single ball swinging back and forth. But we already know how to make a ball bounce off the sides, so we took that approach instead. The y coordinate need not change this way, so the only state needed is an x position, and an x velocity. We wrote this simulation of a ball bouncing from side to side in the cradle:
main = simulationOf(initial, step, picture)
initial = (0, 5)
step(state, dt) = bounce(motion(state, dt))
bounce(x, v)
| x > 9 = ( 9, -v)
| x < -9 = (-9, -v)
| otherwise = ( x, v)
motion((x, v), dt) = (x + v * dt, v)
picture(x, v) = translated(solidCircle(1), x, 0)
The simulation has two of the same parts as the bouncing ball from earlier: motion, and bounce. It is simpler, though, because the ball only moves in the x direction. This means that we only need two numbers in the state, and also that we can dispense with the gravity function, since gravity acts on the y axis.
We stopped here for this simulation today, intending to add more balls next week.
We continued with simulations in this class. We started with a review of last class's simulations. Then everyone was challenged to build a simulation of a spinning rectangle.
We then went on to talk about some physics. In physics, we say that every object has a position, which tells us where it is, and a velocity, which tells us how fast, and in which direction, it is moving. Each of those has an x and y part. To make an object move realistically, we want to keep track of its position and velocity, in both x and y. This means four numbers in the simulation state. We can start with a simple simulation like this.
main = simulationOf(initial, step, picture)
initial(rs) = (-5, -5, 5, 10)
step((x, y, vx, vy), dt) = (x + vx * dt, y + vy * dt, vx, vy)
picture(x, y, vx, vy) = translated(solidCircle(1), x, y)
This ball will move diagonally on the screen. We can make these observations about the ball:
x + vx * dt
means that from one frame to the next, the x position increases by the velocity, times the amount of time that passed. The same thing happens for y, as well.We now want to add some more effects to our simulation. We'll do this by breaking down the step function into several parts:
step(s, dt) = bounce(gravity(motion(s, dt), dt))
motion((x, y, vx, vy), dt) = (x + vx * dt, y + vy * dt, vx, vy)
gravity((x, y, vx, vy), dt) = (x, y, vx, vy - 10 * dt)
bounce(x, y, vx, vy)
| y < -9 = ( x, -9, vx, -vy)
| y > 9 = ( x, 9, vx, -vy)
| x < -9 = (-9, y, -vx, vy)
| x > 9 = ( 9, y, -vx, vy)
| otherwise = ( x, y, vx, vy)
There are three things going on here: First, motion tells the computer that the position of an object changes according to its current velocity. Second, gravity tells the computer that a object's y velocity decreases by 10 every second, because it's pulled downward by gravity. Finally, bounce tells the computer that if an object moves off the screen in either x or y, it should be moved back, and that velocity in that component should switch directions. All three of these functions represent rules of physics that make our simulation work!
Notice how we've used step to put them all together. In the end, you need one step function that combines all of the rules about how the system changes over time. We can just nest the other functions together, so that they all happen in turn.
Looking at this program:
It's worth noticing that now, the definitions of main, initial, and picture are very simple: just one line long! The interesting work we're doing happens in step. From here on, step will usually be the heart of our programs!
In this class, we started learning about simulations. Simulations, like animations, result in a moving display on the screen. But an animation has to provide a function that can tell you what the program looks like at any instant, regardless of what happened before. Simulations can remember things, so what's happening 20 seconds into the program can depend on what happened in those 20 seconds. This makes our programs more powerful, and gets us closer to writing games.
We began with a typical animation:
main = animationOf(ball)
ball(t) = translated(circle(1), 2 * t - 10, 0)
We can make some observations about this animation:
translated(circle(1), x, 0)
.In animations, all of these facts are mixed together in one definition. In simulations, each answer is handled separately. Here is the equivalent program as a simulation.
main = simulationOf(initial, step, picture)
initial(rs) = -10
step(x, dt) = x + 2 * dt
picture(x) = translated(circle(1), x, 0)
Every simulation has a state. A state is a description of what is happening in the simulation at an instant in time. Our first observation above, that only the x coordinate changes, was about the state. In this simulation, the state is the x coordinate. (In later simulations, though, the state can be something else, even something much more complicated than a single number.) The state is what gives simulations memory, so anything that should be remembered needs to go in the state somewhere.
Once we've settled on the state, we define the three parameters to simulationOf. These can be called anything, but in this class, we'll always call them initial, step, and picture.
You can run the program above to see it work. But we can also do the computation ourselves, and that can help us understand the process better. The table below shows how this works. Notice that the dt parameter to step is normally a very small number! That's because it is the number of seconds from one frame to the next. Frames are usually shown very quickly: up to 60 frames every second. So dt will actually be something like 0.02, depending on how fast your computer is; but we often pick bigger values to try out, to make the math easier.
frame | oldState | dt | step(oldState, dt)
-----------------------------------------------------
1 | -10.0 | 0.1 | -10.0 + 2 * 0.1 = -9.8
2 | -9.8 | 0.1 | -9.8 + 2 * 0.1 = -9.6
3 | -9.6 | 0.2 | -9.6 + 2 * 0.2 = -9.2
4 | -9.2 | 0.1 | -9.2 + 2 * 0.1 = -9.0
Before every row, the state is converted into a picture, which is shown. Then the step function is evaluated with that old state, and the amount of time until the next frame. Once that's done, a new frame is drawn and the process starts over again.
After playing with the first example above (such as, for example, making the movement in the y dimension instead of x), we take on a new challenge. Now we want the ball to move diagonally, in both x and y directions. There is more than one way to solve this problem, but the one we'll choose is to use a state with two numbers in it: x and y. Now the state won't be a number, but rather an ordered pair of numbers. The program looks like this:
main = simulationOf(initial, step, picture)
initial(rs) = (-10, 10)
step((x, y), dt) = (x + 2 * dt, y - 3 * dt)
picture(x, y) = translated(circle(1), x, y)
This is mostly what you'd expect, but there as a few tricky bits. Taking each of the differences from the earlier program in turn:
This program, then, does everything needed to run a simulation with several values in its state.
In general, it was fairly easy to reason about an animation and tell what it was going to do. Simulations, though, can often have behavior that happens unexpectedly. You give the rules that govern what the system does, but the consequences of those rules can be interesting, or surprising! We'll see later that we can even run science experiments using simulations, and learn things from what we see.
This next simulation isn't much different from the example we wrote previously. But it behaves in a much more interesting way:
main = simulationOf(initial, step, picture)
initial(rs) = (0, 8)
step((x, y), dt) = (x + y * dt, y - x * dt)
picture(x, y) = translated(circle(1), x, y)
This different from the previous example only in that the speed in the x direction depends on position in y, and vice versa. But try this out! The result, which isn't obvious at all, is that the dot moves around in a circle! To be sure of this without running the program, you would need to use some very advanced math: differential equations, which is studied by engineers and mathematicians in college, can be used. But we can also just run the program, and let the computer tell us!
The one thing to worry about is that the computer isn't perfectly fast, and only gives an approximate answer to the equations you've written. That's because it only updates the state once each frame. For example, the equations in this program describe a circle, but if you look closely, you will notice that the radius of the path actually grows slowly over time. Most of the time, though, we don't need our programs to be exact, and this is okay!
Next class, we'll look at some more interesting examples of step functions, including how to make things fall and bounce!
We finished developing our understanding of functions in two ways.
Recall that we have talked about three representations of functions:
We started this class by taking a closer look at the third representation: graphs of functions. Graphs exist on a coordinate plane, and are produced by connecting points, where the x (horizontal) coordinate of each point is an input to the function, and the y (vertical) coordinate is the output. We can create a graph on paper by plotting a few of these points, and connecting them with a continuous line.
We can also use CodeWorld to ask the computer to graph functions for us. We built together a program to draw a graph of a function:
main = drawingOf(graph)
graph = coordinatePlane & path([(x, f(x)) | x <- [-10 .. 10]])
f(x) | t < 8 = t
| otherwise = 4 + t / 2
By writing this, we have made the computer act like a graphing calculator, and graph the function for us. We then played around with different definitions for the function f to see how the graph differs. For example, f(t) = t * t
produces a graph of a parabola.
Differently shaped graphs of functions let us accomplish different effects in animation, so to become better animators, one thing we can do is build up a repertoire of functions of different shapes, so that we can choose between them when animating things. We took a few steps toward this, with the following list:
f(t) = t
is a linear function, and so is f(t) = 3 * t - 5
. We can describe any linear function by talking about a rate of change, which is the number multiplied by the input, and a starting point, which is the number added or subtracted. (When talking about the graph of the function, these numbers are sometimes called the slope and y-intercept.)f(t) = -3 * t * t
is a quadratic function. Quadratic functions can speed up, and even change direction once! Because they speed up over time, they are useful for animations where things are falling.f
that we graphed in the example above is piecewise, because it has one definition when t < 8, and a different definition otherwise.sin
function. This function is interesting in its own right, and is studied in trigonometry (typically in high school). But for our purposes, it's enough to know that it can be used to make something move back and forth repeatedly. Multiplication can be used to change both the frequency (how quickly it repeats) and the amplitude (how much it changes in each repetition). So f(t) = 5 * sin(60 * t)
is a periodic function, where the 60 determines the frequency, and the 5 determines the amplitude.For the second half of the class, we returned to formulas to describe functions, and looked at a trick called recursion. This lets a function be defined using itself! One use for recursion is to define fractals, such as pictures that contain smaller copies of themselves. Consider this picture, for example:
If you look carefully, you'll notice that inside the large circle, there are two smaller copies of exactly the same image. Making a first attempt to describe this picture, we might try to write this:
googleyEyes = circle(8)
& translated(scaled(googleyEyes, 1/2, 1/2), -4, 0)
& translated(scaled(googleyEyes, 1/2, 1/2), 4, 0)
In theory, this is a correct description of the shape. However, asking for a drawing of this picture will just crash the program! That's because the picture we're trying to draw is infinitely detailed along the center line, and computers can't draw something with an infinite amount of detail. They would simply never finish!
To work around this, we need to ask the computer to draw an approximation of the picture, instead. We can use functions to add a parameter, which is how many levels deep to draw the smaller versions of the shape before stopping. The program looks like this.
main = drawingOf(googleyEyes(10))
googleyEyes(0) = circle(8)
googleyEyes(depth) = circle(8)
& translated(scaled(googleyEyes(depth - 1), 1/2, 1/2), -4, 0)
& translated(scaled(googleyEyes(depth - 1), 1/2, 1/2), 4, 0)
First, we add the depth parameter to googleyEyes
, and in the definition of main
, we ask for a picture with a depth of 10 levels. Then we write two equations for the function. The first says that once the depth has reached 0, the computer should stop drawing smaller copies and only draw one large circle. When the depth is greater than zero, in the second equation, it should continue to draw smaller copies. However, these copies should have a smaller depth than the current shape, so that ultimately we'll reach a depth of 0, and the program will finish.
This pattern can be followed to draw many different fractal images. These include a bunch of different shapes, such as trees, geometric patterns, and more. The challenge for the next week, should you choose to accept it, is two-fold:
In this class, we further developed the notion of a function.
To start with, we looked at a simple example of a function, and wrote tables of inputs and outputs. These tables of inputs and outputs are the true meaning of a function. We drew these tables first for a built-in function, circle. Then we did the same for a function defined in CodeWorld:
f(t) = rotated(translated(circle(1), t, 0), 30 * t)
We can evaluate the value of the function at each input, by following these steps. First, make a copy of the right-hand side of the definition. Second, substitute the input anywhere the parameter name occurs. Finally, simplify and evaluate expressions in the result. So we can easily convert from a formula - which is the way functions are defined in CodeWorld - into a table of values. (Converting the other way, from a table of values to a formula, requires more creativity. In fact, it is in some ways the main challenge of computer programming!)
Next, we looked at the UFO example, in the resource links earlier, and looked for ways to simplify these animations by introducing functions. The first UFO program defines an animation like this:
scene :: Number -> Picture
scene(t) = translated(ufo, 3 * t - 15, 2)
& translated(shadow, 3 * t - 15, -5)
We don't like to repeat ourselves, so we'd like to just write the 3 * t - 15
once. But it depends on a value of t, so this is actually a function in disguise.
scene :: Number -> Picture
scene(t) = translated(ufo, x(t), 2)
& translated(shadow, x(t), -5)
x :: Number -> Number
x(t) = 3 * t - 15
We can take this a bit further in the second example, which defines this animation.
scene :: Number -> Picture
scene(t)
| t < 8 = translated(ufo, 3 * t - 15, 2)
& translated(shadow, 3 * t - 15, -5)
| otherwise = translated(ufo, 33 - 3 * t, 2)
& translated(shadow, 33 - 3 * t, -5)
We could start by extracting the common expressions, like 3 * t - 15
, and 33 - 3 * t
, into functions. But instead, let's notice that really, the picture part of this animation is the same both when t is less than 8, and greater than 8. So if we name the function that produces an x coordinate, we can also drop the guards, and write the exact same animation as we did in step 1, even though the motion is more complicated now!
scene :: Number -> Picture
scene(t) = translated(ufo, x(t), 2)
& translated(shadow, x(t), -5)
The logic of turning around the UFO has to live somewhere, of course! So it moves to the new function, x
.
x :: Number -> Number
x(t) | t < 8 = 3 * t - 15
| otherwise = 33 - 3 * t
There's a bigger picture here. In the past semester of CodeWorld, we've been rather single-mindedly focused on building up pictures. This is really the first time we've defined a function that does not build a picture. Instead, we're thinking about the relationships between the data, such as numbers, used in our programs. By writing the function x
as a function on numbers, we were able to separate the question of where the UFO is at different times from the different question of what a UFO and its shadow look like in general.
As we move from pictures and animations into more powerful kinds of programs, we'll do a lot more of this. So we will be writing a lot more functions that work with other types, like numbers, points, or even lists of things!
One side advantage of working with functions like x, which map numbers to other numbers, is that they can be graphed. Just like tables and formulas, graphs are another way of thinking about a function. But they really only make sense for functions that work with numbers. We only looked at this briefly this week, but it's a really useful way to understand what a function is doing! Here is a web site I found with a few examples of converting from a formula, to a table of values, and from there to a graph.
This class started with a review of resources available:
We reviewed the related ideas of function, domain, and range. A function expresses a relationship between inputs and outputs. The type, or form, if the inputs is called the domain. The type, or form, of outputs is called the range. We can write a function's type by writing its domain and range, separated by an arrow.
It's possible to be explicit about types when defining functions in CodeWorld. A type annotation looks like this:
fish :: Color -> Picture
You can read ::
as "has type". This line tells the computer that fish
has the type Color -> Picture
. In other words, fish
is a function with a domain of Color
, and a range of Picture
. You never have to write type annotations. They are completely optional, and the computer can figure out the types of functions on its own. But here's why you might want to write them anyway:
We further investigated functions with numbers in their domain. Starting from the first in-class resource above (code link), we made person into a function, by adding three parameters:
The resulting program, using functions, looked something like this: (code link)
Next, we investigated functions with pictures in their domain. We started with the second in-class resource, a ring of stars. (code link) My making a simple change, we were able to name the idea of arranging pictures into a ring, using a function. This allowed us to easily get a ring of stars, or squares, or hearts, or rectangles, or whatever else we want. (code link)
In today's class, we talked more about functions. As most of you know already, a function described a relationship between input values (sometimes called parameters) and output values (sometimes called the result).
Different functions expect different types of values as inputs, and produce different types of values as outputs. The type of value expected by a function for its input is called the function's domain. The type of value it produces for output is called its range. We can write a function's type by writing its domain and range, separated by an arrow.
We made a list of some common functions, and their domains and ranges, like this:
Function | Domain | Range | Type
-------------+---------------------+----------+-------------------------------
drawingOf | Picture | Program | Picture -> Program
circle | Number | Picture | Number -> Picture
light | Color | Color | Color -> Color
sqrt | Number | Number | Number -> Number
rectangle | (Number, Number) | Picture | (Number, Number) -> Picture
colored | (Picture, Color) | Picture | (Picture, Color) -> Picture
animationOf | (Number -> Picture) | Program | (Number -> Picture) -> Program
We also talked about some symbols we know which are not functions. For example, codeWorldLogo is not a function, because it has no parameters. Instead, it's merely a variable. The same is true of main, red, and coordinatePlane. These are words known to the computer. But because they don't describe a relationship between a domain and a range, they are not functions.
When using a function, we talked about the three parts to a function application. Looking at the expression light(green), we say:
At the end of the last class, we talked about animations, which are one kind of function. But to start out this class, we'll look at a variety of different things you can accomplish using functions.
Our first example starts with this fish: (link to code) Suppose we want to draw both a blue and a green fish at the same time. There are several ways we might accomplish this:
colored(fish, blue)
. But this turns the entire fish solid blue, and we lose the shading and the eye.To finish up the class, you worked on your own examples, which involved a ball with a stripe. You defined the ball to start with, added the stripe, and then turned your ball into a function, so you could change the colors without losing the stripe.