The Programmers Guide To Kotlin - Iterators, Sequences & Ranges
Written by Mike James   
Monday, 29 January 2018
Article Index
The Programmers Guide To Kotlin - Iterators, Sequences & Ranges
Progressions

Collections are fundamental to the use of most object-oriented languages and Kotlin is no different. It takes the Java collections and pushes them further. In this extract from my recently published book, the focus is on iterators, sequences and ranges. 

Iterators

Collections of objects usually have an iterator to allow you to step through each object in turn. In fact, you can have an iterator that isn't associated with a collection of objects and simply generates the next object on demand.

An iterator has to have at least two functions – next which returns the next object, and hasNext which returns true if there is a next object.

For example the Kotlin for can be written as:

while(it.hasNext()){
 e=it.Next()
 instructions that use e
}

For example:

var a= mutableListOf<String>("a","b","c")
for(e in a.iterator()) println(e)
var it=a.iterator()
while(it.hasNext()){
   var e=it.next()
   println(e)
 }

The two for loops work in the same way.

The iterator method returns an initialized iterator for the collection. You can use this to retrieve each element in turn. Notice that you can't reset an iterator to the first value – you simply create a new instance. If this behavior doesn't suit you then simply include a start and/or end parameter in the constructor and modify the Next and hasNext methods accordingly.

Although iterators are generally linked to collection style data structures, they can also be used to generate sequences. In the case of a collection the Next method retrieves the next item in the collection, but for a sequence it simply computes the next value.

For example a CountToTen class would be something like:

class CountToTen():Iterator<Int>{
    private var i:Int=1
    override fun next(): Int {
       return i++
    }

    override fun hasNext(): Boolean {
       if(i>10) return false
        return true
    }
}

and it could be used anywhere you needed the sequence of numbers. For example:

val a=CountToTen()
for(e in a) println(e)

prints 1 to 10.

In most cases it would be better to create a class that implemented the Iterable interface. This has just one method, Iterator, which returns an Iterator object for the class in question.

Notice that an iterator is "lazy" in the sense that it doesn't compute the complete sequence of values at once, it only computes a value when needed.

Kotlin has a lot of facilities for functional programming, and in functional programming you often chain together functions like iterators which produce sequences. For efficiency it is important that these put off creating a sequence until it is actually needed – i.e. they need to be lazy in an extended sense. Kotlin provides the Sequence<T> type to allow you to use and implement iterators that are lazy in this broader sense.

Sequences

Although functional programming isn't the focus of this book it is worth giving an example of the way iterators and sequences  differ, if only because non-functional programmers find it hard to understand why there are two so closely related types.

Compare:

val s= sequenceOf( 1,2,3,4,5,6,7,8,9,10 )
val ms=s.map({println(it);it})
println("end")

which uses a sequence and:

val i= listOf(1,2,3,4,5,6,7,8,9,10)

val mi=i.map({println(it);it})

println("end")

which uses the iterator associated with the List. The map method simply applies the specified function, i.e. the println, to each of the elements of the sequence or collection.

If you run this you will discover that the map acting on the sequence doesn't print anything, but the iterator does.

The reason is that in the case of the sequence, map returns an unevaluated sequence ready for further operations. In the case of the List, the map returns another List after evaluating everything.  If you want to force the sequence to evaluate, you have to do something that makes use of its results.

For example:

val s= sequenceOf( 1,2,3,4,5,6,7,8,9,10 )
val ms=s.map({println(it);it})
println("end")
println(ms.last())

You will now see printed:

end
1
2
3
4
5
6
7
8
9
10
10

This happens as the sequence is evaluated to get the last element which is then printed. Notice that if you use first in place of last then only the first element is evaluated.

This is aggressively lazy and it has to be to ensure that many functional forms of simple algorithms are practical.

Ranges & Progressions

Kotlin provides two data types that make it easier to write for loops – the Range and the Progression.

Roughly speaking, the Range allows you to write things like:

for(i in 1..10)

and a Progression is a Range for which you can specify a step size. For example:

for(i in 1..10 step 2)

As already explained in connection with the for loop, Ranges and Progressions integrate into the for using the rangeTo function as an operator .. and the step function.

It isn't difficult to create your own Ranges and Progression.

As an example let's create a Range that works for dates.

In Java there are far too many Date classes in use at the moment, so for simplicity we will use the original Date class despite most of its methods being deprecated. To use this in a Kotlin project you need to add:

import java.util.*

To create a date range class we need to implement a class, DateRange, that  implements the Iterable interface, and ClosedRange interface which has the endInclusive property:

class DateRange(override val start: Date,
                override val endInclusive:Date)

                 :Iterable<Date>,ClosedRange<Date>

We need to add the override modifier to the constructor because the properties created by the primary constructor are hiding properties inherited from ClosedRange.

We also have to implement iterator which returns a suitable Iterator for the range:

class DateRange(override val start: Date,
                override val endInclusive:Date)
        :Iterable<Date>,ClosedRange<Date>{
    override fun iterator(): Iterator<Date> {
     return DateRangeIterator(start,endInclusive)
    }
}

where DateRangeIterator is the iterator we have yet to implement.

DateRangeIterator inherits from Iterator and ClosedRange. We only need to override the Iterator's next and hasNext methods and provide implementations of the ClosedRange start and endInclusive properties:

class DateRangeIterator(val start: Date,
                        val endInclusive:Date,
                        val stepDays:Long)
                  :Iterator<Date>{
  private var current=start
  override fun next(): Date {
       var next = Date(current.getTime())
        current.setTime(current.getTime()+
                         stepDays*24*60*60*1000)
        return next
    }
  override fun hasNext(): Boolean {
        if(current>endInclusive) return false
        return true
    }
}

Notice that as Date is an object we can't just use:

var next=current

because next and current would both reference the same object and changes made to current would also change next. To keep the values separate we have to create a new instance with the same values – i.e. we have to clone current.

To make the .. operator work we also need to define a suitable rangeTo function:

operator fun Date.rangeTo(other: Date) =

                           DateRange(this, other)

With this all defined, all that remains is to use the new Range class:

val dfm = SimpleDateFormat("yyyy-MM-dd")
val startDay=dfm.parse("2017-01-01")
val endDay=dfm.parse("2017-01-07")
  for(d in startDay..endDay){
       println(d)
  }

which prints:

Sun Jan 01 00:00:00 GMT 2017
Mon Jan 02 00:00:00 GMT 2017
Tue Jan 03 00:00:00 GMT 2017
Wed Jan 04 00:00:00 GMT 2017
Thu Jan 05 00:00:00 GMT 2017
Fri Jan 06 00:00:00 GMT 2017
Sat Jan 07 00:00:00 GMT 2017

 

<ASIN:1871962536>

<ASIN:1871962544> 



Last Updated ( Monday, 29 January 2018 )