Wednesday, October 30, 2019

Intro to Kotlin, Part 2 Outline

Let's start out with showing how classes work in Kotlin. Here's a basic User class:
class User(val username: String)

fun main() {
  val user = User("bob")
  println(user.username)
}
  • In one line we've declared a class with a property on that class
  • Note that public is the default visibility
  • Also note no new keyword when creating an instance
  • Since val was used to define the username, you can only get the value, and not change it
  • If you want to change it (e.g. have a setter), then use var
class User(var username: String)

fun main() {
  val user = User("bob")
  user.username = "robert"
  println(user.username)
}
  • Also, you can set default values for class properties
class User(var username: String = "unknown")

fun main() {
  val user = User()
  println(user.username)
}
  • You can also define multiple classes in the same file
class User(val username: String)
class Comment(val message: String, val author: User)

fun main() {
  val user = User("bob")
  val comment = Comment("Hi there!", user)
}
  • Classes like these that only contain data are called value objects, and are frequently known as pojos in Java
  • Typically with these kinds of classes, you should implement the toString, equals, and hashCode methods
  • In Kotlin, these methods can be auto implemented for you by declaring the class a data class
class User(val username: String)
class Comment(val message: String, val author: User)

fun main() {
  val user1 = User("bob")
  val comment1 = Comment("Hi there!", user1)

  val user2 = User("bob")
  val comment2 = Comment("Hi there!", user2)

  println(comment1)
  println(comment2)
  println(comment1 == comment2)
  println(comment1.hashCode())
  println(comment2.hashCode())
}
compared to
data class User(val username: String)
data class Comment(val message: String, val author: User)

fun main() {
  val user1 = User("bob")
  val comment1 = Comment("Hi there!", user1)

  val user2 = User("bob")
  val comment2 = Comment("Hi there!", user2)

  println(comment1)
  println(comment2)
  println(comment1 == comment2)
  println(comment1.hashCode())
  println(comment2.hashCode())
}
Let's look at something a little more complicated:
class User(val username: String)
class Comment(initialMessage: String, val author: User) {

  private val msgHist = mutableListOf(initialMessage)

  init {
    println("Logging initial message: $initialMessage")
  }

  var message: String
    get() {
      println("Message retrieved")
      return msgHist.last()
    }
    set(value: String) {
      println("Message edited: $value")
      msgHist.add(value)
    }

  fun compareHistory(startIndex: Int, endIndex: Int): List<String> {
    return listOf(msgHist[startIndex], msgHist[endIndex])
  }
}

fun main() {
  val comment = Comment("hi thre!", User("bob"))
  comment.message = "hi there!"
  comment.message = "Hi there!"
  println(comment.message)
  println(comment.compareHistory(0, 2))
}
  • We didn't want initialMessage to be a property, so ommitted the val/var, making it just a parameter
  • Parameters can be used in the init block or to initialize properties
  • Properties can have custom accessors
  • Here's an example of a function in a class
  • Also note that if you need more than the primary constructor, you can create secondary constructors like this:
class User(val username: String)
class Comment(messageHistory: List<String>, val author: User) {

  private val msgHist = messageHistory.toMutableList()

  init {
    println("Logging initial messages: $messageHistory")
  }

  constructor(initialMessage: String, author: User): this(listOf(initialMessage), author) {
    println("Secondary constructor used")
  }

  var message: String
    get() {
      println("Message retrieved")
      return msgHist.last()
    }
    set(value: String) {
      println("Message edited: $value")
      msgHist.add(value)
    }

  fun compareHistory(startIndex: Int, endIndex: Int): List<String> {
    return listOf(msgHist[startIndex], msgHist[endIndex])
  }
}

fun main() {
  val comment = Comment("hi thre!", User("bob"))
  comment.message = "hi there!"
  comment.message = "Hi there!"
  println(comment.message)
  println(comment.compareHistory(0, 2))
}
  • You are also not required to have a primary constructor
class User(val username: String)
class Comment {

  private val msgHist: MutableList<String>
  val author: User

  init {
    println("Logging init block usage")
  }

  constructor(initialMessage: String, author: User) {
    println("Logging initial message: $initialMessage")
    msgHist = mutableListOf(initialMessage)
    this.author = author
  }

  constructor(messageHistory: List<String>, author: User) {
    println("Logging initial messages: $messageHistory")
    msgHist = messageHistory.toMutableList()
    this.author = author
  }

  var message: String
    get() {
      println("Message retrieved")
      return msgHist.last()
    }
    set(value: String) {
      println("Message edited: $value")
      msgHist.add(value)
    }

  fun compareHistory(startIndex: Int, endIndex: Int): List<String> {
    return listOf(msgHist[startIndex], msgHist[endIndex])
  }
}

fun main() {
  val comment = Comment("hi thre!", User("bob"))
  comment.message = "hi there!"
  comment.message = "Hi there!"
  println(comment.message)
  println(comment.compareHistory(0, 2))
}
  • Also, you can have different visibilities for the get and set accessors
class User(val username: String)
class Comment {

  private val msgHist: MutableList<String>
  var author: User
    private set

  init {
    println("Logging init block usage")
  }

  constructor(initialMessage: String, author: User) {
    println("Logging initial message: $initialMessage")
    msgHist = mutableListOf(initialMessage)
    this.author = author
  }

  constructor(messageHistory: List<String>, author: User) {
    println("Logging initial messages: $messageHistory")
    msgHist = messageHistory.toMutableList()
    this.author = author
  }

  var message: String
    get() {
      println("Message retrieved")
      return msgHist.last()
    }
    set(value: String) {
      println("Message edited: $value")
      msgHist.add(value)
    }

  fun compareHistory(startIndex: Int, endIndex: Int): List<String> {
    return listOf(msgHist[startIndex], msgHist[endIndex])
  }

  fun anonymize() {
    author = User("anonymous")
  }
}

fun main() {
  val comment = Comment("hi thre!", User("bob"))
  comment.message = "hi there!"
  comment.message = "Hi there!"
  println(comment.message)
  println(comment.compareHistory(0, 2))
}
  • And you can access the backing field for a property with the field keyword
data class User(val username: String)
class Comment {

  private val msgHist: MutableList<String>
  var author: User
    private set(value: User) {
      println("User changed from $field to $value")
      field = value
    }

  init {
    println("Logging init block usage")
  }

  constructor(initialMessage: String, author: User) {
    println("Logging initial message: $initialMessage")
    msgHist = mutableListOf(initialMessage)
    this.author = author
  }

  constructor(messageHistory: List<String>, author: User) {
    println("Logging initial messages: $messageHistory")
    msgHist = messageHistory.toMutableList()
    this.author = author
  }

  var message: String
    get() {
      println("Message retrieved")
      return msgHist.last()
    }
    set(value: String) {
      println("Message edited: $value")
      msgHist.add(value)
    }

  fun compareHistory(startIndex: Int, endIndex: Int): List<String> {
    return listOf(msgHist[startIndex], msgHist[endIndex])
  }

  fun anonymize() {
    author = User("anonymous")
  }
}

fun main() {
  val comment = Comment("hi thre!", User("bob"))
  comment.message = "hi there!"
  comment.message = "Hi there!"
  println(comment.message)
  println(comment.compareHistory(0, 2))
  comment.anonymize()
}
  • You can also create infix functions
class Thread() {
  val comments = mutableListOf<Comment>()
}
data class User(val username: String)
class Comment {

  private val msgHist: MutableList<String>
  var author: User
    private set(value: User) {
      println("User changed from $field to $value")
      field = value
    }

  init {
    println("Logging init block usage")
  }

  constructor(initialMessage: String, author: User) {
    println("Logging initial message: $initialMessage")
    msgHist = mutableListOf(initialMessage)
    this.author = author
  }

  constructor(messageHistory: List<String>, author: User) {
    println("Logging initial messages: $messageHistory")
    msgHist = messageHistory.toMutableList()
    this.author = author
  }

  var message: String
    get() {
      println("Message retrieved")
      return msgHist.last()
    }
    set(value: String) {
      println("Message edited: $value")
      msgHist.add(value)
    }

  fun compareHistory(startIndex: Int, endIndex: Int): List<String> {
    return listOf(msgHist[startIndex], msgHist[endIndex])
  }

  fun anonymize() {
    author = User("anonymous")
  }

  infix fun on(thread: Thread) {
    thread.comments.add(this)
  }
}

fun main() {
  val thread = Thread()
  val comment = Comment("hi thre!", User("bob"))
  comment.message = "hi there!"
  comment.message = "Hi there!"
  println(comment.message)
  println(comment.compareHistory(0, 2))
  comment.anonymize()
  comment on thread
}
Visibility
  • Let's touch on visibility real quick
Modifier Class member Top-level declaration
public (default) Visible everywhere Visible everywhere
internal Visible in a module Visible in a module
protected Visible in subclass --
private Visible in a class Visible in a file

Enums
  • Enums are declared via enum class, other than that they're the same as in Java
enum class Colors {
  RED,
  GREEN,
  BLUE
}
Interfaces
  • Interfaces work very similarly to interfaces in Java
interface Clickable {
  fun click()
}

class Button : Clickable {
  override fun click() = println("Click!")
}

fun main() {
  Button().click()
}
  • Note the colon used to specify interface implementation
  • Also note the required override keyword
Inheritance
  • Unlike Java, Kotlin classes are final by default. Use the open keyword to make a class inheritable
  • Also, functions must be marked as open to be overridable
open class Animal {
  open fun speak() {
    println("...")
  }

  fun sleep() {
    println("zzz")
  }
}

class Dog : Animal() {
  override fun speak() {
    println("Bark")
  }
}

class Cat : Animal() {
  override fun speak() {
    println("Meow")
  }
}

fun main() {
  val animal = Animal()
  animal.speak()
  animal.sleep()

  val dog = Dog()
  dog.speak()
  dog.sleep()

  val cat = Cat()
  cat.speak()
  cat.sleep()
}
  • You can also use the final keyword for a final override
open class Animal {
  open fun speak() {
    println("...")
  }

  fun sleep() {
    println("zzz")
  }
}

open class Dog : Animal() {
  final override fun speak() {
    println("Bark")
  }
}

class Bulldog : Dog() {
  //Can't override speak()

  fun growl() {
    println("Grr")
  }
}

class Cat : Animal() {
  override fun speak() {
    println("Meow")
  }
}

fun main() {
  val animal = Animal()
  animal.speak()
  animal.sleep()

  val dog = Dog()
  dog.speak()
  dog.sleep()

  val bulldog = Bulldog()
  bulldog.speak()
  bulldog.sleep()
  bulldog.growl()

  val cat = Cat()
  cat.speak()
  cat.sleep()
}
  • You can also create abstract classes, where the base class can't be instantiated
abstract class Animal {
  open fun speak() {
    println("...")
  }

  fun sleep() {
    println("zzz")
  }
}

class Dog : Animal() {
  override fun speak() {
    println("Bark")
  }
}

class Cat : Animal() {
  override fun speak() {
    println("Meow")
  }
}

fun main() {
  val animal = Animal() //This won't compile

  val dog = Dog()
  dog.speak()
  dog.sleep()

  val cat = Cat()
  cat.speak()
  cat.sleep()
}
  • You can also create sealed classes, where subclasses are required to be defined in the same file
  • This limits subclasses to a pre defined set of classes
sealed class Animal {
  open fun speak() {
    println("...")
  }

  fun sleep() {
    println("zzz")
  }
}

class Dog : Animal() {
  override fun speak() {
    println("Bark")
  }
}

class Cat : Animal() {
  override fun speak() {
    println("Meow")
  }
}

fun main() {
  val dog = Dog()
  dog.speak()
  dog.sleep()

  val cat = Cat()
  cat.speak()
  cat.sleep()
}
Class delegation and the by keyword
  • Most experts encourage composition over inheritance, but many languages don't make composition easy
  • Kotlin addresses this
interface Door {
  fun enter()
}

class WoodDoor : Door {
  override fun enter() {
    println("Entered by door")
  }
}

interface Window {
  fun openWindow()
}

class ClearWindow : Window {
  override fun openWindow() {
    println("Opened the window")
  }
}

class House : Door by WoodDoor(), Window by ClearWindow()

class Shed(door: Door) : Door by door

class WindowDisplay(window: Window = ClearWindow()) : Window by window

fun main() {
  val house = House()
  house.enter()
  house.openWindow()

  val shed = Shed(WoodDoor())
  shed.enter()

  val windowDisplay = WindowDisplay()
  windowDisplay.openWindow()
}
Extension functions and properties
  • You can, in essence, extend classes that you don't have control over through extension functions
fun main() {
  val helloWorld = "Hello World"
  println(helloWorld.removeVowels())
  println(helloWorld.firstVowel)
}

val vowels = listOf('a', 'e', 'i', 'o', 'u')

fun String.removeVowels() =
  this.filter { it.toLowerCase() !in vowels }

val String.firstVowel
  get() = filter { it.toLowerCase() in vowels }.first()
  • You can also use this to give classes certain functionality only in the right circumstances
fun main() {
  val helloWorld = "Hello World"
  println(helloWorld.removeVowels())
  println(helloWorld.firstVowel)  //This won't work
  MySpecialClass(helloWorld) //Inside the class it will work
}

val vowels = listOf('a', 'e', 'i', 'o', 'u')

fun String.removeVowels() =
  this.filter { it.toLowerCase() !in vowels }

class MySpecialClass(str:String) {
    
    init {
      println(str.firstVowel)
    }
    
    private val String.firstVowel
      get() = filter { it.toLowerCase() in vowels }.first()
}
Lambdas
fun main() {
  val incrementer = { a:Int -> a + 1 }
  println(incrementer(1))
}
  • You define a lambda by surrounding it with curly braces
  • You define the parameters on the left side of the arrow
  • Lamdas can be multiline, and the result of the last statement is returned
fun main() {
  val incrementer = { a:Int ->
    println("Input: $a")
    a + 1
  }
  println(incrementer(1))
}
  • The Kotlin standard library has lots of predefined functions that use lambdas, such as map and filter
fun main() {
  val incrementer = { a:Int ->
    println("Input: $a")
    a + 1
  }
  println(listOf(1, 2, 3).map(incrementer))
}
  • You can of course pass the lambda into the method directly
fun main() {
  println(listOf(1, 2, 3).map({ a:Int ->
    println("Input: $a")
    a + 1
  }))
}
  • And since it is being passed in, you can infer the parameter type
fun main() {
  println(listOf(1, 2, 3).map({ a ->
    println("Input: $a")
    a + 1
  }))
}
  • And when there's only one parameter, Kotlin will provide a default parameter name that you can use, called it
fun main() {
  println(listOf(1, 2, 3).map({
    println("Input: $it")
    it + 1
  }))
}
  • When a lambda is the last parameter for a method, the lambda can be moved outside of the parentheses
fun main() {
  println(listOf(1, 2, 3).map() {
    println("Input: $it")
    it + 1
  })
}
  • And when a lambda is the only parameter for a method, you can remove the parentheses entirely
fun main() {
  println(listOf(1, 2, 3).map {
    println("Input: $it")
    it + 1
  })
}
  • Why does Kotlin support this? Because it allows you to create constructs that look like they're part of the language. For example:
fun main() {
  ifnot (false) {
    println("here")
  }
}

fun ifnot(conditional:Boolean, body:() -> Unit) {
  if (!conditional) body()
}
  • And here we also see how to create functions that take lambdas as parameters
  • body is a lambda that has no parameters, so it uses ()
  • It also returns nothing, so it specifies the return type Unit
  • To specify parameters and return types, we can do something like this:
fun main() {
  val result = 1.singleMap { it > 0 }
  println(result)
}

fun <T, R> T.singleMap(mapper:(T) -> R) =
  mapper(this)
  • Note that parenthesis around the parameter types is always required
  • We also get a small taste of generics here
  • We can also return a lambda from a function, like this:
fun main() {
  val add1 = curry(::add, 1)
  println(add1(2))
}

fun add(a:Int, b:Int):Int = a + b

fun <T, U, R> curry(function:(T, U) -> R, t:T): (U) -> R {
  return { u ->
    function(t, u)
  }
}
  • Also note that we can pass regularly defined functions as lambdas by prepending them with ::
  • You can also inline functions, and the compiler will inline the code with the lambda at compile time
fun main() {
  ifnot (false) {
    println("In ifnot")
  }
}

inline fun ifnot(conditional:Boolean, body:() -> Unit) {
  if (!conditional) body()
}
  • This allows for some speedups, since lambda objects don't need to be created at runtime
  • Note that an inlined function cannot call and pass its lambda to another function unless that function is also inlined
  • It also allows you do to some extra things in your lambda, such as using a return statement
fun main() {
  ifnot (false) {
    println("In ifnot")
    return
    println("After return") //It won't reach this code
  }
  println("After ifnot") //It won't reach this code, either
}

inline fun ifnot(conditional:Boolean, body:() -> Unit) {
  if (!conditional) body()
}
  • Note that if you wanted to just return from the lambda, you can do so as follows:
fun main() {
  ifnot (false) {
    println("In ifnot")
    return@ifnot
    println("After return") //It won't reach this code
  }
  println("After ifnot") //But it will reach this code
}

inline fun ifnot(conditional:Boolean, body:() -> Unit) {
  if (!conditional) body()
}
  • You can also specify a label, instead of using the method name:
fun main() {
  ifnot (false) marker@{
    println("In ifnot")
    return@marker
    println("After return") //It won't reach this code
  }
  println("After ifnot") //But it will reach this code
}

inline fun ifnot(conditional:Boolean, body:() -> Unit) {
  if (!conditional) body()
}
  • You can also create lambdas with receivers, which bind the this keyword to something specific in the lambda
  • You can think of them as extension functions as lambdas
  • Here's how the with statement in the standard library works (note that the standard library with statement has a little more to it)
fun main() {
  val result = with("Hello World") {
    substring(6)
  }
  println(result)
}

inline fun <T, R> with(receiver:T, block:T.() -> R): R {
  return receiver.block()
}