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 2.7: A Complete Scala Application

About This Page

Questions Answered: How do I write and store an entire Scala program? Where does a Scala application’s execution begin? How can I read input from the user’s keyboard? Now that I’ve created a conceptual model of a game world, how can I define a GUI that displays the world?

Topics: App objects. Keyboard input with readLine. The basics of graphical user interfaces with o1.View.

What Will I Do? Mostly program, guided by the text.

Rough Estimate of Workload:? Two hours.

Points Available: A75.

Related Modules: Ave (which we’ll create from scratch), Odds, IntroApps (new), FlappyBug.

../_images/person06.png

Introduction

You have created objects. You have called their methods in the REPL, an environment that is well suited for experimentation.

However, the commands that you’ve entered in the REPL don’t form a unified whole that is stored for later use, that can be lauched again at will, that can be readily edited, and that is easily copied for someone else to run. To achieve these things, you’ll need to store your application in a file.

Let’s take a stab at doing just that.

A Traditional Program

../_images/ave_munde.png

Already the ancient Romans started the study of each new programming language or technology by using it to create a so-called “Hello, World” program. Such a program simply displays a message and does nothing else.

Let’s create and store a program that contains a couple of Scala commands and that we can launch as may times as we like. These print commands will serve:

println("Ave, Munde!")
println("Well, ave at thee, too!")

Create a new IntelliJ module for this experiment:

  1. File → New → Module.
  2. In the dialog window that pops up, make sure that Scala is selected. Press Next.
  3. Write Ave as the module’s name. But don’t press Finish yet. Instead...
  4. ... open More Settings at the bottom of the dialog. Remove the text "src" from the Create source root text field. Leave the checkmark next to it, though.
    • (This step isn’t otherwise crucial, but doing this gives you a module whose structure matches that of O1’s other modules so that our A+ Courses plugin can submit it for auto-grading. If you skip this step, you get an src subfolder within the module that contains all the Scala files.)
  5. Hit Finish. The Ave module shows up in IntelliJ’s Project view.

Create a new Scala code file:

  1. Right-click Ave module and select New → File.
  2. You’re prompted for a file name. Enter Ave.scala
  3. Hit Finish. The file shows up within the module and also opens up in IntelliJ’s editor. It is empty.

Write the program:

  1. Enter the two println commands in the empty file you just created. Compile the module (F10).
  2. Witness: Compilation fails. The Messages tab elaborates: Error: expected class or object definition. What’s wrong?

The underlying problem is that we haven’t set up our application as a proper object-oriented program. When we write a Scala application, we need to define a special “app object” that provides a starting point for program execution. This object can then activate (call) other program components as needed.

The GoodStuff module has an app object named GoodStuff, which you’ve activated before to run the application. The Pong module contains an app object named PongApp. Our Ave application, however, lacks such an object.

Writing an App Object

Here’s a template for an app object:

object MyOwnApplication extends App {
  // This is an app object.
  // The commands you enter here will be executed when the app runs.
}
You can read Scala’s extends keyword as “is a kind of”.
We state that this singleton object represents a kind of application. Adding this bit to our object definition gives our object all the capabilities provided by Scala’s App data type. The object now represents our application as a whole and enables us to run the program.
It’s not necessary to write any methods on an app object: we can simply enter commands inside the body of the object. These commands will be executed, in order, when the app is launched.

At this stage of O1, feel free to think of extends App as simply a phrase that marks an app object. In later chapters (e.g., 7.2) it will turn out that the extends keyword has many other uses as well.

A Tiny But Complete Scala Program

A larger program may consist of dozens, hundreds, or even more classes and singleton objects. However, for our tiny greeting program, we need just a single app object where we place the print commands.

Edit Ave.scala to contain the following:

object Ave extends App {
  println("Ave, Munde!")
  println("Well, ave at thee, too!")
}

Now you can run the program by opening the menu for the Ave file in the Project tab and selecting Run 'Ave'. A still easier way is to click the Play icon that appeared in the margin next to the work object in the code. Try it.

(This is also how you started GoodStuff and Pong back in Chapter 1.2. These and some alternative ways to launch a Scala app in IntelliJ were presented there.)

Notice that the output appears in IntelliJ’s Run tab at the bottom. An output area like this is called a text console (tekstikonsoli) or simply a console.

You have now written a complete Scala application.

A+ presents the exercise submission form here.

Reading Keyboard Input

The Ave app produces the same output each time it’s run, and the user has no way to affect what the program does as it runs. In constrast, most meaningful applications take in some sort of input (syöte) from their users, either directly or indirectly. Here are some examples:

  1. The program has a graphical user interface. The user can click on it to indicate what they wish the program to do.
  2. The program operates on data that it loads from files stored on the computer’s hard drive.
  3. The program interacts with the user in the text console. It pauses to wait for the user to enter lines of text as input.

We’ll get started with graphical user interfaces later in this chapter. Working with files will be discussed in Chapter 11.3. But first, let’s explore the third form of user interaction.

A text-based app in the console

Let’s write a program that works in IntelliJ’s Run tab as shown below.

Halt! Who is it?
Pechkin the Postmaster
Ave, Pechkin the Postmaster!
The highlighted middle row is an input entered by the user. The computer has paused at this point to wait for the input, continuing only when the user has pressed Enter.
The name that the user entered during this program run appears in the second output.

The readLine function

The library function readLine receives, or “reads”, a single line of user input. Calling readLine suspends the program until the input has been received. The function returns the input as a String.

The interactive program described above can be implemented with readLine:

import scala.io.StdIn._

object GreetingApp extends App {

  println("Halt! Who is it?")
  val name = readLine()
  println("Ave, " + name + "!")

}
The easiest way to work with readLine is to import it first. StdIn is short for “standard input”, which here essentially means input that the user enters through a text console.

Type in (or copy–paste) the above program in IntelliJ. You can either edit the Ave object or create a new file for this second app.

Run the program. See what it prints in the console and answer the program’s request for input. (N.B. You will need to click the console before you can use the keyboard to enter input.)

It’s also good to know that you can pass a string parameter to readLine. If you do, the function first prints the string as a prompt and then reads the user’s input off the same line. Here’s an example:

val name = readLine("Halt! Who is it? ")
println("Ave, " + name + "!")

This produces interactions like the one below.

Halt! Who is it? Pechkin the Postmaster
Ave, Pechkin the Postmaster!

Mini-assignment

Write a program that asks the user for two strings and surrounds the first string with the second as shown in these example runs:

Please enter a string: llama
And a surrounding string: !?
!?llama!?
Please enter a string: cad
And a surrounding string: abra
abracadabra

Write your code in an app object named Surround. Define the app object in a file named Surround.scala. Create the file in the Ave module next to Ave.scala .

A+ presents the exercise submission form here.

Assignment: Odds (Part 5 of 9)

Let’s write a small test program for the Odds module from Chapters 2.4 and 2.5. Our program will use keyboard input to create Odds objects, call the objects’ methods, and print a report of the return values.

Task description

Two of the files in module Odds are relevant to this assignment. Odds.scala you already know; it defines class Odds. Now take a look at OddsTest1.scala. It defines an app object, but the definition is incomplete and the app doesn’t actually use class Odds at all.

Flesh out the app object so that it produces an output that exactly matches the following example. (Of course, the user might well enter numbers other than the ones shown.)

Please enter the odds of an event as two integers on separate lines.
For instance, to enter the odds 5/1 (one in six chance of happening), write 5 and 1 on separate lines.
7
2
The odds you entered are:
In fractional format: 7/2
In decimal format: 4.5
Event probability: 0.2222222222222222
Reverse odds: 2/7
Odds of happening twice: 77/4
Please enter the size of a bet:
50.0
If successful, the bettor would claim 225.0
Please enter the odds of a second event as two integers on separate lines.
10
1
The odds of both events happening are: 97/2
The odds of one or both happening are: 70/29

Instructions and hints

  • You can start by running OddsTest1 as given. It reads in some input but doesn’t produce the correct output.
  • In OddsTest1, add the commands to create Odds objects and call their methods.
    • Do not edit class Odds or copy any of it into OddsTest1.scala. Use the class as it is.
    • It’s important that you sequence the commands right. Pay attention to where within OddsTest1 you create new Odds objects. You can create an instance only after you’ve read the required inputs; on the other hand, you need to create it before you can call any methods on it.
  • Since OddsTest1 and Odds are in the same package, you can instantiate Odds in OddsTest1 without importing anything.
  • You’ll notice that the given code calls functions named readInt and readDouble. These two work like readLine, above, differing from it only in that they interpret user inputs as numbers. For example, readInt returns a value of type Int, not a String.
  • Hint: you can use both to compute the odds of an event occurring twice. (E.g., to roll a six twice is to roll a six and to roll another six.)

A+ presents the exercise submission form here.

Optional assignment: eliminate redundant code with abstraction

OddsTest1 features two pieces of code that do essentially the same thing: they read in two numbers and use them as constructor parameters for a new Odds object. Avoid this unnecessary repetition with a different implementation:

  • In OddsTest1, add a method called requestOdds that takes no parameters. This effectful method reads in two integers, uses them to create an Odds instance, and returns a reference to the new object.
  • Call requestOdds (twice).

A+ presents the exercise submission form here.

An Application with a GUI

Recap: An application program operates on some problem domain. The programmer creates a model of that domain. A user interface presents the model to the end user in some form, usually as images and/or text. Many user interfaces also let the user interact with the model, affecting the model’s state. Many applications have a graphical user interface (GUI) that consists of various visual elements and is typically displayed in a separate window.

A toy example of a model

The domain of the FlappyBug game is the two-dimensional game world; in the previous chapter, we wrote the classes Bug, Game, and Obstacle, which together model this domain. Before we write a GUI for that model, let’s consider a simpler model and a GUI that displays it.

Our example model consists of just a single class, Block:

class Block(val size: Int, val location: Pos, val color: Color) {
  override def toString = this.color.toString + " block at " + this.location
}

Below is a usage example. It works if you have the above class loaded in the REPL; the class is provided in the IntroApps module.

val model = new Block(20, new Pos(300, 50), Gray)model: Block = Gray block at (300.0,50.0)

Now let’s write a GUI that displays a view of a single Block object set against a solid background.

Quick recap of custom methods

Remember the Person class from the end of Chapter 2.4? We created several instances of it, including a Superman person that was created with this command:

val superman = new Person("Clark") {
  def fly = "WOOSH!"
}

That object is an instance of the person class but also has a fly method. In just a moment, we’ll find a more concrete use for defining a method on a single instance.

Displaying a GUI window: o1.View

Fortunately, programmers don’t have to build a GUI for every application from scratch, pixel by pixel. Instead, they find a software library suitable for their purposes.

There are many GUI libraries. One well-known one is called Swing; we’ll use Swing in Chapter 12.3 to work with buttons, text fields, and the like. Right now, we’ll instead use O1’s own GUI library, which is particularly useful for building small apps whose GUI consists of geometric shapes. (This library is also compatible with Swing.)

Package o1 provides a class called View. As its name suggests, this class can be used to display a view to an object that models the app’s domain. Let’s use the following Block object as our model:

val model = new Block(20, new Pos(300, 50), Gray)model: Block = Gray block at (300.0,50.0)

Now to create the View object. This object represents a single GUI window that displays a single object of type Block.

val viewOfBlock = new View(model) {
  def makePic = {
    val background = rectangle(500, 500, Black)
    val blockPic = rectangle(model.size, model.size, model.color)
    background.place(blockPic, model.location)
  }
}viewOfBlock: o1.gui.mutable.ViewFrame[Block] = view of Block
The object is an instance of class View. As a constructor parameter, we pass in a reference to what the view will display.
In addition to being capable of everything that any View object is, our particular View instance has an effect-free method makePic that forms an image of the model. The image is of type Pic, and we form it using the Pic tools familiar from earlier chapters.
The view accesses the model’s (here: the block’s) attributes to generate an image.

Nothing graphical actually showed up when we entered that command in the REPL. This is because we didn’t yet start up our GUI. Every View object has an effectful method that starts it. The following command displays our primitive GUI. Try it.

viewOfBlock.start()

Any View object is capable of drawing itself onscreen and showing the image produced by its makePic method. So what we get is a window that contains the image of a block set against a black background. The window also has the customary controls for minimizing and closing it; that little bit of interactivity is automatically provided by View without us having to do anything about it.

An App with a View

Let’s combine what we’ve covered in this chapter to create an app object that fires up a GUI view.

object BlockApp extends App {

  val background = rectangle(500, 500, Black)

  val block = new Block(20, new Pos(300, 50), Gray)

  val viewOfBlock = new View(block, "An uninteractive test app") {
    def makePic = {
      val blockPic = rectangle(block.size, block.size, block.color)
      background.place(blockPic, block.location)
    }
  }

  viewOfBlock.start()

}
Within the app object...
... we create the model that we wish to present to the app’s user as well as the view that displays a visual representation of the model.
A small addition: we can give our app a title by passing an additional parameter to the View we construct.
Finally, we must remember to invoke start to make the GUI visible onscreen. After that, all the code within our app object has been executed but our application will keep running in its separate window until the user signals otherwise.

You can find this mini-app in the IntroApps module. It is ready to run.

That sure was a boring program. Thankfully, FlappyBug will be more interesting.

Side note: makePic as an abstract method

Question: what happens if I remove makePic from the above app?

Answer: you’ll receive a compile-time error message informing you that you can’t create a View object that doesn’t have a makePic method.

Explanation: the View class has been defined so that even though it doesn’t actually implement a makePic method, it requires such an implementation to exist on all objects of type View. In more technical terms, makePic is an abstract method in class View. More on abstract methods in Chapter 7.2.

An optional practice task

If the previous example seems unclear or the next official assignment feels too difficult, you can practice on this small additional assignment.

Let’s construct a View that displays the Earth and the Moon in space. As our model, we’ll use an instance of class AreaOfSpace and the two instances of class CelestialBody that are linked to it. Here is the implementation of those classes (repeated from Chapter 2.6’s optional section where the classes were introduced):

class CelestialBody(val name: String, val radius: Double, var location: Pos) {

  def diameter = this.radius * 2

  override def toString = this.name

}
class AreaInSpace(size: Int) {
  val width  = size * 2
  val height = size

  val earth = new CelestialBody("The Earth", 15.9, new Pos(10,  this.height / 2))
  val moon  = new CelestialBody("The Moon",   4.3, new Pos(971, this.height / 2))

  override def toString = s"${this.width}-by-${this.height} area in space"
}

That code is also available in Space.scala in module IntroApps . The related file SpaceProgram.scala contains starter code for a program that displays a this sort of simple model of space in a View:

object SpaceProgram extends App {

  val space = new AreaInSpace(500)

  val emptySpacePic = rectangle(space.width, space.height, Black)
  val earthPic = circle(space.earth.radius * 2, MediumBlue)
  val moonPic  = circle(space.moon.radius  * 2, Beige)

  // Replace the question marks below with code that works.
  val gui = new View(???, "A Very Simple View of Space") {
    def makePic = ???
  }

  ??? // Should launch the view that variable "gui" refers to.

}
We create a single object of type AreaInSpace. It will be our domain model.
We need a few component Pics that we’ll put together to generate a Pic of the entire model.
We define a variable that refers to an instance of class View. Let’s call this variable gui, for example.

Your task is to fill in the missing pieces:

1) the View’s first constructor parameter, which specifies the model that the View instance will depict.
2) an implementation for makePic that places the images of the Earth and the Moon in their correct positions against emptySpacePic. (In practice, the Earth will be visible at the left edge of the view, and the Moon near the right edge.)
3) A command to launch the View (displaying it onscreen).

Make the changes in module IntroApps and try running the program.

See below for the solution. You can also submit your answer for feedback.

How to replace the first ??? (the constructor parameter)

Show the solutionHide the solution

Our domain model is the instance of AreaInSpace that we created earlier. We have a variable space that refers to that object, so you can replace the first question marks with that variable name.

A hint for replacing the second ??? (makePic)

Show the hintHide the hint

We need to place two pictures onto emptySpacePic. Calling the place method a couple of times will do the trick.

You can use the space variable to access the celestial bodies and their correct positions (cf. Chapter 2.6).

Remember Chapter 2.3’s rotating-horse example? When you combine method calls on Pics, keep in mind that Pic objects are immutable and the second place operation should target the output of the first.

How to replace the second ??? (makePic)

Show the solutionHide the solution

Here are a couple of different ways to phrase a solution:

def makePic = emptySpacePic.place(earthPic, space.earth.location).place(moonPic, space.moon.location)
def makePic = {
  val earthAdded = emptySpacePic.place(earthPic, space.earth.location)
  earthAdded.place(moonPic, space.moon.location)
}

How to replace the last ??? (launching the view)

Show the solutionHide the solution

We have a variable named gui that refers to the view, and the relevant method is named start, so gui.start() works.

A+ presents the exercise submission form here.

Assignment: FlappyBug (Part 3 of 17: The Beginnings of a GUI)

../_images/module_flappybug.png

A diagram of the components in module FlappyBug. The model we’ve already discussed; now we’ll make the user interface.

Introduction

Here’s some starter code for an app object. You can also find this code in FlappyBugApp.scala.

import constants._

object FlappyBugApp extends App {

  val sky        = rectangle(ViewWidth, ViewHeight,  LightBlue)
  val ground     = rectangle(ViewWidth, GroundDepth, SandyBrown)
  val trunk      = rectangle(30, 250, SaddleBrown)
  val foliage    = circle(200, ForestGreen)
  val tree       = trunk.onto(foliage, TopCenter, Center)
  val rootedTree = tree.onto(ground, BottomCenter, new Pos(ViewWidth / 2, 30))
  val scenery    = sky.place(rootedTree, BottomLeft, BottomLeft)

  val bugPic = Pic("ladybug.png")


  def rockPic(obstacle: Obstacle) = circle(obstacle.radius * 2, Black)


  // INSERT YOUR OWN CODE BELOW.


}
The first bunch of commands just compose an scenic image that we intend to use as our game’s background. The variable scenery stores the end result. It isn’t necessary that you understand precisely how the background was constructed, but you can find an explanation in the optional material at the end of Chapter 2.5
Your task is to develop the given app object further. The comment shows where your code will go.
The variable bugPic stores an image that we’ll use to depict the bug in the GUI. The function rockPic forms a Pic of a given obstacle. You’ll need these components in the next part of the FlappyBug assignment but not just yet. For now, focus on displaying the background scenery in a View.

Assignment: FlappyBug (Part 4 of 17: Fleshing Out the View)

Improve the makePic method. It should construct and return an image of the game’s current state by combining the scenery, the bug’s image, and an image of an obstacle so that the bug and the obstacle have been placed against the scenery in their correct positions.

A few hints:

  • Use the images given as scenery and bugPic.
  • At least for now, let’s depict obstacles as “rocks” that are simply solid black circles. Calling rockPic on an obstacle with a radius of 5 gives you a pebble; a radius of 150 yields an almighty chunk of rock.
  • Given what you did in Chapter 2.6, the Game object should have two variables: obstacle and bug. You can use them to access the game’s bug and the obstacle. You’ll need to do so.
    • E.g., myGameObject.bug
  • You can access the positions of the game’s bug and obstacle via their pos attributes (Chapter 2.6).
    • E.g., myGameObject.bug.pos
  • Place the bug and the obstacle at their initial locations as indicated by their poses.
    • Use something in the vein of scenery.place(referenceToObstacle, positionOfObstacle).
    • Cf. BlockApp above (and the optional SpaceProgram).
    • You may want to think back to Chapter 2.3’s horse-rotating example to remember how to apply a sequence of method calls on a Pic.

Run the program. You should see both the bug and the obstacle at their initial positions. They don’t move. Yet.

A+ presents the exercise submission form here.

Recap and clarifications: the View class, start, and makePic

There’s nothing really new in this box. If you feel like you understood the above, you can skip this elaboration on what a View is and how it behaves.

The two main components of an application are its internal model and a user interface. The user interface makes the model visible and typically enables the human user to somehow interact with the model (Chapter 1.2).

View is a class whose instances (View objects) are graphical windows that serve as user interfaces. Each such View object is associated with another object, which serves as the application’s internal model and which the View window displays as graphics.

When you create a View object, you need to indicate which object will be the internal model and therefore displayed in the view. For instance, in the FlappyBug example above, we wrote new View(game, ...), making this a View that displays information about a game object.

Just creating a View object does not display the user-interface window. That happens only when you start the view by calling its start method. When you do, the window not only shows up but also starts responding to user actions in the window (which we’ll discuss in Week 3).

I didn’t understand how the makePic method just “ran itself”. I defined the method but never called it anywhere in my program.

Once you’ve started a View, the View draws itself onscreen using its own makePic method, which you’ve defined on it. It’s true that you don’t have to expressly invoke makePic yourself, since the View object has been programmed to take care of that on its own after it’s been started. The View window displays exactly the picture that makePic returns.

View and makePic will soon become more familiar as Week 3 continues right where Week 2 left off.

Optional activity: write prettier code

In the given FlappyBugApp, the background image is constructed with a sequence of commands that use multiple temporary variables (e.g., foliage, ground), each of which stores a partial image but has no purpose once the final scenery has been constructed. We can make our code a bit more elegant by structuring it differently. Try the following.

  1. Can you restructure that part of the code so that scenery isn’t a variable but a parameterless function that returns the scenic image? Like this:

    def scenery = {
      // place the code that constructs the image here
    }
    

    In this solution, temporaries such as foliage become local variables as you define them within scenery’s body.

  2. How about changing def scenery to val scenery in the modified solution? Does that work? Can you tell what difference this makes, if any?

Summary of Key Points

  • A Scala application is launched through an app object. You can write an app object by including extends App in the definition of a singleton object.
    • You may not need to write any methods on an app object. The commands in the object’s body are executed one by one when the application is launched.
    • In a larger application, the app object activates other program components.
  • The Scala library provides functions for reading keyboard input from the user through the text console.
  • A typical application’s program code features a domain model and a user interface that depicts and operates on the model.
  • There are various software libraries that help programmers build graphical user interfaces.
    • One GUI library is provided as part of package o1. A key component of this library is the View class, whose instances represent GUI windows.
  • Links to the glossary: app object; input, I/O; model, user interface, graphical user interface (GUI); text console.

Feedback

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...