3
\$\begingroup\$

I'm writing a small custom DSL and interpreter as an exercise in understanding how the language stack (lexer/parser/(interpreter/compiler)) works. Because I want to be able to report the location of semantic errors, the AST needs to retain information about where the elements were in the source.

For this purpose I wrote a data class Point(val line: Int, val column: Int). All well and good, until I realized that manually specifying the position of EVERY node in the AST for unit tests on parsing would be prohibitively time consuming (and error prone). (Obviously, some tests need to do so to test the assigning of positions, but some is a lot fewer than all.)

To allow me to write more good unit tests quicker, I decided I wanted to default the position of all my nodes to a wildcard value that would match all valid Points. The parsed nodes would have the correct position, but the manually assembled AST to check against would not specify (and thus not test) the positions. This is the solution I came up with.

/**
 * DO NOT EXTEND THIS CLASS!!
 *
 * This is an custom open data class to allow a wildcard value.
 */
open class Point(val line: Int, val column: Int) {
    object Wildcard : Point(-1, -1) {
        override fun toString() = "Point.Wildcard"
        override fun copy() = this
    }

    override fun equals(other: Any?) = when {
        this === other         -> true
        other === Wildcard     -> true
        other !is Point        -> false
        this === Wildcard      -> true
        line != other.line     -> false
        column != other.column -> false
        else                   -> true
    }

    override fun hashCode(): Int { // generated
        var result = line
        result = 31 * result + column
        return result
    }

    override fun toString() = "Point(line=$line, column=$column)"
    operator fun component1() = line
    operator fun component2() = column
    open fun copy() = Point(line, column)
}

I wanted to keep all the behavior of a data class, but a data class cannot be open and this was the only way I could come up with of implementing the wildcard value. This solution still feels wrong and too much like a hack, however. It could easily be broken by outside code extending Point (as it must necessarily be available to be used) and so much of this code is just re-implementing what data class gives us for free. I feel like there must be a solution with a sealed class, but I haven't figured it out.

Recommended review points: idiomaticness (or not) of code, conventions, alternate solutions (and any other points)

\$\endgroup\$
2
\$\begingroup\$

Your equals method is not transitive (e.g. Point(2, 2) == Point.Wildcard and Point.Wildcard == Point(3, 3) but Point(2, 2) != Point(3, 3). See Any.equals - stdlib - Kotlin Programming Language for more details on the requirements for equals. You might also consider using EqualsTester from guava-testlib for testing your own implementations of equals.

If you define your own method instead of defining a custom, non-transitive equals implementation than your existing solution can become much simpler:

data class Point(val line: Int, val column: Int) {
    companion object {
        val wildcard = Point(-1, -1)
    }

    infix fun matches(other: Point): Boolean {
        return this == other || other === wildcard || this === wildcard
    }
}

Notes:

  1. Point(2, 2) matches Point(-1, -1) will return false.
  2. Point(2, 2) matches Point.wildcard will return true.
  3. If you want both to return true then replace the === with == in matches.
  4. Marking matches as infix is purely optional but it seems to me like a good fit.
\$\endgroup\$
  • 1
    \$\begingroup\$ Is ~= a valid operator in Kotlin? That seems like a (somewhat) standard operator that is used for this kind of fuzzy match. \$\endgroup\$ – CAD97 Jan 12 '17 at 17:59
  • 1
    \$\begingroup\$ I have not seen such an operator in Kotlin. \$\endgroup\$ – mfulton26 Jan 12 '17 at 18:01
  • 1
    \$\begingroup\$ This is the solution I couldn't find; good catch! I assume that ::matches would propagate up my AST to allow for fuzzy matching while ::equals checks for the correct equality, then. Is there a way to auto-implement ::matches the way ::equals is (most likely with reflection on an interface)? \$\endgroup\$ – CAD97 Jan 12 '17 at 18:03
  • 1
    \$\begingroup\$ If I understand correctly, I think you would simply need interface Matchable { fun matches(other: Any) } and then implement it appropriately. Probably with a single Matchable.wildcard instance to use across all matchables (instead of Point.wildcard, Blah.wildcard, etc.). \$\endgroup\$ – mfulton26 Jan 12 '17 at 18:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.