A+ will be down for a version upgrade on Thursday October 17th 2024 at 09:00-12:00.
This course has already ended.

The latest instance of the course can be found at: O1: 2024

Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.

Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit. Myös suomenkielisessä materiaalissa käytetään ohjelmien koodissa englanninkielisiä nimiä kurssin alkupään johdantoesimerkkejä lukuunottamatta.

Voit vaihtaa kieltä A+:n valikon yläreunassa olevasta painikkeesta. Tai tästä: Vaihda suomeksi.


Chapter 8.3: Robots and Conditional Looping

About This Page

Questions Answered: How can I repeat sequence of a commands if I can’t find a higher-order method that’s sufficiently convenient or efficient for my needs? I’ve heard programmers talk about something called a while loop — what are they and does Scala have them? How about creating different sorts of robots?

Topics: Classics from the imperative programmer’s toolkit: do and while loops. A class hierarchy for robots. We’ll say a bit more about lazy-lists, too.

What Will I Do? First read, then program.

Rough Estimate of Workload:? This is one of the most time-consuming chapters in O1. Reading the text and doing the next two parts of the Robots assignment will probably take about two or three hours. The last four parts of the assignment might take another four or five hours.

Points Available: A15 + B50 + C70.

Related Modules: Robots. Plus some mini-examples in DoWhile (new).

../_images/person10.png

Introduction

Recap from 6.3 and 6.4: When solving a programming problem, you may be able to find a tool at a high level of abstraction that provides a simple solution for just what you need. If you don’t, you can build a solution using lower-level tools. Those lower-level tools may not be quite as convenient to use but they are applicable to a wider range of situations; any higher-level tools you have at your disposal havbe also been built from lower-level pieces. Occasionally, it makes sense to deliberately adopt lower-level tools in order to optimize efficiency.

An Example Program

Consider a toy example: We’re writing an interactive program that prompts the user for their name. We’d like the program to check that the user actually enters a string that contains at least one character; the program should keep re-prompting the user until it receives a non-empty string.

The program should work in the text console as illustrated below. In this example run, the user first just hits Enter twice, then actually inputs their name:

Enter your name (at least one character, please):
The name is 0 characters long.
Enter your name (at least one character, please):
The name is 0 characters long.
Enter your name (at least one character, please): Juha
The name is 4 characters long.
OK. Your name is Juha.

In Chapter 7.1, we approached problems like this by thinking about a lazy-list of inputs and the operations that we applied to the list elements. Alternatively, we may think of the program in imperative terms: as a sequence of commands that adjust the program’s state step by step. Let’s formulate such a solution, first as pseudocode:

var name = ""

Do the following things:
    name = readLine("Enter your name (at least one character, please): ")
    println("The name is " + name.length + (if (name.length != 1) " characters" else " character") + " long.")
    Finish by checking whether name equals the empty string,
    and if so, do these same things again. Otherwise, advance to the code that follows.

println("OK. Your name is " + name + ".")

Our pseudocode bears a resemblance to an if statement in that it checks a condition — is the name empty? — and uses the result to decide where to proceed in the program. The main difference is that the code is potentially executed multiple times, as long as the condition keeps being met.

On the other hand, this program resembles a for loop in that it too involves repetition, a loop. However, this loop isn’t tied to a collection like the for loops you’ve seen: it’s formed by repeatedly checking a particular condition.

The do Loop

Here is a Scala implementation for our pseudocode. You can also find this program inside the DoWhile module.

var name = ""
do {
  name = readLine("Enter your name (at least one character, please): ")
  println("The name is " + name.length + (if (name.length != 1) " characters" else " character") + "long.")
} while (name.isEmpty)
println("OK. Your name is " + name + ".")
We start the loop definition with do. Another keyword, while, appears on the line that ends the loop. These are two of Scala’s reserved words (like if etc.), not methods on an object.
We follow while with round brackets that contain a condition, just like we’d do in an if. This Boolean expression is evaluated each time the commands above have been executed.
The loop body is executed at least once, repeating each time the condition evaluates to true. In this example, the loop jumps back to the beginning each time name is checked and holds an empty string.
The last line in our example isn’t part of the loop. It’s executed only after the loop exits, which happens when the end of the loop is reached and the condition evaluates to false.

Such a dowhile loop is commonly known simply as a do loop. Here’s a generic outline for writing one:

(Up here, you might have some commands that precede the loop and are executed just once.)

do {
  One or more commands that are executed at least once
  and whose execution is followed by checking the condition below.
  In case that condition is true these commands are executed again from the beginning.
} while (condition for continuing)

(After the condition evaluates to false the program advances to any commands that follow the loop.)

Another example of a do loop

Study the animation below and predict the program’s behavior when prompted.

What does “while” mean exactly?

Beginners are occasionally confused by how Scala and many other programming languages use the English word while. Consider the program above. Since “while” means “as long as”, one might easily think that as soon as number reaches 13 — which isn’t less than 10 — the loop exits and we proceed to the code that follows. However, the condition is only checked after each complete execution of the loop body, and so the program prints 13 twice.

For the same reason, our first program reports “The name is X characters long.” also after the user has entered the final, non-empty input.

The same while keyword also appears in another, slightly different construct:

The while Loop

The while loop is an alternative for the do loop. This type of loop uses only the while keyword, not do.

Here’s a do loop, and below it, a while loop. As you see, the two are very similar.

(Up here, you might have some commands that precede the loop and are executed just once.)

do {
  One or more commands that are executed at least once
  and whose execution is followed by checking the condition below.
  In case that condition is true these commands are executed again from the beginning.
} while (condition)

(After the condition evaluates to false the program advances to any commands that follow the loop.)
In a do loop (above) the while keyword and the condition appear at the bottom. In a while loop (below), they are at the top.
Correspondingly a do loop checks the condition after each iteration of the loop body. A while loop instead checks the condition before every iteration.
That causes the only real difference between the loops: a do loop’s body always executes at least once, before checking the condition for the first time. A while loop checks the condition right off the bat, and if it evaluates to false, the loop body is not executed at all.
(Up here, you might have some commands that precede the loop and are executed just once.)

while (condition) {
  One or more commands that are executed zero or more times,
  checking the condition above before each iteration.
  If the condition is true these commands are executed.
}

(After the condition evaluates to false the program advances to any commands that follow the loop.)

In many cases, there is barely any difference between a do loop and a while loop. For instance, the following while-based code produces precisely the same interaction as our earlier do-based program:

var name = ""
while (name.isEmpty) { // The name is initially empty so we end up running the body at least once anyway.
  name = readLine("Enter your name (at least one character, please): ")
  println("The name is " + name.length + (if (name.length != 1) " characters" else " character") + " long.")
}
println("OK. Your name is " + name + ".")

These two types of loop are so much alike that it is simple to rewrite any do loop as a while loop and vice versa. (Optional assignment: consider how to do that.) Whenever you’ve decided you want to use these loops, you can just pick one based on whether you intend the body to repeat once or more (in which case you might as well pick do) or zero or more times (hence while).

Practice on while and do

Consider what happens while the program below runs. Which values are stored in result at each step?

var result = "TRO"
while (result.length < 10) {
  result += "LO" * (result.length / 2)
}

In the field below, please list all the strings that are stored in result during the program run. Write each string on its own line, in order. Don’t forget the final value!

Compare these two programs:

def exampleA(limit: Int) = {
  var number = 1
  var square = 1
  do {
    println(square)
    number += 1
    square = number * number
  } while (square <= limit)
}
def exampleB(limit: Int) = {
  var number = 1
  var square = 1
  while (square <= limit) {
    println(square)
    number += 1
    square = number * number
  }
}

Which values of limit cause the two programs to generate different output?

Some Loop-Writing Advice

When you write a loop, take care. If you break even one of the “golden rules of looping” listed below, you’ll have a buggy program on your hands.

val word = "llama"
var index = 0
while (index < word.length) {
  println("Letter: " + word(index))
  index += 1
}
#1 Initialize: Set up the program’s initial state as appropriate before starting the loop. What this often means in practice is that you’ll need to initialize one or more vars.
#2 Terminate: Make sure your loop is controlled by an appropriate conditional expression. In this example, we cap the value of the index variable; once we reach the limit, it’s time to stop.
#3 Advance the Loop: Your loop needs to modify the program’s state so that the loop will eventually terminate. Here, we increment the index variable so that it eventually reaches the limit we’ve set.
#4 Actually Do Stuff: Of course, you’ll need to include one or more commands that cause whichever effect your loop is designed to bring about. This toy loop’s only purpose is to print out some letters.

What happens if you overlook one of these “rules” depends on the specific loop. A fairly typical outcome is a so-called infinite loop: the computer repeats the same commands “forever”, that is, until it runs out of resources or the program run is externally interrupted.

Consider what would happen if we had forgotten the line that increments index from the above program. Which of the following best describes that scenario?

How about this program?

val word = "llama"
var index = 0
var result = ""
while (index < word.length) {
  result += word(index).toString * (result.length + 1)
}
println(result)

When does an “infinite” loop stop? How can one stop it?

The alternatives are more or less the same as those in a boxing match:

  • Concession: The user can interrupt the program. How that is done depends on the environment where the program is running. In IntelliJ, you can click the red Stop button. Many command-line environments use the keyboard shortcut Ctrl+C.
  • Technical knockout: If the program keeps consuming more and more memory, it will eventually crash with an error once that resource is depleted.
  • Disqualification: If you submit such a program in A+, the system will interrupt it after a while by externally terminating the process that executes your program.
  • Knockout: Power off.

Of course, the main thing to do is to prevent such problems, not to react to them.

Programmer Jim was heading to the store to pick up groceries. As he was leaving the house his wife said: “While you are there, buy some milk.” Jim never came back.

—thanks to the O1 student who shared this cautionary tale

A Couple of Voluntary Exercises

Loop-writing practice

In Chapter 7.1, we wrote this little program:

def report(input: String) = "The input is " + input.length + " characters long."
def inputs = LazyList.continually( readLine("Enter some text: ") )
inputs.takeWhile( _ != "please" ).map(report).foreach(println)

Rewrite the program to use while or do loop instead of LazyList. The program’s behavior should be the same as before. Write your code in Task1.scala in the DoWhile module.

A+ presents the exercise submission form here.

More practice

In Task2.scala, write a program that works in the text console as illustrated in these example runs:

I will compute the squares of positive integers and discard other numbers.
To stop, just hit Enter.
Please enter the first number: 10
Its square is: 100
Another number: 0
Another number: -1
Another number: 20
Its square is: 400
Another number: 30
Its square is: 900
Another number: 0
Another number: 40
Its square is: 1600
Another number:
Done.
Number of discarded inputs: 3
I will compute the squares of positive integers and discard other numbers.
To stop, just hit Enter.
Please enter the first number: 0
Another number: 0
Another number: 0
Another number: 0
Another number:
Done.
Number of discarded inputs: 4

Even the first input may be empty:

I will compute the squares of positive integers and discard other numbers.
To stop, just hit Enter.
Please enter the first number:
Done.
Number of discarded inputs: 0

A+ presents the exercise submission form here.

do and while vs. Higher-Level Tools

Comparing solutions

These two snippets do essentially the same thing: multiply some integers by two and find the first one that fulfills a particular condition:

val result = LazyList.from(0).map( _ * 2  ).dropWhile( _ <= 20 ).head
val result = {
  var number = 0
  var doubled = 0
  while (doubled <= 20) {
    number += 1
    doubled = number * 2
  }
  doubled
}
In the LazyList solution, the function we pass to dropWhile has the same purpose as the conditional expression in the while loop.

Here’s a toy function that we’ll need for our next example:

def examine(number: Int): Boolean = {
  println("I’m examining the number " + number + ". Is it over 90?")
  number > 90
}

The next two snippets generate some random numbers until they happen upon a sufficiently large random number:

LazyList.continually( Random.nextInt(100) ).map(examine).find( _ == true )
var isBig = false
do {
  isBig = examine(Random.nextInt(100))
} while (!isBig)
In this solution, the find call serves the same purpose as the loop’s conditional: it governs the number of repetitions.
As generate random numbers in the Int list, we'll call examine on each number. This yields a lazy-list of Booleans.
We do this until examine returns true on an element and find therefore stops examining the lazy-list further. Once that happens, no more random numbers or Boolean values are generated.

We could have alternatively used some other method that forces the lazy-list to generate elements until true. Calling contains(true) also works, for instance.

If we were to remove the final method call .find( _ == true ) from the above program, how would the program behave?

Which tools should I choose?

It should be clear by now that you can use do and while loops for the same purposes as the higher-order methods that you’ve seen earlier. Often that isn’t the best idea, but sometimes it does pay off.

By choosing to use the higher-order methods on collections, you emphasize the data that your program operates on and the operations that it performs on that data. When you use these methods, you leave the details of step-by-step execution for library methods to deal with and focus on expressing the program’s purpose. Which is nice.

By choosing to use do and while, you instead emphasize the step-by-step execution of commands. These looping commands bring you a little bit closer to the low-level sequential execution that takes place within the computer system as it runs your programs. When you use these loops, you assume direct control of the program’s flow of execution and specify the sequential steps of your algorithm in detail.

For many purposes, do and while loops are unnecessarily detailed. Generally, if you opt for higher-order methods instead, you’ll have less work to do, you’ll write code that is more readable, and your program is less likely to contain errors. That being said, there are reasons to use the loops sometimes. For instance:

  • Perhaps you’re implementing an algorithm that relies on modifying the program’s state step by step. Due to the nature of the algorithm, it can be natural to write it down as a sequence of consecutive steps. For instance, some programs that engage the user in dialogue in the text console arguably fall in this category.

Or:

  • Perhaps you’re working on an application that needs to be highly efficient (i.e., it needs to run fast). Perhaps you’ve studied your code carefully and identified one or more subprograms that require optimization. To that end, you may wish to painstakingly detail the exact operations in those subprograms rather than leaving those minutiae to library methods.

Conditional loops such as do and while are available in many programming languages and they are used widely. As you continue studying programming, you will keep running into these constructs. In fact, they are probably used too much by many programmers. Sometimes, people use them because the programming language affords them no alternatives; sometimes, people use them because of the surrounding programming culture or because of historical inertia; sometimes, people use them because they are unaware of better alternatives.

You should know these loops. But don’t think of them as the primary or only means to implement repetition in a program. Higher-order methods, for instance, are convenient, elegant, and efficient enough for most purposes, and there are other alternatives too.

Loops as first-class citizens

Chapter 6.1 mentioned the expression “first-class functions”, which refers to the notion that a programming language treats functions like it treats other values: functions can be assigned to variables, passed as parameters, returned by functions, etc.; they are first-class citizens of the language,

Chapter 7.1 brought up lazy-lists, which enabled us to create collections of indeterminate length and repeat operations until a given condition was fulfilled. That is, we could use lazy-lists for the same sorts of things that we’ve used loops for in this chapter. On the other hand, a lazy-list — unlike a loop — can be treated as data: you can invoke methods such as map on a list to produce a different list, or you can call find, contains, or exists to iterate over a part of a list. This is why some people call lazy-lists “first-class loops”.

What about for loops?

The code of a for loop defines the loop’s “shape” simply and implicitly: after each iteration of the loop body, the loop advances towards termination by plucking the next element from the collection. The implicit condition for continuing the loop is: “as long as there are more elements to process”.

for loops thus operate on a higher level of abstraction than the more explicit while and do loops. Scala’s for loop is a flavor of syntactic sugar, a different notation for a higher-order method call.

Robots That Change Location

You’ve done the first four parts of the Robots assignment. Now’s the time for the remaining five.

Robots, Part 5 of 9: Nosebot

Implement a type of mobile robots as class Nosebot:

  1. Define the constructor parameters and the rest of the class header.

  2. Write the simple mayMoveTowards method. (Remember to override.)

  3. Two movement methods are missing: moveBody and attemptMove. Start with the latter and use it as you implement moveBody.

    A lazy-list or a loop?

    You could implement moveBody with a loop. Alternatively, you could use a lazy-list. Can you come up with both solutions? (You don’t strictly need to, but do try.)

    A hint for the lazy-list solution: you can, for example, use a combination of map and find similar to the one in the random-numbers example above.

  4. Try creating some nosebots in the app’s GUI. Notice how the design of the program made it very easy to add a new type of robot in the simulator. Basically, the only thing you needed to do was implement the algorithm that nosebots use for moving.

Submit your solution before moving on to the next part.

A+ presents the exercise submission form here.

Robots, Part 6 of 9: collisions

Spinbots and Nosebots never collide with anything during their own turn but other sorts of robots, such as the ones you’re about to implement in Part 8, may. Before that, though, you’ll need to set up a few things so that Square’s subtypes support collisions.

  1. First, notice that Wall and Floor are defined in the same file as their supertype Square: Square.scala. They could have been in separate files, too, but since there is little code and the classes are closely associated with each other, why not?
  2. The Wall singleton’s addRobot method doesn’t do anything yet, apart from returning a value that indicates that the arriving robot did not fit into the same square with the wall. Edit this method so that it breaks any robot attempting to enter a wall square.
  3. Also edit the addRobot method in class Floor so that it meets the specification. The method should attend to collisions between robots.

Robots, Part 7 of 9: Staggerbot

Implement Staggerbot.

You’ll need a random-number generator (Chapter 3.6) and you’ll need to use it exactly as specified in the Scaladocs. Create a single generator per Staggerbot object, and pick a random number when — and only when — the bot needs another a random direction. (If your implementation deviates from the Scaladoc, your code will produce random numbers that differ from what A+ expects, and you won’t score points.)

You’re allowed to write private methods, and we recommend that you do. For instance, you could write a separate method for picking a random direction.

A+ presents the exercise submission form here.

You may find it helpful to add private methods during the following steps, too.

Robots, Part 8 of 9: Lovebot

Implement Lovebot.

A+ presents the exercise submission form here.

Robots, Part 9 of 9: Psychobot

Implement Psychobot.

You again have a choice between a loop and/or methods on lazy-lists. Can you implement the class in different ways?

You may find the methods in GridPos useful; check the Scaladocs.

A+ presents the exercise submission form here.

Chapter Summary

  • Many programming languages have constructs known as do and while loops; many, many programs contain such loops. These loops repeat a command or sequence of commands as long as a specific condition continues to be met.
    • The conditional expression is checked at the beginning (while) or end (do) of each iteration of the loop body.
    • When writing these loops, you must pay particular attention to initializing the loop, setting an appropriate condition for continuing, and advancing the program’s state so that the loop will eventually terminate.
  • for loops and higher-order methods are often efficient enough and quite a bit more convenient than do or while loops.
  • Links to the glossary: loop, do loop, while loop, iteration; lazy-list; level of abstraction.

Hey, What About break and continue? And return?

Some readers who have earlier exposure to other programming languages will be familiar with additional commands that sometimes appear in loops. If you’re one of those readers, you may be wondering if Scala also has those commands?

See below for answers, which are educational for complete beginners, too.

Loop-breaking commands

Various programming languages have commands for exiting a loop body either by terminating the loop altogether (break) or by jumping back to the top for a new iteration (continue).

When programming in Scala, such commands are seldom used. You can handle practically any scenario more elegantly by choosing a different approach. However, there is a break command of sorts available in the Scala API; see elsewhere for more information. Scala doesn’t provide a continue command since that command is so very seldom useful in a well-designed Scala program.

Returning a value from a loop

Various programming languages have a command that explicitly instructs the computer to immediately terminate the ongoing function call and return a value. This command is usually named return. Programmers may use it to interrupt a function call during a loop, terminating the loop at once along with the rest of the function.

Scala, too, has the return keyword, although it is rather rarely used. In most cases, it’s possible to find a better solution without it, although this is to an extent a matter of taste.

A simple example of return is shown below. This function loops through a vector and returns the first non-empty string:

def firstNonEmpty(vector: Vector[String]): Option[String] = {
  for (element <- vector) {
    if (element.length > 0) {
      return Some(element)  // Found it. Terminate the search without considering the remaining elements.
    }
  }
  return None
}
The command terminates both the loop and the entire function. It returns the value of the expression that follows the return keyword.
The last line in this function’s body is only ever executed in case the loop didn’t hit the early return.
In Scala, methods with return need an explicit return type annotation.

In this ebook, we seldom use return.

A bit more about return

Many Scala programmers look askance at return and avoid using it. This has to do with writing clearer code and the fact that return isn’t strictly needed for anything.

return commands may make it harder to follow the steps of a program (the program’s control flow). This is a risk especially if the code verbose, with many nested constructs. A reliance on return may also lead you to write such needlessly complicated code.

It’s generally considered a good programming practice to split a program in small, cleanly delimited subprograms. If you do that, there is usually little temptation to use return. Moreover, many languages, including Scala, have various elegant alternatives to the “loop and return” approach. These techniques include higher-order methods that process only a part of a collection (such as find, takeWhile, and exists) as well as recursion (Chapter 12.1); they make return largely redundant. Higher-order methods and recursion are particularly common in the functional programming style (Chapter 10.2) but they aren’t unique to it.

It’s not a mortal sin to put a return in your Scala program, especially if your code is otherwise nicely written.

A tangent on StarCraft and return

In his blog post Whose bug is this anyway?!?, game developer Patrick Wyatt is kind enough to recount a past blunder: he didn’t initially notice a bug in the StarCraft game even though it was “trivial”. It’s a nice read.

The error involved a return command that terminated a subprogram early in case a particular condition was met. However, another section of code many lines further down in the same subprogram was written without consideration of the fact that that code won’t run at all if an early return had already triggered.

A particularly noteworthy thing about this cautionary tale is that the conditional return was followed by a long sequence of other commands all within the same subprogram. All those commands were implicitly dependent on the return way above. return is dangerous when a program isn’t split into small enough functions.

A bit more about break

../_images/breaking_bad.png

Even more Scala programmers frown at break than at return.

For the most part, break has been criticized on similar grounds as return has. The additional complaint is: assuming you do wish to break out of a loop for some reason, and further assuming your code is appropriately divided in small functions, each with its own cleanly delimited purpose, then you don’t need break; you can just return your way out of the loop.

If you feel you need break, you should first consider if you might be able to place your loop in a function and use return to terminate it. (And then you can consider whether there might be a better alternative to return as well.)

More kinds of loops?

Some programming languages provide a different sort of for loop that has separate “slots” for initializer code, the loop-controlling condition, and the code that advances the loop towards termination. In a Java program, for instance, you can write:

// This is Java, not Scala.
for (index = 0; index < myString.length(); index += 1) {
  // Do the loop’s actual job here.
}

Scala doesn’t have that sort of for loop. On the other hand, Scala’s for expressions are capable of various other tricks, which we’ll discuss later.

Feedback

../_images/be_back.png

The robots will return in Chapter 11.2.

Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.

Credits

Thousands of students have given feedback that has contributed to this ebook’s design. Thank you!

The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.

The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.

The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy of using O1Library for simple graphical programming (such as Pic) is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.

The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It was created by Nikolai Denissov, Olli Kiljunen, and Nikolas Drosdek with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits appear at the ends of some chapters.

a drop of ink
Posting submission...