Exercises in Kotlin: Part 2 - High level syntax and Variables
Dhananjay NeneThis 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 keywordfun
. - 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 typeArray<T>
instead whereT
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 toSystem.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 anInt
- 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
andincrement
- Variables are of two types.
val
ie. read-only variables andvar
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 keywordfinal
. However in Kotlin it is required to use one of the two keywordsval
orvar
explicitly. The statement// increment = increment + 1
was commented out, because sinceincrement
is aval
such a statement would not compile.radius
on the other hand being avar
can change its value, as is done atradius = radius + increment
. - A predominantly functional style of programming will almost always use
val
s and rarely if anyvar
s. 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 avar
. They are alwaysval
s. 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 ofcircleArea
) or functions that have a block body but have aUnit
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 toprintln()
. 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 useddefRadius
. 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 aString
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