Parsing S-Expressions with Kotlin: Scanners

This is of a series on parsing with Kotlin which starts here
Let’s look at these expressions:
font
"some string"
(font 12.5 12.5)
Now consider the plight of our parser: it can only read one character at a time; it cannot put it back. More precisely, this is the interface we are consuming:
interface Iterator<T> {
fun next(): T
fun hasNext(): Boolean
}
(more, more precisely we are dealing with Iterator<Char>)
When we look at our examples, and we are at the beginning of any of our examples,
hasNext() will tell us we can proceed, and read() will gives us one of
'f', '"', or '('.
We see an 'f' and we know that we have a symbol in our hands, and now we have
to read the rest of the symbol, checking hasNext() before we read(),
and when we get to the end of the symbol hasNext() returns false, and we know
we are done.
In the case of the string, once we read the second '"', we also know we are done.
For numbers is more of the same (read digits, dots and minus signs…). Alas, in the
case of both numbers and symbols, we only know that we are done once we have read one
character past the chunk that we are interested in: we need to implement takebacksies.
That is where Scanner<T> comes in:
interface Scanner<T> {
fun get(): T?
fun peek(): T?
}
get() gets us the next T available, and peek() reads it, shows it to us,
and puts it back where it came from-ish. Well, excepts for I said we cannot put anything
back with iterators, so when we peek() we just read() the next T from
the iterator, then hide it behind our backs (unless we have already hidden it behind our
backs, etc., etc.). get() is an exercise of calling next() judiciously or passing
forward the T that we had previously peeked(). E.g.
CharScanner⌗
class CharScanner(private val s: Iterator<Char>) : Scanner<Char> {
var next: Char? = null
override fun get(): Char? =
if (next != null) {
val ret = next
next = null
ret
} else {
if (!s.hasNext()) null else s.next()
}
override fun peek(): Char? =
if (next != null)
next
else {
if (!s.hasNext()) null else {
val ret = s.next()
next = ret
ret
}
}
}
I really love the way in which get() and peek() exhibit symmetry, one
producing next and one consuming it, from opposite sides of mirror if
statements.
This code exhibits another key pillar of Kotlin: nullable types allow the embrace of
null in a safe way by enabling the developer to specify whether a variable can
be null, and enabling the compiler to stop us from allowing a null where it don’t
belong.
Next we’ll see how we can go from Scanner<Char> to Scanner<Token>.