Exercises in Kotlin: Part 2 - High level syntax and Variables

Exercises in Kotlin: Part 2 - High level syntax and Variables

Dhananjay Nene

This post first discusses the code that was written so far in the last post, Exercises in Kotlin: Part 1 - Getting Started. Subsequently it focuses on the basic syntax of variables, discusses a bit about packaging and also about Type Inference

A review of code so far

Let us take a look at the code we wrote so far in the last blog post first part of this series and glean what we can from the same.

helloworld.kt

package com.example.kotlin.learning

fun main(args: Array<String>) {
    println("Hello World!")
}
fun greetings() = "Hello World!"

Some of the things that can be noted from this program are follows

  • Similar package convention: Kotlin follows similar package naming conventions as Java.
  • Standalone functions: Yes, Kotlin has them. That means there is no requirement to have a class just in order to wrap functions, that frankly have no reasons to be part of a class.
  • Keyword fun: Functions are declared using the keyword fun.
  • Variable declaration order: When declaring variables (or arguments to a function as in this case of the arguments to main), the order of declaration is reversed, with the type specification following the variable/argument name and a colon used to separate the two.
  • Array is a generic type: Arrays are not represented by [] but a generic type Array<T> instead where T is the type whose array is being represented. Thus at a syntactic level there is no necessity for using [] for arrays.
  • There is a helper function called println() similar to System.out.println()
  • Semicolon as a statement terminator is optional: A semi-colon does not seem to be mandatory
  • Optional function return types: Return type of the function is not required. to be specified if the return type is a Unit. Which is why the function main does not require a return type declaration.
  • Alternative function representation: As the function declaration for greetings() shows, function bodies can be substituted by expressions if the separator = is used between the function declaration and the expression. When the function body is specified as an expression, the compiler will attempt to infer the return type and in most cases no return type declaration will be required.

testgreet.kt

package com.example.kotlin.learning

import org.junit.Test
import org.junit.Assert.assertEquals

class TestGreet() {
    @Test
    fun testGreetings() {
        assertEquals("Greeting should be 'Hello World!'",
                "Hello World!", greetings())
    }
}

From this we can glean the following

  • Class declaration: A Class declaration is similar to java in the sense it starts with the keyword class followed by the class name. The class body is also wrapped within { and }.
  • Constructor: There is no explicit constructor that seems to be required. That is not true. The parenthesis after the class name, in this case with empty content, are in fact the argument list for the primary constructor. More details about that later.
  • Method declarations: Methods are also declared using the fun keyword.
  • Annotations: Annotations can also be used just like they are used in java (in this case the @Test annotation.

The above gives us some sort of understanding about Kotlin syntax, Let us look at a few more examples to flesh out some of the more commonly used details. This has a main function. So you can copy this code into a file and compile and run it.

Example 1 : Comments and variables

/**
* This is a comment
*/

/*
   So is
   this */

// As is this

val PI = 3.14159

fun circleArea(r: Double) = PI * r * r

fun main(args: Array<String>) {
    var radius: Double = 1.0
    val increment = 1
    // increment = increment + 1 <-- will not compile since increment is a val
    println("The radius of a circle with radius ${radius} is ${circleArea(radius)}")
    radius = radius + increment
    println("The radius of a circle with radius ${radius} is ${circleArea(radius)}")
}

The output of the above program is

The radius of a circle with radius 1.0 is 3.14159
The radius of a circle with radius 2.0 is 12.56636

Note the following:

Comments

  • Kotlin follows the same syntax for comments as Java does. Enough said.

Primitive types

  • Kotlin has primitive types. Though they are not named identical to the ones in Java. They are in fact the same names with the first character capitalised viz Boolean, Byte, Int, Long, Float, Double etc. (not sure if there is an exception to that).
  • There are member methods available for these primitives unlike Java. Thus you can call a toString() on an Int
  • Although they seem to be like objects, their storage space is the same as primitives in Java unless they are declared as nullable (more about that later) or used by generic classes in which case their storage is like boxed primitives.

Variables

  • As mentioned earlier, variables can be declared at the top level (eg PI)
  • Functions can also have local variables. eg. radius and increment
  • Variables are of two types. val ie. read-only variables and var ie. read-write variables. In Java all variables by default are read-write and those that are to be treated as read-only are marked using the keyword final. However in Kotlin it is required to use one of the two keywords val or var explicitly. The statement // increment = increment + 1 was commented out, because since increment is a val such a statement would not compile. radius on the other hand being a var can change its value, as is done at radius = radius + increment.
  • A predominantly functional style of programming will almost always use vals and rarely if any vars. The rationale for that is beyond the scope of this post.
  • A variable may or may not have an explicitly type declared. The compiler will attempt to infer it based on the value it is initialised to. However often that may not be feasible, and the programmer may be required to specify the type explicitly.

Parameters

  • Parameter declarations for functions are similar to a variable declaration. However they are not preceded by a val or a var. They are always vals. They always need an explicit type declaration.
  • A function may return no value whatsoever (represented using the type Unit). The return type of the functional is optional for functions having an expression body (as in case of circleArea) or functions that have a block body but have a Unit return type. In other situatons the compiler will not attempt to infer the return type, and the programmer is required to provide it explicitly.

String templates

  • String interpolation is supported by allowing expressions to be embedded in strings using the ${expr} syntax as in the arguments to println(). This feature is called string templates in Kotlinspeak

(Optional) Some more advanced aspects about variables

There are some nuanced aspects of variables that you might use only occasionally. These are discussed here. You could chose to skip this section and instead move on to the next post since these might entangle you into learning and thinking about infrequently leveraged capabilities.

Consider the following code, which is an extended version of the code we saw earlier. It is kind of a little odd, but has been deliberately constructed to talk about the advanced features.

const val PI = 3.14159

fun circleArea(r: Double) = PI * r * r

var defRadius = 5.0

var defaultRadius: Double
    get() = defRadius
    set(value) { defRadius = value }

val area: Double
    get() = circleArea(defaultRadius)

fun main(args: Array<String>) {
    var radius = 1.0
    val increment = 1
    println("The radius of a circle with radius ${radius} is ${circleArea(radius)}")
    radius = radius + increment
    println("The radius of a circle with radius ${radius} is ${circleArea(radius)}")
    println("The radius of a circle with radius 5.0 is ${area}")
    defaultRadius = 6.0
    println("The radius of a circle with radius 6.0 is ${area}")
}

The output of the code above is as follows

The radius of a circle with radius 1.0 is 3.14159
The radius of a circle with radius 2.0 is 12.56636
The radius of a circle with radius 5.0 is 78.53975
The radius of a circle with radius 6.0 is 113.09723999999999

Things to be noted.

  • getters: Variables can have getter functions as shown for the variable area above. Frankly, not sure why they would be used for top level variables. Though I could imagine them to be used to work around variable visibility constraints by having a private var being accessible as a read-only variable function from another file by using a getter function.
  • setters: We can also similarly use setters as in the case of defaultRadius. Again this is a bit of a contrived example, since one could have just as easily directly used defRadius. However do note, getters and setters to tend to be more useful when used with class member variables, a topic we will get to later.
  • const variables: Variables can be prefixed with a const as in case of PI above. However this capability is restricted to top level variables only. In addition such variables have to be initialised to a String or a primitive type. and may not have a custom getter. In turn such consts can be used as parameters to annotations.
  • package namespaces and visibility: Just like top level functions, you can also have top level variables/constants being declared. Also note that all the standalone functions get declared in the namespace of the package. So if you have multiple files with the same package and also same function (or top level variable) names, that will be treated as an error since one of them will end up attempting to redeclare the other. On the other hand if you declare a top level variable as private, it is not private to that package namespace, but is instead treated as private within a file

We shall be reviewing functions in the next post Exercises in Kotlin: Part 3 - Functions