Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to implement visitor pattern in Kotlin

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.

like image 480
Max Avatar asked Nov 09 '15 04:11

Max


2 Answers

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:

  • Visitor pattern with Java 8 default methods (Kotlin has them too)
  • Visitor pattern vs. lambda abstractions in Java 8 (Kotlin has lambdas too)
  • Visitor Pattern Java 8 Lambda implementation (Kotlin has lambdas too, remember to use a when instead of a large if)
like image 133
4 revs, 4 users 92% Avatar answered Sep 29 '22 01:09

4 revs, 4 users 92%


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))
}
like image 22
David Soroko Avatar answered Sep 29 '22 00:09

David Soroko