5

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.

David Soroko
  • 8,521
  • 2
  • 39
  • 51
Max
  • 4,882
  • 2
  • 29
  • 43
  • 1
    Are you asking for someone to write a sample visitor pattern for Kotlin on your behalf? The question is broad, open ended, and likely should be flagged for closing. – Jayson Minard Dec 29 '15 at 20:17
  • No, I'm just asking how to idiomatically implement a particular pattern in a language I'm still learning -- that's all. – Max Dec 29 '15 at 21:38
  • 1
    Post your best guess at it, and let us help you tune it. Probably you just would pass a lambda to the nodes that would traverse and call back the lambda with the node being visited. Start there, post code, then we can help from there. – Jayson Minard Dec 29 '15 at 22:03
  • Ok, I took a general approach to answering your question given I don't know anything about your class hierarchy and which problems come into play. See below. – Jayson Minard Dec 30 '15 at 19:37

4 Answers4

8

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:

Ulises
  • 9,115
  • 2
  • 30
  • 27
Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • 4
    Using big if-else/when clauses with type checks is what the visitor pattern tries to avoid, isn't it? Hence only the solution in the second of the related articles ([phd-in](http://amrphd.blogspot.com.uy/2014/12/visitor-design-pattern-vs-lambda.html)) seems to be valid, since the single dispatch is used there. But having an accept-method with n lambdas instead of one visitor object does not sound reasonable either. The issue becomes even more apparent, if you do not have access to the classes to be double-dispatched. Then you use extension methods, which are statically dispatched, either... – Juangamnik Jul 08 '16 at 18:00
  • 1
    The visitor pattern provides an approach for separating data from logic that acts on that data. Double dispatching is an implementation detail. – David Soroko Jan 13 '19 at 20:07
3

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))
}
David Soroko
  • 8,521
  • 2
  • 39
  • 51
0

To solve the problem I would use something like this:

interface Visitable {

    fun accept(visitor: Visitor)
}

Then implementations:

class House : Visitable {

    override fun accept(visitor: Visitor) {
         visitor.visit(this)
    }
}

class Car : Visitable {

    override fun accept(visitor: Visitor) {
         visitor.visit(this)
    }
}

Visitor itself:

interface Visitor {

    fun visit(entity: Car)

    fun visit(entity: House)
}

and implementation:

class Printer : Visitor {

    override fun visit(entity: Car) {
         println("Im in A Car")
    }

    override fun visit(entity: House) {
        println( "I'm in a House")
    }
}

Usage:

fun main(args: Array<String>) {

    val list = listOf<Visitable>(House(), Car())

    val printer = Printer()

    list.map { it.accept(printer) }
}

output:

I'm in a House
Im in A Car
Henry Twist
  • 5,666
  • 3
  • 19
  • 44
-3

A combination of companion objects and lambdas can be used to achieve dynamic visitation, like so:

interface Visitable { fun visit()}

class FooOne(): Visitable {
    val title1 = "111"
    companion object { var visit: (FooOne)->Unit  = {} }
    override fun visit() { FooOne.visit(this) }
}

class FooTwo(): Visitable {
    val title2 = "222"
    companion object { var visit: (FooTwo)->Unit  = {} }
    override fun visit() { FooTwo.visit(this) }
}

/* assign visitor functionality based on types */
fun visitorStars() {
    FooOne.visit = {println("In FooOne: ***${it.title1}***") }
    FooTwo.visit = {println("In FooTwo: ***${it.title2}***") }
}

/* assign different visitor functionality */
fun visitorHashes() {
    FooOne.visit = { println("In FooOne: ###${it.title1}###") }
    FooTwo.visit = {println("In FooTwo: ###${it.title2}###") }
}

fun main(args: Array<String>) {
    val foos = listOf<Visitable>(FooOne(), FooTwo())
    visitorStars()
    foos.forEach {it.visit()}
    visitorHashes()
    foos.forEach {it.visit()}
}

>>>
In FooOne: ***111***
In FooTwo: ***222***
In FooOne: ###111###
In FooTwo: ###222###
Basel Shishani
  • 7,735
  • 6
  • 50
  • 67
  • Why did you need to put a lambda in the companion object for this? It would've worked just as well with a regular method in the class itself. – cha0site May 13 '19 at 09:08