Are there any tricks or common approaches for implementing the visitor pattern in Kotlin? Anything that might be non-obvious to beginners, but leads to more concise or organized code.
EDIT for clarification: I have an AST with many (~30) types of nodes in it. Currently each class implements its own print()
method, which I want to factor out into a separate Printer class. With the visitor pattern in place it'll be cleaner to add other AST-traversal classes, of which there will be several.
Read this answer for Java 8, everything it says also applies to Kotlin:
The additions made to the Java language do not render every old concept outdated. In fact, the Visitor pattern is very good at supporting adding of new operations.
This holds true for Kotlin. Like Java 8 it has Lambdas, SAM conversions, and interfaces that allow default implementations.
One change is if you are doing class instance type checking, instead of using a large if
statement for each instanceof
check, use the when
expression in Kotlin:
On that same Stackoverflow page in a different answer it talks about Lambdas being used and shows if
statement in Java deciding which lambda to call. Instead of their Java sample:
if (animal instanceof Cat) {
catAction.accept((Cat) animal);
} else if (animal instanceof Dog) {
dogAction.accept((Dog) animal);
} else if (animal instanceof Fish) {
fishAction.accept((Fish) animal);
} else if (animal instanceof Bird) {
birdAction.accept((Bird) animal);
} else {
throw new AssertionError(animal.getClass());
}
Use this Kotlin:
when (animal) {
is Cat -> catAction.accept(animal)
is Dog -> dogAction.accept(animal)
is Fish -> fishAction.accept(animal)
is Bird -> birdAction.accept(animal)
else -> throw AssertionError(animal.javaClass)
}
In Kotlin you don't need to cast since a smart cast is automatically made when the compiler sees the is
check for the instance type.
Also in Kotlin you can use Sealed Classes to represent your possible options in the hierarchy and then the compiler can determine if you have exhausted all cases meaning you do not need the else
in the when
statement.
Otherwise what holds true on that page, and other common answers to the same question is good information for Kotlin. I don't think it is as common to see an actual literal visitor pattern as much in Java 8, Scala or Kotlin, but rather some variation using lambdas and/or pattern matching.
Other related articles:
when
instead of a large if
) Here is an implementation that does not do double dispatch but does achieve the separation between the data and the code that acts on it.
The visitor's dispatch is done "by hand" using a when
expression (that is exhaustive) which requires less boiler plate because there is no need to override the accept()
method in all visitees and multiple visit()
methods in a visitor.
package visitor.addition
import visitor.addition.Expression.*
interface Visitor {
fun visit(expression: Expression)
}
sealed class Expression {
fun accept(visitor: Visitor) = visitor.visit(this)
class Num(val value: Int) : Expression()
class Sum(val left: Expression, val right: Expression) : Expression()
class Mul(val left: Expression, val right: Expression) : Expression()
}
class PrintVisitor() : Visitor {
val sb = StringBuilder()
override fun visit(e: Expression) {
val x = when (e) {
is Num -> sb.append(e.value)
is Sum -> stringify("+", e.left, e.right)
is Mul -> stringify("*", e.left, e.right)
}
}
fun stringify(name : String, left: Expression, right: Expression) {
sb.append('(')
left.accept(this); sb.append(name); right.accept(this)
sb.append(')')
}
}
fun main(args: Array<String>) {
val exp = Sum(Mul(Num(9), Num(10)), Sum(Num(1), Num(2)))
val visitor = PrintVisitor()
exp.accept(visitor)
println(visitor.sb) // prints: ((9*10)+(1+2))
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With