teaching machines

SENG 440: Lecture 2 – Conditionals, Functions, and Classes

Dear students,

Last time we introduced Kotlin as our tool for developing Android apps this semester. This time we continue that discussion but raise the complexity a couple of notches. We’ll look at conditional statements to choose between values and code, functions and classes to manage complexity, and lambdas to pass code around to other code.

Before we forget, here’s your TODO list for next time:

  • Read Control Flow, Functions, and Classes.
  • On a quarter sheet of paper to be turned in at the beginning of the next class, write a main function that computes something interesting to you and uses either a helper function or a lambda.

One more thing. Why do want to learn about mobile computing? What’s a goal you are aiming to reach with the opportunities that this class presents?

Here’s my goal: I want to be able to make apps that help me or my children grow, learn, and create in the “small” minutes of the day.

Conditional Statements

Conditional statements in Kotlin look very much like Java’s:

if (conditionA) {
  // A
} else if (conditionB) {
  // B
} else {
  // C
}

However, conditional statements are actually conditional expressions. They can yield a value and be embedded any place a value is expected: on the right-hand side of variable assignment or as an actual parameter in a function call. For example:

val direction = if (Random.nextBoolean()) {
  "L"
} else {
  "R"
}
println(direction)

For single-statement blocks, we can eliminate the braces and see that if statements are a lot like the ternary operators we’ve seen in other languages:

println(if (Random.nextBoolean()) "L" else "R")

The blocks can have multiple statements. The value that the if yields to its context comes from the last statement only. For example, we could add some counters to examine the fairness of random direction generator:

var nLefts = 0
var nRights = 0

for (value in 1..100) {
  val direction = if (Random.nextBoolean()) {
    ++nLefts
    "L"
  } else {
    ++nRights
    "R"
  }
  println(direction)
}

println(nLefts)
println(nRights)

When we get more than two conditions in our conditional statements, the syntax-to-logic ratio boils over to unhealthy levels. Kotlin’s solution is the when statement, which comes in two forms. The first behaves much like a switch statement that doesn’t need breaks to prevent fall-through. Let’s try this out to determine what animal represents this year in the Chinese zodiac:

val zodiac = when (year % 12) {
  0 -> "Monkey"
  1 -> "Rooster"
  2 -> "Dog"
  3 -> "Pig"
  else -> "Quokka"
}

For this particular example, an array would probably be a better choice. But we can do things with when that arrays can’t do so easily, like combine cases:

val nDays = when (month) {
  1, 3, 5, 7, 8, 10, 12, -> 31
  2 -> 28
  else -> 30
}

Or check ranges:

val generation = when (year) {
  in 1981..1996 -> "Millenial"
  in 1946..1964 -> "Baby Boomer"
  else -> "?"
}

The second form is a generalization of an if ladder, allowing arbitrary boolean expresssions for each case.

val quadrant = when {
  x > 0 && y > 0 -> "I"
  x < 0 && y > 0 -> "II"
  x < 0 && y < 0 -> "III"
  x > 0 && y < 0 -> "IV"
  else -> "None"
}

Functions

Functions in Kotlin have this general structure:

fun name(param1: Type, param2: Type, ...): ReturnType {
  // body
}

We’ll be writing a lot of functions in this class, so let’s ground ourselves in this structure with an exercise:

Write a function getRingArea that computes the area of the shaded region—which is called an annulus. What parameters do you need? What kind of return value? How do you compute it?

Function definitions can be shortened sometimes. If the function doesn’t return a value, its return type can be omitted. (We say that the function returns Unit, which is like void in other languages.) If a function has a body that is a single expression, we can replace braces with =, omit the return type, and omit the return keyword. For example:

fun roll2d6() = Random.nextInt(6) + Random.nextInt(6) + 2

Classes

When writing a new class in most object-oriented programming languages, we have three things to think about:

  • the behaviors that the object supports
  • the state or instance variables needed to represent the object
  • the behavior that describes how a new instance is born, also known as the constructor

We’ll find that Kotlin makes some of these tasks much simpler than their Java counterparts.

Basic Kotlin classes have this form:

class Model(param1: Type, ...) {
  val prop1: Float = ... // read-only property
  var prop2: String = ... // read-write property

  fun method(...) {
    ...
  }
}

Based on this structure, let’s model a Point together. Let’s start with just the state:

class Point(x: Float, y: Float) {
  var x: Float = x
  var y: Float = y
}

If this were Java, we would have written this:

class Point {
  private float x;
  private float y;

  public Point(float x, float y) {
    this.x = x;
    this.y = y;
  }
}

In Kotlin, we tend not write constructors. Instead, we do direct assignments at the time we declare instance variables. If the initialization requires some extra computation like a loop, we’ll use an init block, which we’ll discuss as the need arises.

The Java constructor feels really redundant. But the Kotlin version also has some awkwardness with name shadowing. But good news! JetBrains recognized how often we turn constructor parameters into instance variables, and they decided to coalesce the parameters, instance variable declarations, and initializations into an easier syntax. We just move the val or var onto the parameter declaration:

class Point(var x: Float, var y: Float) {
}

Let’s test it with a main. To construct an object, we simply invoke the class name as if it were a function. We do not use the keyword new:

fun main(args: Array<String>) {
  val p = Point(3, 4)
  println(p)
}

Does this work? No. Just as in Java, this implicitly calls the toString method of the superclass Any, which yields the class name and hashcode. Let’s override the toString method:

class Point(var x: Float, var y: Float) {
  override fun toString = "($x, $y)"
}

We will about inheritance and properties later.

Lambdas

While we lived under the hysteria of object-oriented programming, we believed that everything had to be an object. To respond to mouse clicks, you had to create a mouse listener object. To influence sorting criteria, you had to create a comparator object. Thankfully these days are behind us. Our modern languages allow us to pass functions around with first having to wrap them in an object. We don’t have to even name the function. Such function literals are called lambdas.

Functions have this syntactical structure:

fun name(param1: Type, param2: Type, ...): ReturnType {
  body
}

And lambdas have this syntactical structure:

{ param1: Type, param2: Type2 ->
  body
}

The return type is inferred from the last statement of body. And usually the parameter types can be inferred as well, so we are more likely to write this:

{ param1, param2 ->
  body
}

Suppose we want to print every element of an array. We could use Array.forEach and pass it a lambda like so:

val xs = intArrayOf(1, 2, 3, 4, 5, 6, 7)
xs.forEach({ x -> println(x) })

We can improve the syntax. If the lambda is the last parameter, we can move it outside the parentheses:

xs.forEach() { x -> println(x) }

And if the lambda is the only parameter, we can omit the parentheses entirely:

xs.forEach { x -> println(x) }

Now let’s solve some problems together using lambdas:

  • Round a list of integers to the nearest multiple of 10.
  • Filter out short strings from a paragraph, like the preamble of Treaty of Waitangi:
    HER MAJESTY VICTORIA Queen of the United Kingdom of Great Britain and Ireland regarding with Her Royal Favor the Native Chiefs and Tribes of New Zealand and anxious to protect their just Rights and Property and to secure to them the enjoyment of Peace and Good Order has deemed it necessary in consequence of the great number of Her Majesty’s Subjects who have already settled in New Zealand and the rapid extension of Emigration both from Europe and Australia which is still in progress to constitute and appoint a functionary properly authorised to treat with the Aborigines of New Zealand for the recognition of Her Majesty’s Sovereign authority over the whole or any part of those islands – Her Majesty therefore being desirous to establish a settled form of Civil Government with a view to avert the evil consequences which must result from the absence of the necessary Laws and Institutions alike to the native population and to Her subjects has been graciously pleased to empower and to authorise me William Hobson a Captain in Her Majesty’s Royal Navy Consul and Lieutenant-Governor of such parts of New Zealand as may be or hereafter shall be ceded to her Majesty to invite the confederated and independent Chiefs of New Zealand to concur in the following Articles and Conditions.
  • Autori on Open Kattis.

That’s enough for today. See you next time!

Sincerely,

P.S. It’s time for a haiku!

Old seems right to us Till New comes and gives us choice Then they both seem wrong

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *